Programmer's Note

コード読み書きの備忘録。

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))))

で特別扱いしている。 もっとうまいやり方ありそうだけど、まあとりあえず。