combine
make parser
combine
余り資料が見当らない。わざとテストがフェイルするように設定して例を実行。実行出来るまでに171箇もの木箱がコンパイルされた。効率悪いな。
[sakae@fb /tmp/combine]$ cargo test --example date : ---- test stdout ---- thread 'main' panicked at 'assertion failed: `(left == right)` left: `Ok((DateTime { date: Date { year: 2015, month: 9, day: 2 }, time: Time { hour: 18, minute: 54, second: 42, time_zone: 120 } }, ""))`, right: `Ok((DateTime { date: Date { year: 2015, month: 8, day: 2 }, time: Time { hour: 18, minute: 54, second: 42, time_zone: 120 } }, ""))`', examples/date.rs:181:5
nomなんかでも、そうなのだけど、exampleを実行すると、余計な木箱がわんさかコンパイルされる。クィックにやるなら、前回やったように一つだけ取出してきて実行した方が良い。それだと改変も思う存分出来るしね。
[sakae@fb /tmp/combine]$ cargo run --example date : Running `target/debug/examples/date` 2022-03-14T23:66:77 // Press RETURN and C-d to eof Parse error at line: 1, column: 20 Unexpected ` ` Expected `Z`, `-` or `+`
2021-15-14T25:81:99Z0900 OK
こういうのがパース出来てしまうのは、例としていかがなものかと。冒頭に詳しい亊は、 ISO 8601を参照って記述があった。date-timeは、不合理の権家だからなあ。
いっそ、date-timeを、浮動少数点にしちゃったらどうよ。基準日時をEPOCに習って1970年にする。日は1をインクリメント、1日内(23:59:59まで)を、少数点で表す。楽でいいぞ。EXCELLはこの方針でやってるね。
/// Parses a date /// 2010-01-30 fn date<Input>() -> impl Parser<Input, Output = Date> where Input: Stream<Token = char>, Input::Error: ParseError<Input::Token, Input::Range, Input::Position>, { ( many::<String, _, _>(digit()), char('-'), two_digits(), char('-'), two_digits(), )
任意の桁数の表現が、プチ気になるぞ。こういう記述が求められるのか。すっきりしないな。
ci.sh
combineのTOPに、下記のような総合テスト用意されてた。bashで駆動ってのが食わないけど、そんなのを無視して、OpenBSDで実行してみる。
#!/bin/bash -x set -ex if [[ "$TRAVIS_RUST_VERSION" == "1.40.0" ]]; then cargo "$@" check cargo "$@" check --no-default-features else cargo "$@" build cargo "$@" test --all-features cargo "$@" test --all-features --examples :
ob$ cargo "$@" test --all-features Compiling futures-sink v0.3.17 Compiling tokio v1.12.0 Compiling futures-channel v0.3.17 Compiling futures-util v0.3.17 Compiling tokio-util v0.6.8 Compiling futures-executor v0.3.17 Compiling futures v0.3.17 Compiling combine v4.6.3 (/tmp/combine) Building [======================> ] 174/182: combine, parser(test), as... /tmp: write failed, file system is full error: failed to write `/tmp/combine/target/debug/.fingerprint/combine-3a304743a712a81c/test-integration-test-buffered_stream`
オイラーが用意したRAMDISK(1G)を食い潰してしまった。これもそれ、実使用を想定してasyncな環境を作るため、わんさかと木箱を用意してるから。tokioだとかhttpね。
しょうがないのでdebianで実行
+ cargo test --all-features running 47 tests test error::tests_std::parse_clone_but_not_copy ... ok test parser::byte::num::tests::no_rangestream ... ok test parser::byte::tests::bytes_read_stream ... ok : Running tests/async.rs (target/debug/deps/async-1948641015d3d81d) running 23 tests test any_send_partial_state_do_not_forget_state ... ok test choice_test ... ok test decode_async_std ... ok test decode_loop ... ok : Doc-tests combine running 156 tests test src/error.rs - error::Commit<T>::combine (line 291) ... ok : test src/parser/char.rs - parser::char::alpha_num (line 181) ... ok test src/parser/char.rs - parser::char::char (line 16) ... ok test src/parser/char.rs - parser::char::crlf (line 110) ... ok test src/parser/char.rs - parser::char::digit (line 32) ... ok test src/parser/char.rs - parser::char::hex_digit (line 233) ... ok : test src/parser/mod.rs - parser::EasyParser::easy_parse (line 974) ... ok test src/parser/mod.rs - parser::Parser::and (line 414) ... ok test src/parser/mod.rs - parser::Parser::and_then (line 740) ... ok test src/parser/mod.rs - parser::Parser::boxed (line 836) ... ok
しっかりテストで安心出来ます。ちなみに、全テストを実施した時の残骸は1.5Gになってましたよ。
使いかたは、WEBからどうぞって亊だな。ci.shの最後にdocを作っているよ。
sakae@deb:/tmp/combine$ cargo doc --open Finished dev [unoptimized + debuginfo] target(s) in 0.12s Opening /tmp/combine/target/doc/combine/index.html
csv
昔結構技術書を買ってた。自分への投資。 Real World Haskee なんてのもそうだ。通読して終りにしちゃったな。今が再読する機会です。確かParsecも扱っていたな。
in haskell
ソースをお取り寄せ。 book-real-world-haskell (ch16)
一番簡単なCSVのパーサー。
import Text.ParserCombinators.Parsec csvFile = endBy line eol line = sepBy cell (char ',') cell = many (noneOf ",\n") eol = char '\n' parseCSV :: String -> Either ParseError [[String]] parseCSV input = parse csvFile "(unknown)" input
なんと、これだけで素朴なCSVファイルをパース出来るとな。声に出して読んでみると、ファイルは、ラインが寄り集まって、最後はEOLで終わるやつ。EOLってのは、\\n ね。世の中には、他のEOLもあるけど、取り敢えず無視。
ラインの構成は、セル(EXCELLで考えるんだ)をカンマでくわけしたもの。で、肝心のセルってのは、カンマとEOLじゃ無い文字の羅列。たったこれだけ。定義を書いただけって趣。
in rust
これをcombineの木箱を使って賭。
use combine::{ many1, parser::char::{char, digit}, sep_by, Parser, }; fn type_of<T>(_: T) -> &'static str { std::any::type_name::<T>() } fn main() { let mut cell = many1(digit()); println!("{}", type_of(&cell)); let mut csv = sep_by(cell, char(',')).map(|words: Vec<String>| words); let result = csv.parse("221,130,64,59\n123,65,67,55\n"); println!("{:?}", result.unwrap()); }
まだ、中途半端なやつだ。
&combine::parser::repeat::Many1<alloc::string::String, combine::parser::char::Digit<&str>> (["221", "130", "64", "59"], "\n123,65,67,55\n")
地獄
haskellのお手本に習って、追加すればいいんだな。
fn main() { let cell = many1(digit()); let mut line = sep_by(cell, char(',')); let mut csv = sep_by(line, char('\n')).map(|words: Vec<String>| words); let result = csv.parse("221,130,64,59\n123,65,67,55\n"); println!("{:?}", result.unwrap()); }
突然、地獄へ引っ張りこまれました。エラーのオンパレード。
error[E0283]: type annotations needed for `Many1<F, Digit<&str>>` --> src/main.rs:12:16 | 12 | let cell = many1(digit()); | ---- ^^^^^ cannot infer type for type parameter `F` declared on the function `many1` | | | consider giving `cell` the explicit type `Many1<F, _>`, where the type parameter `F` is specified | = note: cannot satisfy `_: Extend<char>`
推測できひんか。
note: required by a bound in `many1` --> /home/sakae/.cargo/registry/src/github.com-1285ae84e5963aae/combine-4.6.3 /src/parser/repeat.rs:532:8 | 532 | F: Extend<P::Output> + Default, | ^^^^^^^^^^^^^^^^^ required by this bound in `many1` help: consider specifying the type arguments in the function call | 12 | let cell = many1::<F, Input, P>(digit()); | +++++++++++++++
もう、お手上げです。
見様見まね
自分で考えていても解らん。benches/ なんて所にjson.rsがいた。ベンチマーク用? 藁にもすがる気持で、コードを眺めましたよ。
で、分った亊は、自分で関数を定義する必要があるって亊。その関数ってのは、新しい型を返す関数。
下記の例だと、cellは1つ以上の数値の集まり。それが見付かったら、整数に変換して出力するよ。そういうParserだからね。って言う関数だ。
use combine::{ many1, parser::char::{char, digit}, sep_by, ParseError, Parser, Stream, }; fn cell<Input>() -> impl Parser<Input, Output = i32> where Input: Stream<Token = char>, Input::Error: ParseError<Input::Token, Input::Range, Input::Position>, { many1(digit()) .map(|s: String| { let mut n = 0; for c in s.chars() { n = n * 10 + (c as i32 - '0' as i32); } n }) .expected("integer") } fn main() { let line = sep_by(cell(), char(',')).map(|words: Vec<i32>| words); let mut csv = sep_by(line, char('\n')).map(|words: Vec<Vec<i32>>| words); let result = csv.parse("221,130,64,59\n305,135,67,55\n"); println!("{:?}", result.unwrap()); }
して、結果は
([[221, 130, 64, 59], [305, 135, 67, 55], []], "")
何とか動き出したな。でも不満がある。
不満解消
不満解消に取り掛かる。以下は変更部分。
fn main() { let mut csv = sep_by( (cell(), char(','), cell(), char(','), cell(), char(','), cell()) .map(|(ymdh, _, hi, _, lo, _, _)| [ymdh, hi, lo]), char('\n') ); let input = "22031005,135,72,56 22031021,116,60,50 22031105,129,71,61 22031121,122,61,57"; let result: Result<(Vec<[i32; 3]>, &str), easy::ParseError<&str>> = csv.easy_parse(input); println!("{:?}", result.unwrap().0); }
csvのパーサーに、データに即した構造を持たせた。データは毎度お馴染の血圧データである。
今回はそのデータから脈拍を除くものを抽出する。cellはカンマで区切られたデータが4つ並んでいるよ。4つのデータの区切はリターン記号だよって定義。
cellの並びは4つを組にして(タプルで表現)、得られた結果をmapを使って配列に組み立てる。 だからmapの引数もタプルで表現。脈拍データはタプルの最終項に現れるけど、それは無視。勿論、',' もデータとして得られるけど、アンダーバーにして、無視してる。 ymdh,hi,loだけで、配列を組み立てている。
csvをパースする。その時に却ってくる構造をResult型で注釈してる。その型は、データ部とエラーの組。更にデータ部は、評価が終了したVec<[i32: 3]>と、未評価なインプットデータの組って表現だ。
最終のデータ表示は、resultをunwrap()して、データ部のタプルを取出し、更にその.0で、タプルの最左部分を使ってる。
[[22031005, 135, 72], [22031021, 116, 60], [22031105, 129, 71], [22031121, 122, 61]]
どうしろと?
上記では、データが決めうち。これじゃ実用にならん。ファイルから読み込んだデータを使いたい。
let input = std::fs::read_to_string("current.csv").unwrap();
ファイルから全部読みしたのを使いたい。が、エラーだ。
error[E0597]: `input` does not live long enough --> src/main.rs:30:26 | 30 | = csv.easy_parse(&input); | ^^^^^^ borrowed value does not live long enough 31 | println!("{:?}", result.unwrap().0); 32 | } | - | | | `input` dropped here while still borrowed
これに対する説明として、rustc –explain E0597 こんな事例で解説されてた。
struct Foo<'a> { x: Option<&'a u32>, } let mut x = Foo { x: None }; { let y = 0; x.x = Some(&y); // error: `y` does not live long enough } println!("{:?}", x.x);
下のやつが改訂版。
struct Foo<'a> { x: Option<&'a u32>, } let mut x = Foo { x: None }; let y = 0; x.x = Some(&y); println!("{:?}", x.x);
目をこらして違いを見付ないといかん。悪いやつは、ブロックの中でyを宣言して、それを使って代入してる。ブロックの外で使おうとすると、rustの規則によりyは藻屑となってるからエラー。
だから、ブロックを削除して、同一階層で使いましょうとな。こんなに分かりやすい例ならいいんだけど。。
何かcombineの深い所で、これに反する亊が起きているんだろうね。深入りは止めておこう。
どうしろと(2) ?
今迄cellって関数を使ってたけど、docs.rsに手軽な数値化を行うパーサーが出てた。すっきりっぽい。
let cell = spaces() .with(many1(digit()) .map(|string: String| string.parse::<i32>().unwrap()));
一般的な文字列から数値に変換する方法だ。これを使ってやれ。
error[E0382]: use of moved value: `cell` --> src/main.rs:12:27 | 8 | let cell = spaces() | ---- move occurs because `cell` has type `With<impl Parser<combine\ ::easy::Stream<&str>>, combine::parser::combinator::Map<Many1<String, Digit<com\ bine::easy::Stream<&str>>>, [closure@src/main.rs:10:20: 10:67]>>`, which does n\ ot implement the `Copy` trait ... 12 | (cell, char(','), cell, char(','), cell, char(','), cell) | ---- ^^^^ value used here after move | | | value moved here
至る所が地雷源である。オイラーに取っては、combineって、取扱注意木箱ですな。 nom = alt(combine) になるかな。