Clojureの標準入力とファイル読み込み(文字列カウント)
ファイルのリードと標準入力の扱いのメモ。 「プログラミング言語C」の例題にもあるword count的なものを作ってみる。
ソース
leiningen環境にて
$ lein new app t_stdin
とやったあとにsrc/t_stdin/core.clj
を編集。
(ns t-stdin.core (:require [clojure.java.io :as io] [clojure.string :as str]) (:gen-class)) (defn count-words [line] (let [w (-> line (str/replace #"^\s+" "") (str/split #"[\s\t]+"))] (if (or (= line "") (= w [""])) 0 (count w)))) (defn -main [& args] (let [f (if-not (empty? args) (io/file (first args)) *in*) dat (line-seq (io/reader f))] (println (str "lines: " (count dat))) (println (str "words: " (reduce + (map count-words dat))))))
実行結果
このソースコード自体を入力に与えてみる。
t_stdin $lein run < src/t_stdin/core.clj lines: 32 words: 85
上記はプログラムに引数を与える形で、lein run src/t_stdin/core.clj
と打っても同じ。
wc
コマンドの結果で答え合わせ。
t_stdin $wc src/t_stdin/core.clj 32 85 713 src/t_stdin/core.clj
メモ
Clojureのファイルのリードは、下記のようにすれば
(line-seq (clojure.java.io/reader (clojure.java.io/file "file-name")))
行に分割したシーケンスとして返してくれる。
"file-name"
はファイル名の文字列。
(ファイルが見つからないときのエラー処理はまったく無視してるが・・・)
標準入力の場合は、
(line-seq (clojure.java.io/reader (clojure.java.io/file *in*)))
とするだけ。
とりあえず、今回は
(let [f (if-not (empty? args) (io/file (first args)) *in*) dat (line-seq (io/reader f))]
引数が与えられいればそれをファイル名として使う。
そうでない場合*in*
を使う。
一行の中の文字数のカウントは関数count-words
で行っている。
文字列のカウントは結局、空白で文字列を分割して得たシーケンスの要素の数を数える。
(count (str/split line #"[\s\t]+"))
的な感じでやるのだが、split関数は行の先頭に空白があった場合に、""
が要素に追加されてしまう。
t-stdin.core=> (str/split " aa bb" #"[\s\t]+") ["" "aa" "bb"]
ので、splitに与える前にreplaceで先頭の空白を除いている。
(-> line (str/replace #"^\s+" "") (str/split #"[\s\t]+"))
一方で、空行の場合文字列が""で、上記の正規表現のフィルターにひっかからない。
また、先頭が空白だけの行は結果が[""]
になるので、
(if (or (= line "") (= w [""])) 0 (count w))))
で特別扱いしている。 もっとうまいやり方ありそうだけど、まあとりあえず。