Tuesday, October 21, 2014

Ancient Laptops and being a Heroic Boyfriend - Text Formatting inspired Stories

You might think a topic like text formatting would be pretty bland. Not so! I have not one, but two stories inspired by the topic. Allow me to share.

I can remember it vividly: I'm 13 years old and working away on my very own laptop. Specifically, a NEC PC-8500, with a whopping 64K (yes kilobytes!) of RAM. I'm typing up an essay for school in the WordStar word processor and having a heck of a time. Specifically, the paragraphs are all askew, with some lines being shorter than others. In the end, I'm left fighting with the computer trying to get the different lines to have somewhat equal length. What a pain.

Looking back, I was running into a fairly classic text formatting problem. Either WordStar had automatic word wrap and I was defeating it by hitting enter at the end of some lines, or it had a manual function to refill paragraphs that I didn't know how to operate. All I know is, this is one of my earliest memories of software appearing to be buggy, but actually performing as designed. Little did I know that I'd pretty much make this fight my life's work by becoming a programmer. Now, on a nearly daily basis I look at software and think "hmmm, it can't possibly be behaving that way...." And yet, it almost always is. And usually it's my fault!

Fast forward from middle school to college for the second story. I'm on Unix Talk with my long-distance girlfriend, and she starts telling me about the arduous task in front of her. She's manually re-formating a massive final lab report. It's taking her forever to adjust each line to the right length. I finally convince her to send me a copy. A minute or two later she receives it back, perfectly formatted. How'd I work this bit magic? Why thanks to the Unix fmt command.

You may be thinking, big deal, you formatted some text. Turns out, it was a big deal. That girlfriend is now my wife of almost 16 17 years. And when I started describing the story a few days ago, she immediately started mentioning details I'd forgotten. That little bit of unix-manship, is probably in the top 10 list of amazing things I've done for my wife. Never underestimate the power of the command line.

In honor of both those stories, I give you a Scheme implementation of fmt. Incidentally, this answers the first part of the Programming-Praxis Challenge, I've still got the second part to complete.

; http://programmingpraxis.com/2014/10/07/text-formatting/

(define (empty? str)
  (= (string-length str) 0))

(define (++ . args)
  (apply string-append
         (map 
           (lambda (any)
             (cond ((string? any) any)
                   ((number? any) (number->string any))
                   ((char? any) (string any))))
         args)))

(define (g options key . default)
  (cond ((assoc key options) => cdr)
        (else (if (null? default)
                  (error "Missing default: " key options)
                  (car default)))))
            
(define (read-token port)
  (let loop ((buffer ""))
    (let ((c (peek-char port)))
      (cond ((eof-object? c)
             (if (empty? buffer)
                 (cons 'eof (read-char port))
                 (cons 'word buffer)))
            ((equal? c #\newline)
             (if (empty? buffer)
                 (cons 'newline (read-char port))
                 (cons 'word buffer)))
            ((equal? c #\space)
             (if (empty? buffer)
                 (begin (read-char port) (loop buffer))
                 (begin (read-char port) (cons 'word buffer))))
            (else
              (loop (++ buffer (read-char port))))))))

(define (output line port opts)
  (let* ((width (g opts 'width 60))
         (align (g opts 'align 'left))
         (delta (- width (string-length line))))
    (case align
      ((left) (display line port))
      ((right)
       (display (make-string delta #\space) port)
       (display line port))
      ((center)
       (display (make-string (floor (/ delta 2)) #\space) port)
       (display line port)))
    (newline port)))
    
(define (handle-word word buffer out width loop opts)
 (let ((line (if (empty? buffer) "" (++ buffer " "))))
   (if (> (string-length (++ line word)) width)
       (begin
         (output buffer out opts)
         (loop word))
       (begin
         (loop (++ line word))))))


(define (fmt in out opts)
  (call-with-input-file in
    (lambda (pin)
      (call-with-output-file out
        (lambda (pout)
          (format pin pout opts))))))

(define (format in out opts)
  (let ((width (g opts 'width 60)))
    (let loop ((buffer ""))
      (let ((next (read-token in)))
        (case (car next)
          ((eof) (output buffer out opts))
          ((word)
           (handle-word (cdr next) buffer out width loop opts))
          ((newline)
           (let ((peek (read-token in)))
             (case (car peek)
               ((newline)
                (output  buffer out opts)
                (newline out)
                (loop ""))
               ((eof) (output buffer out opts))
               ((word)
                (handle-word (cdr peek) buffer out width loop opts))
               (else (error "Unknown peek token:" peek)))))
          (else (error "Unknown token:" next)))))))

(define in "/sdcard/Documents/input.txt")
(define out  "/sdcard/Documents/output.txt")
(define opts '((width . 30)))

(define (range low high)
  (let loop ((i low) (result '()))
    (if (> i high) (reverse result)
        (loop (+ 1 i) (cons i result)))))

(define-macro (repeat qty . body)
  `(for-each 
    (lambda (i) ,@body) (range 1 ,qty)))

No comments:

Post a Comment