aboutsummaryrefslogtreecommitdiff
path: root/2022/puzzle-7.lisp
blob: 08dd08d9092122716aaf8e3f0fd52fa9d3ec9e23 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
(load "lib")
(defparameter input (uiop:read-file-string "7-input"))
(defparameter lines (get-lines input)) ;; first line is ALWAYS "$ cd /"

(defun tokenise-lines (lines)
  "Split each LINE in LINES by space"
  (mapcar (lambda (line)
       (mapcar #'clist-to-string
          (split-by-completely (string-to-clist line) #\Space)))
     lines))

(defun is-token-cmd? (token)
  "Checks if TOKEN is a command (by the first member)"
  (string= (car token) "$"))

(defun till-next-cmd (tokens)
  "Iterates over TOKENS till a command is found, returning the remaining
tokens (including command)."
  (loop
    for token-set on tokens
    if (is-token-cmd? (car token-set))
      return token-set))

;;; Functions to manage a directory structure
(progn
  #| What is a directory structure? You have some root directory, with
  some entries.  Your current working directory (CWD), at the start of
  the program, will be at the root but later on could be any descendant
  of the root.  How do we manage moving around the directory structure
  and ensure updates to the overall structure?

  We should maintain a HISTORY stack where
  CAR(HISTORY) = (NAME of CWD . State of CWD according to parent).

  So the top of the history stack represents the current directory
  structure in the form it'll inhabit in its parents.  We need a way to
  update the top of the history stack with the CWD.

  UPDATE-CURRENT-DIRECTORY(CWD, HISTORY) {
  CWD-ENTRY = PEEK(HISTORY);
  IF NULL(CWD-ENTRY)
  HISTORY = ((NIL . CWD))
  ELSE
  SETF((CDR CWD-ENTRY), CWD);
  }
  |#
  (defun update-current-directory (cwd history)
    "Updates the top of HISTORY (structure of CWD) with CWD i.e. set the
'state' of the top of the the HISTORY stack to CWD."
    (if (null (car history))
        (values
         cwd
         `((nil ,@cwd)))
        (progn
          (setf (cdr (car history)) cwd)
          (values
           cwd
           history))))

  #|
  Say CWD = {... a = directory, ...}.  How do I go down to `a`?

  DOWN-DIRECTORY(a, CWD, HISTORY) {

  UPDATE-CURRENT-DIRECTORY(CWD, HISTORY); <-- This is so we don't lose
  any current information
  HISTORY = ACONS(a, CWD@a, HISTORY);
  CWD     = CWD@a;
  }
  |#
  (defun down-directory (name cwd history)
    (multiple-value-bind (cwd history) (update-current-directory cwd history)
      (let ((cwd@a (cdr (assoc name cwd :test #'string=))))
        (values
         cwd@a
         (acons name cwd@a history)))))

  #|
  How do we ascend the directory structure?  We need to account for 3 cases:
  1) At the root
  2) Parent is the root
  3) Parent is any directory


  Then UP-DIRECTORY(CWD, HISTORY) {
  PAIR   = POP(HISTORY);               <-- (CWD-NAME . PREVIOUS-CWD-STATE)
  if NULL(CAR(PAIR))
  then error(AT ROOT)
  else
  PARENT = PEEK(HISTORY);              <-- Parent of CWD (PARENT-NAME . PARENT-STATE)
  ENTRY = ASSOC(CAR(PAIR) CDR(PARENT)) <-- Gives us the CWD entry in the parent
  SETF CDR(ENTRY) CWD                  <-- Update the parent
  CWD = CDR(PARENT)                    <-- Update CWD
  }
  |#
  (defun up-directory (cwd history)
    (when (null (caar history))
      ;; At root, so we can't go further up
      (error "Directory already at root"))
    ;; We can't use names as in the pseudocode because we need to mutate
    ;; this specific pointer: Lisp copies by value so we won't be
    ;; mutating it if we use names.
    (setf
     ;; CDR(ENTRY)
     (cdr
      ;; ENTRY
      (assoc
       ;; CAR(PAIR)
       (car (car history))
       ;; CDR(PARENT)
       (cdr (cadr history))
       :test #'string=))
     cwd)
    (values
     (cdr (cadr history))
     (cdr history)))

  #|
  Finally, how do we go from some child all the way back to the root?
  UP-TO-ROOT(CWD, HISTORY) {
  IF (NULL (CAAR HISTORY)) <-- Means we're at the root
  RETURN
  ELSE
  UP-DIRECTORY(CWD, HISTORY)
  UP-TO-ROOT(CWD, HISTORY)
  }
  |#
  (defun up-to-root (cwd history)
    (if (null (caar history))
        (values
         cwd
         history)
        (multiple-value-bind (cwd history) (up-directory cwd history)
          (up-to-root cwd history)))))

;;; Functions to parse the shell output
(progn
  (defun parse-ls (tokens)
    "Converts the following TOKENS till a command (via IS-TOKEN-CMD?) into a
directory structure i.e. an alist of string names by their content."
    (loop for token in tokens
          until (is-token-cmd? token)
          collect
          (let ((size (car token))
                (name (cadr token)))
            (if (string= size "dir")
                `(,name . nil)
                `(,name . ,(parse-integer size))))))

  (defun parse-cd (name cwd history)
    "Converts TOKEN into an action on CWD and HISTORY then returns them"
    (cond
      ((string= "/" name)
       (up-to-root cwd history))
      ((string= ".." name)
       (up-directory cwd history))
      (t
       (down-directory name cwd history))))

  (defun parse-next-command (tokens cwd history)
    "Parses the next command available in TOKENS, managing CWD and HISTORY.
Assumes CAR(TOKENS) is a command (see IS-TOKEN-CMD?)"
    (cond
      ((null tokens)
       (multiple-value-bind (cwd history) (up-to-root cwd history)
         (values
          tokens
          cwd
          history)))
      ((string= (cadar tokens) "ls")
       (values
        (till-next-cmd (cdr tokens))
        (parse-ls (cdr tokens))
        history))
      ((string= (cadar tokens) "cd")
       (multiple-value-bind (cwd history) (parse-cd (caddar tokens) cwd history)
         (values
          (till-next-cmd (cdr tokens))
          cwd
          history)))))

  (defun parse-all-tokens (tokens &optional cwd history)
    "Recursively parse all TOKENS, returning (CWD . HISTORY) from the
perspective of the root."
    (if (null tokens)
        (multiple-value-bind (cwd history) (up-to-root cwd history)
          (values
           cwd
           history))
        (multiple-value-bind (tokens cwd history)
            (parse-next-command tokens cwd history)
          (parse-all-tokens tokens cwd history)))))

(defun is-record-directory? (record)
  "Checks if the record (output from parse-tokens) is a directory by
checking if the CDR is a list."
  (listp (cdr record)))

(defun strip-file-names (record)
  "Deletes file names, leaving the corresponding file sizes and
directory names (and the wrapping list structure)."
  (if (is-record-directory? record)
      (cons (car record)
            (loop
              for entry in (cdr record)
              collect
              (strip-file-names entry)))
      (cdr record)))

(defun get-directory-sizes (dir &optional current)
  "Computes the size of the directory dir, as well as all subdirectories.
Returns both the overall size of dir as well as an alist of sizes for
subdirectories."
  (if (not (listp dir))
      (values
       dir
       nil)
      (let ((name (car dir))
            (size (loop
                    for record in (cdr dir)
                    sum
                    (multiple-value-bind (size dir-alist) (get-directory-sizes record nil)
                      ;; This is for the alist of subdirectory sizes
                      (setq current (concatenate 'list dir-alist current))
                      size))))
        (values
         size
         (acons name size current)))))

(defparameter TOTAL-SIZE 70000000)
(defparameter REQUESTED-SIZE 30000000)
(multiple-value-bind (_ history)
    (parse-all-tokens (tokenise-lines lines))
  ;; Since we're at the root, CAR(history) is the entire file structure
  (declare (ignore _))
  (multiple-value-bind (sum size-list)
      (get-directory-sizes
       (strip-file-names
        ;; normalize the name of root
        (cons "/" (cdr (car history)))))
    (format t "Round 1: ~s~%"
            (reduce #'+ (loop for rec in size-list
                              if (< (cdr rec) 100000) ; only records that are at most 100000
                                collect (cdr rec))))
    (let* ((root-size sum)
           (free-space (- TOTAL-SIZE root-size))
           (extra-space-req (- REQUESTED-SIZE free-space)))
      (format t "Round 2: ~s~%"
              (cdar
               (sort (loop
                       for rec in size-list
                       ;; only records whose size is at least the space required
                       if (>= (cdr rec) extra-space-req)
                         collect rec)
                     (lambda (rec1 rec2)
                       (< (cdr rec1) (cdr rec2)))))))))