(module parse-playlist mzscheme
(require (lib "contract.ss")
(lib "lex.ss" "parser-tools")
(lib "yacc.ss" "parser-tools")
(lib "etc.ss")
(lib "pregexp.ss")
(lib "plist.ss" "xml")
(lib "list.ss"))
(define-struct music-db (indexer records outputter))
(define/contract construct-music-db
((string? . -> . ((listof any/c) . -> . any)) (listof (listof any/c)) ((listof (listof any/c)) port? . -> . void?) . -> .
music-db?)
(lambda (indexer records outputter)
(make-music-db indexer records outputter)))
(provide/contract [music-db? (any/c . -> . boolean?)])
(provide/contract [parse-playlist (string? . -> . music-db?)])
(provide/contract [music-db-indexer (music-db? . -> . (string? . -> . ((listof any/c) . -> . any)))]
[music-db-records (music-db? . -> . (listof (listof any/c)))]
[music-db-outputter (music-db? . -> . ((listof (listof any/c)) port? . -> . void?))])
(provide/contract [required-fields (case-> ( -> (listof string?))
((listof string?) . -> . void?))]
[verbose-output? (case-> ( -> boolean?)
(boolean? . -> . void?))])
(define required-fields (make-parameter `("Name" "Location" "Size")))
(define verbose-output? (make-parameter #t))
(define (make-indexer record-name-list)
(let ([index (map list
record-name-list
(build-list (length record-name-list)
(lambda (index)
(lambda (record)
(list-ref record index)))))])
(lambda (name)
(let ([result (assoc name index)])
(unless result
(error 'indexer "field name ~v not found" name))
(cadr result)))))
(define (make-outputter record-name-list)
(lambda (record-list port)
(for-each (lambda (record)
(let loop ([fields record])
(when (car fields)
(fprintf port "~a" (car fields)))
(if (null? (cdr fields))
(display #\return port)
(begin (display #\tab port)
(loop (cdr fields))))))
(cons record-name-list record-list))))
(define-tokens normal (field tab newline eof))
(define (post-parse field)
(cond [(string=? field "") #f]
[(pregexp-match "^[0-9 ]*$" field) (string->number field)]
[else field]))
(define field-pattern (byte-regexp #"[^\r\t]*"))
(define (parse-file port)
(letrec ([read-record
(lambda ()
(let loop ()
(let ([next-field (regexp-match field-pattern port)])
(when (null? next-field)
(error "regexp-match returned zero matches"))
(when (not (null? (cdr next-field)))
(error "regexp-match returned more than one match: ~v" next-field))
(let* ([field-content (bytes->string/latin-1 (car next-field))]
[real-field (if (= (string-length field-content) 0)
#f
(let ([try-num (string->number field-content)])
(or try-num
field-content)))]
[next-char (read-char port)])
(if (eof-object? next-char)
(error "unexpected location for eof")
(if (eq? next-char #\return)
(list real-field)
(cons real-field (loop))))))))])
(let loop ()
(if (eof-object? (peek-char port))
null
(cons (read-record)
(loop))))))
(define (check-record-validity index-list record-list indexer)
(let ([index-length (length index-list)])
(filter (lambda (record)
(if (not (= (length record) index-length))
(begin (when verbose-output?
(fprintf (current-error-port) "omitting record for wrong length: ~e\n" record))
#f)
(let loop ([req-fields (required-fields)])
(if (null? req-fields)
#t
(if (not ((indexer (car req-fields)) record))
(begin (when verbose-output?
(fprintf (current-error-port) "omitting record as missing field ~v: ~e\n" (car req-fields) record))
#f)
(loop (cdr req-fields)))))))
record-list)))
(define (parse-playlist filename)
(call-with-input-file filename
(lambda (port)
(let ([parsed-file (parse-file port)])
(let ([indexer (make-indexer (car parsed-file))])
(construct-music-db indexer
(check-record-validity (car parsed-file)
(cdr parsed-file)
indexer)
(make-outputter (car parsed-file))))))))
)