まじめに文章を書く練習をしてみよう。

人に自分の思ったことを伝えられない気がする。ヤバい。
なので、内容ともかく意味の通る文章を書く練習をする。
逆引きrubyの内容をscheme(gauche)で書いて、それを説明するようにしてみる。

間違ったところや気づかなかったところがあったりした場合には教えてください。

逆引きruby(文字列)をscheme(gauche)で

gaucheで文字列を使う場合、クラスを使用します。だからと言って、主に利用するのがクラスのメソッドというわけはありません。

文字列を結合する

文字列を更新する手続きは基本的には用意されていません。文字列を結合する場合には、結合した文字列を新たに作りだすことになります。渡された文字列を結合した新しい文字列を返す手続きとしてstring-appendが使えます。string-appendは任意個の文字列を引数として取ることができます。

(define s "hello")
(define s1 (string-append s " World")) ; => "hello World"
(string-append "a" "b" "c" "d" "e") ; => "abcde"

繰り返し文字列を生成する

文字列を複数回繰り替えした文字列を生成する手続きは、直接的には用意されていません。
繰り替えし文字列を生成するには、以下の2つの方法があげられます。

  • 出力先を文字列にして、ポートに文字列を出力する手続きを呼び出す
  • 文字列の繰り替えをリストで表現し、作成したリストを文字列として結合する
出力先を文字列にして、ポートに文字列を出力する手続きを呼び出す

with-output-to-stringを利用すると、引数として渡したthunk(無引数関数)の中では、一時的に、printのような出力関数の出力先が標準の出力先ではなく文字列ポートに変更されます。渡したthunkの中で、N回出力してあげれば、繰り返し文字列を作成することができます。

(with-output-to-string 
    (lambda () (dotimes (i 3) (display "Hey ")))) ; => "Hey Hey Hey "
文字列の繰り替えをリストで表現し、作成したリストを文字列として結合する

string-appendを複数回繰り替えして繰り返し文字列を作った場合、string-appendを呼ぶ度に新しい文字列が生成されてしまいます。(そのため、結果の文字列を生成する過程で多くの無駄な文字列が生成されてしまいます。)
listやconsは要素として同じ参照先を持てるため、思いつく表現の完成形が見えない場合には、結合した文字列ではなくlistとして持った方が無駄がありません。文字列が必要になった際には、text.treeライブラリにあるtree->stringなどを使って、リストを文字列にすれば良いでしょう。(現実的には、上の文字列ポートに出力することと同じことになります。)

(use text.tree)
(tree->string (make-list 3 "Hey ")) ; => "Hey Hey Hey "

大文字小文字に揃える

srfi-13にあるstring-upcaseとstring-downcaseを利用すれば、文字を大文字に変換したり小文字に変換したりできます。

(use srfi-13)
(string-upcase "I love Scheme") ; => "I LOVE SCHEME"
(string-downcase "I love Scheme") ; => "i love scheme"

大文字小文字の入れ替え

大文字と小文字を入れ替える手続きは用意されていません。必要なら自分で定義して使うことになります。srfi-13のchar-upper-case?などを利用します。schemeの文字列は文字のリストではないので、一度文字列を文字のリストに変換して使うことにします。

(define (swap-case str)
  (apply string
	 (map (lambda (c) 
		(cond ((char-upper-case? c) (char-downcase c))
		      ((char-lower-case? c) (char-upcase c))
		      (else c)))
	      (string->list str))))
(swap-case "I love Scheme") ; => "i LOVE sCHEME"

コマンドの実行結果を文字列に設定する

コマンドの実行結果を文字列として返す手続きとしてprocess-output->stringが用意されています。また、call-with-input-process
を利用すると実行結果を入力ポートとして、process-output->string-listを利用すると実行結果を行ごとの文字列のリストとして手に入れることができます。一般的には、後者の手続きを利用することをおすすめします。

(use gauche.process)
(process-output->string "LANG=c date") ; => "Wed Feb 3 17:34:45 JST 2010"

(call-with-input-process '(echo "foo\nbar\nbaz")
  (lambda (in) 
    (port-map string-upcase 
	      (lambda () (read-line in))))) ; => ("FOO" "BAR" "BAZ")

