2012年12月30日日曜日

Clojureでテキスト入出力

Clojureでテキスト入出力を行う方法のメモ。

1 テキストファイル

;; 出力
(spit "test.txt" "Hello, World")

;; 入力
(assert (= "Hello, World" (slurp "test.txt")))

2 文字列

(with-open [in (java.io.StringReader. "Hello")]
  (assert (= \H (char (.read in))))
  'ok)

(with-open [in (clojure.java.io/reader (.getBytes "Hello"))]
  (assert (= "Hello" (.readLine in)))
  'ok)

(with-in-str "hoge"
  (assert (= "hoge" (read-line))))

3 XML

;; [org.clojure/data.xml "0.0.6"]
(require '[clojure.data.xml :as xml])
(xml/parse-str "<x id='1'><y>a</y><y id='2'>b</y></x>")

;; [enlive "1.0.0"]
(require '[net.cgrand.enlive-html :as enlive])
(let [s "<x id='1'><y>a</y><y id='2'>b</y></x>"
      x (with-in-str s (enlive/xml-resource *in*))]
  (assert (= '("b") (:content (first (enlive/select x [:#2]))))))

4 CSV

;; [org.clojure/data.csv "0.1.2"]
(require '[clojure.data.csv :as csv])

(let [s "a,\"b,c\",d\ne,fg,h\n"]
  (println (csv/read-csv s))
  (csv/write-csv *out* (csv/read-csv s))
  (assert (= s (with-out-str
                 (csv/write-csv *out* (csv/read-csv s)))))
  'ok)

5 JSON

;; [org.clojure/data.json "0.2.0"]
(require '[clojure.data.json :as json])
(let [m (json/read-str "{\"k1\": 1, \"k2\": \"val\"}")]
  (println m)
  (assert (= m (json/read-str (json/write-str m))))
  (with-open [out (clojure.java.io/writer "tmp.json")]
    (json/write m out))
  (with-open [in (clojure.java.io/reader "tmp.json")]
    (assert (= m (json/read in))))
  'ok)

;; オプションとして :key-fn や :value-fn を指定すると read/write 時にキーと値を変換できる
(println (= {:key 1.0}
            (json/read-str "{\"key\" : 1}"
                           :key-fn keyword
                           :value-fn #(double %2))))

6 INIファイル

;; [clojure-ini "0.0.1"]
(require '[clojure-ini.core :as ini])

(spit "test.ini" "[a]\nb=c\nd=e\n")
(assert (= {:a {:b "c", :d "e"}}
           (ini/read-ini "test.ini" :keywordize? true)))

2012年12月16日日曜日

[メモ]衛星の高度から速度・周期を求める

衛星の高度から速度・周期を求める式が JAXAのページ に書いてありました。

6378[km]赤道半径
H [km]地表からの高度
V [km/s]速度
T [s]周期

$V[km/s] = (\frac{398600[km^3/s^2]}{(6378[km] + H[km])})^\frac{1}{2}$

$T[s] = \frac{2\pi(6378[km] + H[km])}{V[km/s]}$

(defn v [h]
  (Math/sqrt (/ 398600 (+ 6378 h))))

(defn t [h v]
  (/ (* 2 Math/PI (+ 6378 h)) v))

(defn h->t [h]
  (t h (v h)))
;; 静止軌道(35786km) で 23時間56分
user=> (h->t 35786)
86163.61830152525

user=> (/ *1 3600)
23.934338417090345

Racketのサンドボックス機能

Racketには評価時のリソースの利用を制限するサンドボックス機能があります。

このあたりを参照

使用可能なメモリを制限して式を評価するプログラムを書いてみます。

#lang racket
(require racket/sandbox)

(define e (parameterize ((sandbox-memory-limit 1)) ;; 1MByte
            (make-evaluator 'racket/base)))

;; 1KByteくらい (1MByte制限)
(printf "~s\n" (vector-length (e '(make-vector 1000))))
(flush-output)

;; 1MByteくらい (制限なし)
(printf "~s\n" (vector-length (make-vector 1000000)))
(flush-output)

;; 1MByteくらい (1MByte制限)
(printf "~s\n" (vector-length (e '(make-vector 1000000))))
(flush-output)
> racket test.rkt
1000
1000000
out of memory 
  context...:
   /usr/racket/collects/racket/sandbox.rkt:355:0: call-with-limits
   /usr/racket/collects/racket/sandbox.rkt:403:0: call-with-custodian-shutdown
   /usr/racket/collects/racket/private/more-scheme.rkt:146:2: call-with-break-parameterization
   /usr/racket/collects/racket/sandbox.rkt:760:5: loop

[メモ]C#で時刻文字列を扱う・XMLファイルを読み込む

C#で時刻文字列のパース・フォーマット指定して文字列に変換

using System;
using System.Globalization;

class Test
{
  public static void Main()
  {
    DateTime tmp = DateTime.Parse("2012-12-16T00:00:00Z");
    Console.WriteLine(TimeZoneInfo.ConvertTimeToUtc(tmp).ToString("yyyy/MM/dd HH:mm:ss"));
    return;
  }
}
2012/12/16 00:00:00

C#でXMLファイルをパース

// mono-csc xml.cs -r:System.Xml.Linq.dll
using System;
using System.Xml;
using System.Linq;
using System.Xml.Linq;
using System.Collections.Generic;

// test.xml
// <a>
//  <b>b1</b>
//  <b>b2</b>
//  <c>c1</c>
// </a>

class Test
{
  static void Main()
  {
    var doc = XElement.Load("test.xml", LoadOptions.SetLineInfo);
    var elems = 
      from x in doc.Descendants()
      select x;

    foreach(var e in elems)
      {
        var text = String.Format("{0}:{1}", ((IXmlLineInfo)e).LineNumber, e.Value);
        Console.WriteLine(text);
      }

    return;
  }
}
2:b1
3:b2
4:c1

Clojureのリテラルその他

リテラル
内容X(type 'X)
10進数2 => 2java.lang.Long
8進数010 => 8java.lang.Long
16進数0x10 => 16java.lang.Long
2進数2r10 => 2java.lang.Long
36進数36r10 => 36java.lang.Long
BigInt3N => 3Njava.lang.BigInt
小数1.1 => 1.1java.lang.Double
小数1.0E8 => 1.0E8java.lang.Double
BigDecimal2.2M => 2.2Mjava.math.BigDecimal
有理数22/7 => 22/7clojure.lang.Ratio
シンボルabc あいう !clojure.lang.Symbol
キーワード:a => :aclojure.lang.Keyword
キーワード::a => :user/aclojure.lang.Keyword
文字列"abc" "\t\n"java.lang.String
文字\a \あ \spacejava.lang.Character
リスト(1 2)clojure.lang.PersistentList
空リスト()PersistentList$EmptyList
ベクタ[1 2]clojure.lang.PersistentVector
マップ{:k1 :v1 :k2 :v2}clojure.lang.PersistentArrayMap
セット#{:a :b :c}clojure.lang.PersistentHashSet
nil (null)nilnil
truetruejava.lang.Boolean
falsefalsejava.lang.Boolean
正規表現#"\d{2}\w+"java.util.regex.Pattern
コンストラクタ#java.lang.Double[1.1]クラス・レコード・タイプのインスタンス


読み込み時に他のフォームに置き換えられる表現
内容X'X
Quote'a(quote a)
Deref@a(deref a)
typehint^String^{:tag String}
Var-quote#'a(var a)
無名関数#(…)(fn [args] (…))
S式コメント#_Xフォーム X をコメントとして扱う
Syntax-quote`(…)-
読み込み時の評価#=(+ 1 2)3

2012年12月13日木曜日

正規表現を利用して文字列を作成する

( Lisp Advent Calendar 2012 の13日目の記事です)

テストなどのために適当なデータを作成したいことがあります。
この時、データをランダムに作成できると、楽ができる上に 自分の考えていなかった入力パターンから問題を見つけることができて良さそうです。

作成したいデータが数値なら、疑似乱数生成用の関数(rand,randomなど)を使えば事足ります。
文字列を作成したい場合は、正規表現を利用すれば楽ができそうです。

Clojure には、正規表現からその正規表現にマッチするような文字列を作成してくれる re-rand という ライブラリがあります。

[re-rand "0.1.0"]
user> (require [re-rand :as r])

user> (repeatedly 3 #(r/re-rand #"\d{4}/\d{2}/\d{2} \d\d:\d\d:\d\d"))
("7511/27/85 13:37:43" "6728/87/88 96:68:15" "1536/63/37 90:23:02")

user> (repeatedly 3 #(r/re-rand #"[0-9零一二三四五六七八九]{1,10}"))
("六二零七" "1七8" "二三七八一2八1")

Common Lisp には・・・と思って調べて見ましたが、見つけることができませんでした。 仕方ないので自分で作りましょう。

幸い、一番大変そうな正規表現のパースは cl-ppcre の parse-string 関数で行えます。

(asdf:load-system :cl-ppcre)

(defpackage :random-string
  (:use :cl)
  (:export random-string
           *repeat-limit*
           *charset-every*
           *charset-digit*
           *charset-word*
           *charset-whitespace*))

(in-package :random-string)

(defvar *repeat-limit* 64)

(defvar *charset-everything*
  (lambda () (code-char (random 256) )))

(defvar *charset-digit* "0123456789")

(defvar *charset-word*
  (concatenate 'string
   "abcdefghijklmnopqrstuvwxyz"
   "ABCEFGHJIJKLMNOPQRSTUVWXYZ"
   "!\"#$%'()-=^~\\|[]{};:+*,.<>/?_"))

(defvar *charset-whitespace*
  (format nil "%c%c%c" #\space #\newline #\tab))

(defun one-of (obj)
  (if (functionp obj)
      (funcall obj)
      (let ((count (length obj)))
        (elt obj (random count)))))

(defun random-string (regexp)
  (generate (ppcre:parse-string regexp)))

(defmethod generate ((obj string))
  obj)

(defmethod generate ((obj character))
  (string obj))

(defmethod generate ((obj list))
  (generate/list (first obj) (rest obj)))

(defmethod generate ((obj (eql :digit-class)))
  (string (one-of *charset-digit*)))

(defmethod generate ((obj (eql :everything)))
  (string (one-of *charset-everything*)))

(defmethod generate ((obj (eql :whitespace-char-class)))
  (string (one-of *charset-whitespace*)))

(defmethod generate ((obj (eql :word-char-class)))
  (string (one-of *charset-word*)))

(defmethod generate/list ((cls (eql :sequence)) rest)
  (apply #'concatenate 'string (mapcar #'generate rest)))

(defmethod generate/list ((cls (eql :char-class)) rest)
  (string (one-of (mapcar #'generate rest))))

(defmethod generate/list ((cls (eql :range)) rest)
  (string
   (destructuring-bind (from to) rest
     (code-char
      (+ (char-code from)
         (random (1+ (- (char-code to) (char-code from)))))))))

(defmethod generate/list ((cls (eql :INVERTED-CHAR-CLASS)) rest)
  (error "unsupported expression: INVERTED-CHAR-CLASS"))

(defmethod generate/list ((cls (eql :greedy-repetition)) rest)
  (destructuring-bind (min max gen-cls) rest
    (let ((max (or max (+ min *repeat-limit*))))
      (apply #'concatenate 'string
             (loop :repeat (+ min (random (1+ (- max min))))
                :collect (generate gen-cls))))))

(defmethod generate/list ((cls (eql :register)) rest)
  (apply #'concatenate 'string (mapcar #'generate rest)))

(defmethod generate/list ((cls (eql :alternation)) rest)
  (generate (one-of rest)))

CL-USER> (random-string:random-string "b(an)+a")
"banananananananananananananananana"

CL-USER> (dotimes (_ 3)
           (print (random-string:random-string "[012]\\d{3}-((0[1-9])|(1[12]))-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z")))
"2056-09-58T98:80:10Z" 
"2057-02-82T36:65:58Z" 
"1077-11-18T77:72:22Z"

CL-USER> (dotimes (_ 3)
           (print (random-string:random-string "[\\d零一二三四五六七八九]{1,5}")))

"二二四四" 
"五68" 
"二七"

文字集合の選択などで偏りが出てしまいますが、適当なデータを作成する程度なら十分利用できるかなぁ、と思います。

今回書いたコードでは、やりたいことの9割くらいを正規表現ライブラリのcl-ppcreが行ってくれています。
Lisp使いは「全部自分で書く」みたいなイメージがありますが、 leiningen や Quicklisp を使うとライブラリのインストールは簡単ですし、 やりたいことにマッチするライブラリがあったら、ありがたく使わせてもらいましょう。

2012年12月10日月曜日

Log4CLを使ってみる

( Common Lisp Libraries Advent Calendar の10日目の記事です )

最近やっと実感を得ましたが、(デバッグ)ログは大事ですね。 遠隔地にいるライブラリ製作者とやりとりする回数を減らせます。

ということで、Common Lispでログ出力するライブラリ、Log4CL を眺めてみます。

Log4CL は、CLikiの Current recommended libraries で紹介されています。
JavaのロギングライブラリであるLog4jを参考にして作られているようなので、Java使いの人にとっては使いやすいのかもしれません。

インストールは Quicklisp で行えます。

> (ql:quickload :log4cl)

正確な情報はgithubにあるREADME.mdおよびソースコードを読んで調べて頂くとして、 ざっと概要を説明してみます。

Log4CL は次の3つの構成要素から成ります。

  • ロガー : ログの出力先 (論理的)
  • アペンダ : ログの出力先(出力方法) (コンソール出力・ファイル出力など)
  • レイアウト : ログ出力の整形方法

論理的な出力先であるロガーに、実際の出力方法を定義したアペンダを対応づけ、 レイアウトで指定されたフォーマットでログを出力します。

ログレベルは、 fatal,error,warn,info,debug, user1 ~ user4, trace, user5 ~ user9 の15段階が指定できます。 ログレベルの名前に対応する出力関数(マクロ)が定義されています。

CL-USER> (log:fatal "fatal error")
[22:57:21] [fatal] <cl-user> - fatal error

CL-USER> (log:info "hello")
[22:57:51] [info] <cl-user> - hello

1文字の省略形も定義されています。

CL-USER> (log:i "helo")
[22:59:26] [info] <cl-user> - helo

CL-USER> (log:w "w")
[23:00:10] [warn] <cl-user> - w

「<cl-user>」 は現在のパッケージ名です。 ロガーを指定しない場合、デフォルトでカレントパッケージの名前がロガーとして利用されます。

ロガーの作成はmake-logger関数、あるいはmake関数で行えます。
ロガーは親子関係を持ちますが、キーワードシンボルを指定すると 親=カレントパッケージ名, 子=シンボル名 という 構造が作成されるようです。

ログ出力関数でロガーを指定する場合は、名前(シンボル)を直接指定するほうが短くて良さそうです。

CL-USER> (log:w (log:make-logger :child) "w")
[23:02:41] [warn] <cl-user:child> - w

CL-USER> (log:f (log:make-logger '(parent child)) "f")
[23:03:13] [fatal] <parent:child> - f

CL-USER> (log:e :child "e")
[23:04:17] [error] <cl-user:child> - e

ロガーが出力するログレベルを設定するには、 config 関数を使います。省略形は c です。
なお、親ロガーの設定を引き継ぐよう設定するには、:unsetを指定すれば良いようです。 デフォルトの設定は :unset です。

CL-USER> (log:debug "hoge")
; No value
CL-USER> (log:c :d)  ;; (log:config :debug) と同等
CL-USER> (log:debug "hoge")
[23:13:02] [debug] <cl-user> - hoge

ログレベルに対応する出力処理以外に、評価前の式と評価後の値を出力する expr 関数があります。 処理系が SBCL の場合は、どの関数の中でログが出力されたかまでロガー名に含めてくれるようです。

(defun foo (a)
           (flet ((bar (b)
                    (log:expr a b (+ a b))))
             (bar 10)))
(log:config :debug)

;; CCL
CL-USER> (foo 2)
[23:17:57] [debug] <cl-user> - A=2 B=10 (+ A B)=12 

;; SBCL
CL-USER> (foo 2)
[23:17:06] [debug] <cl-user:foo:bar> - A=2 B=10 (+ A B)=12 

その他、アペンダやレイアウトの対応づけや設定ファイルの読み込み等が可能ですが、 README.md 以上に有益なことを書ける気がしないので、ドキュメントにかかれていない アペンダの定義を行い、syslogにログを出力してみます。

(ql:quickload :cl-syslog)

;; アペンダを定義
(defclass syslog-appender (log4cl-impl:appender)
  ())

;; アペンダの出力方法を定義
(defmethod log4cl-impl:appender-do-append ((appender syslog-appender) logger level log-func)
  ;; 出力はcl-syslogに丸投げ。
  (cl-syslog:log "log4cl" :local7 :info
    (with-output-to-string (s)
      (log4cl-impl:layout-to-stream
       (slot-value appender 'log4cl-impl:layout) s logger level log-func))))

;; ロガーを作成
(defvar syslog-logger (log:make '(syslog)))

;; ロガーにアペンダを対応付ける
(log:add-appender syslog-logger (make-instance 'syslog-appender))

;; ログ出力
(log:i syslog-logger "from cl")
kurohuku@mypc:/var/log$ tail -1 syslog
Dec 10 23:49:36 mypc log4cl: INFO - from cl

Log4jと異なりデフォルトで定義されてるアペンダは少ないので、 コンソールやファイル以外に出力を行いたい場合は自分でアペンダを定義する必要がありますが、 expr関数(マクロ)などはデバッグに便利ですし、試しに使ってみると良いと思います。

なお、Quicklispで (ql:system-apropos "log") でライブラリを検索したところ、 他にもロギングライブラリっぽいものが見つかりました。

  • cl-log
  • hu.dwim.logger
  • log5
  • logv
  • (cl-syslog)
  • (irc-logger)

・・・見つかりましたが、数が多いので比較はしません。ごめんなさい。