Let's start Scheme

2012-02-11

ChickenのPackrat

Markdownのパーサーがいるのでpackratを使ってパーサを書こうとしたのだが、いまいち使い方が分からなかった。なので、練習がてらCSVのパーサを書いてみることにしてみた。
とりあえずいい加減なパーサがこんな感じ。
(import (rnrs)
 (packrat)
 (srfi :14 char-set))

(define *text-set* 
  (ucs-range->char-set! #x2d #x7e #f
 (ucs-range->char-set! #x23 #x2b #f
       (ucs-range->char-set #x20 #x21))))
(define (any results)
  (let loop ((acc '()) (results results))
    (let ((ch (parse-results-token-value results)))
      ;; it's just testing
      (if (and ch (char-set-contains? *text-set* ch))
   (loop (cons ch acc) (parse-results-next results))
   (make-result (list->string (reverse! acc)) results)))))
(define (textdata results)
  (any results))

(define parser
  (packrat-parser 
   (begin 
     (define (crlf results)
       (let ((ch (parse-results-token-value results)))
  (case ch
    ((#\linefeed) (make-result "" results))
    ((#\return)
     (let ((ch (parse-results-token-value (parse-results-next results))))
       (if (char=? #\linefeed ch)
    (make-result "" results)
    (field-entry results))))
    (else (field-entry results)))))

     file)

   (file    ((h <- header '#\linefeed r <- records) (cons h r))
     ((r <- records) r))
   (header  ((n <- names) (cons :header n)))
   (records ((f <- fields '#\linefeed r <- records) (cons (cons :record f) r))
     ((f <- fields) (list (cons :record f))))
   (fields  ((f <- field-entry '#\, fs <- fields) (cons f fs)) ;; (1)
     ((f <- field-entry) (list f)))
   (names   ((n <- field-entry '#\, ns <- fields) (cons n ns))
     ((n <- field-entry) (list n)))
   (field-entry ((e <- textdata) e))))

(define (generator p)
  (let ((ateof #f)
 (pos (top-parse-position "")))
    (lambda ()
      (if ateof
   (values pos #f)
   (let ((x (read-char p)))
     (if (eof-object? x)
  (begin
    (set! ateof #t)
    (values pos #f))
  (let ((old-pos pos))
    (set! pos (update-parse-position pos x))
    (values old-pos (cons x x)))))))))

(define (parse-csv p)
  (let ((result (parser (base-generator->results (generator p)))))
    (if (parse-result-successful? result)
 (parse-result-semantic-value result)
 (apply assertion-violation
        (let ((e (parse-result-error result)))
   (list 'parse-csv
         (parse-error-messages e)
         (parse-position->string (parse-error-position e))
         (parse-error-expected e)))))))
(call-with-input-file "test.csv"
  (lambda (p)
    (parse-csv p))))
#|
入力ファイル
aaa,bbb
ccc,ddd,eee
fff,ggg,hhh
a,b,c
出力
((:header "aaa" "bbb")
 (:record "ccc" "ddd" "eee")
 (:record "fff" "ggg" "hhh")
 (:record "a" "b" "c"))
|#
generatorはドキュメントにあったものをちょっと改変しただけ。多分これが基本なんだろう。
かなり適当で本来はCRLFなのがLFだけだったり、エスケープされたものを認識しなかったりするがまぁ動く。気になったのは「/」の使い方で、こんな感じには使えないという不便さであった。
(entry (((/ (e <- escaped) (ne <- non-escaped))) (if e e ne)))
展開されたコードをみたらまぁ納得なのだが、これがやれないとなると結構面倒だよなぁ。あとこのライブラリは「*」とか「+」とか「?」がないので、それらを書こうと思ったら、(1)のように条件を2つ作る必要がある。

やっぱり正規表現をつかってやるべきだろうか。

No comments:

Post a Comment