(process-output->string-list '(echo "ba\nba\nba")) ; => ("ba" "ba" "ba")

複数行の文字列を作成する

ヒアドキュメントは存在しません。なので何を書けば良いのか分かりません。
特別なことを挙げるとすれば、改行を含みたくない行には行末に"\ "をつけられることぐらいでしょうか?

(define s 
  "this is test.\

scheme, the multi paradigm language
")

複数行のコマンドの実行結果を文字列に設定する

省略

部分文字列を取り出す

部分文字列を取り出すにはsubstringを利用します。取り出す文字列の終端位置を示す第3引数は省略できません。

(define s "Apple Banana Orange")
(substring s 0 5) ; => "Apple"
(substring s 6 12) ; => "Banana"
(substring s 13 -1) ; => "Orange"

部分文字列を置き換える

通常、部分文字列を置き換えるといった操作は行いません。新たな文字列を作るか、リストとして持って要素を置き換えることが多いです。string-joinは文字列のリストの要素間を2つめの引数でつなげた文字列を返します。

(define sl '("Apple" "Banana" "Orange"))
(string-join sl " ") ; => "Apple Banana Orange"
(define sl (cons "Vine" (cdr sl)))
(string-join sl " ") ; => "Vine Banana Orange"
(define sl (match-let1 (x _ y) sl
	    (list x "Lemmon" y)))
(string-join sl " ") ; => "Vine Lemmon Orange"

文字列中の式を評価し値を展開する

#`""という形式の不完全な文字列を利用します。内部的には、string-appendとx->stringを利用したS式に置き換えられます。
変数だけでなく、手続きやメソッドの適用結果も渡せます。

(let ((v 123))
  (print #`"value is ,v")) ; => value is 123

(define (sub1 str) #`"Hello, ,str")
(print #`"Say ,(sub1 \"Tomoya\")") ; => Say Hello, Tomoya

文字列を1文字ずつ処理する

string->listで文字に分解した後、for-each、foldなどのリストを走査する手続きを利用します。

(fold (lambda (c n) (+ (char->integer c) n)) 0 (string->list "Scheme")) ; => 597

上記の例は、文字列の各文字の文字コードを合計を計算しています

文字列を1行ずつ処理する

文字列のリストを利用すること以外は、1文字ずつ処理することと同様です。

(define s 
  "this is test.

scheme, the multi paradigm language")

(use srfi-1)
(let1 seq (string-split s "\n")
  (for-each (lambda (no line) (print no ": " line))
	    (iota (length seq) 1) seq))
;;>> 1: this is test.
;;>> 2: 
;;>> 3: scheme, the multi paradigm language

上の例は、要素に要素番号を付加して出力しています。

文字列を整数に変換する (integer)

string->numberで文字列を整数に変換できます。2つ目の引数を与えることで基数に基づく変換も行うことができます。(ただし、読み込める16進数の文字列は"0xff"というような表記ではなく"ff"となります)整数として読み込めなかった場合には#fを返します。

(let ((i 1) (s "999"))
  (+ i (string->number s))) ; => 1000

(string->number "10" 8) ; => 8 ;;oct
(string->number "ff" 16) ; => 255 ;;hex
;;0xなどのprefixがついた文字列には対応してません。
(string->number "0xff" 16) ; => #f

文字列を整数以外の数に変換する(exact,inexact)

正確数(exact number)と非正確数(inexact number)があります。非正確数はいわゆる浮動小数点数です。

(exact->inexact 10) ; => 10.0
(inexact->exact 0.5) ; => 1/2
(inexact->exact 10) ; => 10

ASCII文字をコード値に(コード値をASCII文字に)変換する

それぞれの変換手続きが用意されています。

(char->integer #\R) ; => 82
(integer->char 82) ; => #\R

文字列を中央寄せ・左詰・右詰する

たぶん、そのような手続きはないです。自分で定義することになります。
(formatを使えばもしかしたらできるのかもしれませんが、formatの書式が複雑でまだ完璧に理解していません。)

(define (center s len)
  (let1 pad-size (- len (string-length s))
    (receive (q r) (quotient&remainder pad-size 2)
      (string-append (make-string (+ q r) #\space)
		     s
		     (make-string q  #\space)))))

(define (ljust s len)
  (let1 pad-size (- len (string-length s))
    (string-append s (make-string pad-size #\space))))

(define (rjust s len)
  (let1 pad-size (- len (string-length s))
    (string-append (make-string pad-size #\space) s)))

(center "Ruby" 10) ; => "   Ruby   "
(ljust "Ruby" 10) ; => "Ruby      "
(rjust "Ruby" 10) ; => "      Ruby"


srfi-13のstring-pad,string-pad-rightを使えば文字を寄せることができます。中央に寄せる手続きはなさそうです。

(use srfi-13)
(string-pad-right "Ruby" 10) ; => "Ruby      "
(string-pad "Ruby" 10) ; => "      Ruby"

"次"の文字列を取得する

自分で定義するしかないです。結構たいへんでした。

(define (string-succ s)
  (rxmatch-cond 
    [(#/^\d+$/ s) (n) (number->string (+ 1 (string->number n)))]
    [(#/^(\D*)(\D)$/ s) (#f ps last)
     (let1 chint (char->integer (string-ref last 0))
       (string-append ps (string (integer->char (+ 1 chint)))))]
    [(#/^(\D+)(\d+)$/ s) (#f ps n)
     (let* ((area-size-chars (string->list (number->string (string-length n))))
	    (cmd (apply string `(#\~ ,@area-size-chars #\, #\, #\, #\' #\0 #\@ #\a)))
	    (n+ (format #f cmd (+ 1 (string->number n)))))
       (if (= (string-length n+) (string-length n))
	   (string-append ps n+)
	   (string-append (string-succ ps) (format #f cmd 0))))]))

(string-succ "9") ; => "10"
(string-succ "a") ; => "b"
(string-succ "AAA") ; => "AAB"
(string-succ "A99") ; => "B00"
(string-succ "A099") ; => "A100"

文字列を暗号化する

sys-cryptというcrypt(3)のラッパーが用意されています。もちろんcrypt(3)のない環境では動きません。

(define secretword  "hogehoge")
(define crypt (sys-crypt "key" secretword))

(when (string=? (sys-crypt "key" secretword) crypt)
  (print "ok")) ; => ok
(unless (string=? (sys-crypt "ey" secretword) crypt)
  (print "ok")) ; => ok
(unless (string=? (sys-crypt "key" "hagehoge") crypt)
  (print "ok")) ; => ok

文字列中で指定したパターンにマッチする部分を置換する

文字列中の指定したパターンにマッチする箇所を置き換えるには、regexp-replaceかregexp-replace-allが使えます。regexp-replaceは左から一度切りの置換を、regexp-replace-allはマッチしたすべての箇所の置換を行います。

(define s "Apple Banana Apple Orange")
(regexp-replace "Apple" s "Pine") ; => "Pine Banana Apple Orange"
(regexp-replace-all "Apple" s "Pine") ; => "Pine Banana Pine Orange"

文字列中に含まれている任意文字列の位置を求める

部分文字列の位置を知るには、gauche特有の手続きですがstring-scanが使えます。string-scanは複数のオプションを取ることができます。それによって返す値が変化します。(defaultで'index)string-scanは探索の開始位置を指定することができません。また右から探索を行う手続きも用意されていません。
(実際のところ、srfi-13内の手続きを組み合わせれば思い描いた手続きを作ることは可能です。たぶん。)

(define s "Apple Banana Apple Orange")
(string-scan s "Apple") ; => 0
(string-scan s "Banana" 'index) ; => 6
(define (string-scan* s item beg option)
  (+ beg (string-scan (substring s beg -1) item option)))
(string-scan* s "Apple" 6 'index) ; => 13

文字列の末端の改行を削除する

末端の改行を安全に削除したいならstring-trim-rightを、任意の末尾文字を削除したいならstring-drop-rightを利用します。(例によって、文字列を破壊的に更新する手続きは用意されていません)

(use srfi-13)
(define s "Hello Ruby!\n")
(string-trim-right s #[\n]) ; => "Hello Ruby!"
(string-drop-right s 1) ; => "Hello Ruby!"

カンマ区切りの文字列を扱う

string-splitで切り出せば良いんじゃないでしょうか。

(string-split "001,TAKEUCHI Hitoshi,Yokohama" #\,)
  ; => ("001" "TAKEUCHI Hitoshi" "Yokohama")

真面目にcsvファイルを読み込む際にはtext.csvモジュールの利用を検討しても良いかもしれません。
make-csv-readerは入力ポートを引数として取る手続きを返します。

(use text.csv)

(define reader (make-csv-reader #\,))
(call-with-input-string "001,TAKEUCHI Hitoshi,Yokohama"
  reader) ; => ("001" "TAKEUCHI Hitoshi" "Yokohama")

任意のパターンにマッチするものを全て抜き出す

srfi-1のfilter-mapとand-let*を組み合わせると便利かもしれません。
srfi-1のfilterとfilter-mapはどちらもリストを渡された比較手続きによりフィルタリングするものです。
filterとfilter-mapの違いは、比較に使った手続きの結果を利用するかどうかです。

(filter odd? '(1 2 3 4)) ; => (1 3)
(filter-map (lambda (x) (and (odd? x) (* x x)))
	    '(1 2 3 4)) ; => (1 9)

and-let*は束縛するはずだった値が#fになった瞬間に、以降の実行を取りやめ#fを返すlet*です。

(let* ((x #f))
  (+ x 10)) ; => *** ERROR: operation + is not defined between 10 and #f
(and-let* ((x (odd? 10)))
  (+ x 10)) ; => #f

"#/.*/"などの正規表現は適用可能オブジェクトを返します。これを仮にmとすると"(m 0)"でマッチした部分、"(m 1)"で初めのグループでマッチした部分など、rubyなどの$0,$1,$2..の感覚で利用できます。($`,$'に対応したrxmatch-after,rxmatch-beforeがあります)

(use srfi-1)
(filter-map (lambda (str)
	      (and-let* ((m (#/(\S+):([\d-]+)/ str)))
		(list (m 1) (m 2))))
	    '("hoge:045-111-2222" "boke:045-222-2222"))