Ingredient parsing for the happy path works

This commit is contained in:
Jeremy Wall 2021-11-01 20:49:48 -04:00
parent a96f175e9b
commit 821cc098fb
7 changed files with 470 additions and 116 deletions

148
Cargo.lock generated
View File

@ -2,9 +2,9 @@
# It is not intended for manual editing. # It is not intended for manual editing.
[[package]] [[package]]
name = "abortable_parser" name = "abortable_parser"
version = "0.2.4" version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2febc32aeed847847255580d5938d62c8e3f6cb0a019cc0d36dc1405827d4ec" checksum = "8f520551d986b2be672fac2b1d749874f914ecc03fb347c7b0b5a7e541ffe435"
[[package]] [[package]]
name = "ahash" name = "ahash"
@ -88,6 +88,33 @@ dependencies = [
"libloading", "libloading",
] ]
[[package]]
name = "dirs-next"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
dependencies = [
"cfg-if",
"dirs-sys-next",
]
[[package]]
name = "dirs-sys-next"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]]
name = "endian-type"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d"
[[package]] [[package]]
name = "fallible-iterator" name = "fallible-iterator"
version = "0.2.0" version = "0.2.0"
@ -100,6 +127,16 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fs2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
dependencies = [
"libc",
"winapi",
]
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.2" version = "0.2.2"
@ -141,6 +178,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"recipe-store", "recipe-store",
"recipes", "recipes",
"rustyline",
] ]
[[package]] [[package]]
@ -183,12 +221,42 @@ dependencies = [
"vcpkg", "vcpkg",
] ]
[[package]]
name = "log"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.3.4" version = "2.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
[[package]]
name = "nibble_vec"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43"
dependencies = [
"smallvec",
]
[[package]]
name = "nix"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a"
dependencies = [
"bitflags",
"cc",
"cfg-if",
"libc",
]
[[package]] [[package]]
name = "nom" name = "nom"
version = "5.1.2" version = "5.1.2"
@ -271,6 +339,16 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "radix_trie"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd"
dependencies = [
"endian-type",
"nibble_vec",
]
[[package]] [[package]]
name = "recipe-store" name = "recipe-store"
version = "0.1.0" version = "0.1.0"
@ -291,6 +369,25 @@ dependencies = [
"uuid", "uuid",
] ]
[[package]]
name = "redox_syscall"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc"
dependencies = [
"bitflags",
]
[[package]]
name = "redox_users"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
dependencies = [
"getrandom",
"redox_syscall",
]
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.4.5" version = "1.4.5"
@ -329,6 +426,35 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustyline"
version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e1b597fcd1eeb1d6b25b493538e5aa19629eb08932184b85fef931ba87e893"
dependencies = [
"bitflags",
"cfg-if",
"dirs-next",
"fs2",
"libc",
"log",
"memchr",
"nix",
"radix_trie",
"scopeguard",
"smallvec",
"unicode-segmentation",
"unicode-width",
"utf8parse",
"winapi",
]
[[package]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]] [[package]]
name = "shlex" name = "shlex"
version = "1.0.0" version = "1.0.0"
@ -352,12 +478,30 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "unicode-segmentation"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796"
[[package]]
name = "unicode-width"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"
version = "0.2.1" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
[[package]]
name = "utf8parse"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "0.8.2" version = "0.8.2"

View File

@ -9,3 +9,4 @@ edition = "2018"
[dependencies] [dependencies]
recipes = {path = "../recipes" } recipes = {path = "../recipes" }
recipe-store = {path = "../recipe-store" } recipe-store = {path = "../recipe-store" }
rustyline = "~8.0.0"

View File

@ -7,7 +7,7 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
abortable_parser = "~0.2.4" abortable_parser = "~0.2.5"
chrono = "~0.4" chrono = "~0.4"
[dependencies.uuid] [dependencies.uuid]

View File

@ -11,13 +11,16 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
mod parse;
pub mod unit; pub mod unit;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::str::FromStr;
use chrono::NaiveDate; use chrono::NaiveDate;
use uuid::{self, Uuid}; use uuid::{self, Uuid};
use parse::{ingredient, measure};
use unit::*; use unit::*;
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
@ -201,6 +204,26 @@ impl Ingredient {
self.amt.measure_type(), self.amt.measure_type(),
); );
} }
pub fn parse(s: &str) -> Result<Ingredient, String> {
Ok(match ingredient(abortable_parser::StrIter::new(s)) {
abortable_parser::Result::Complete(_, ing) => ing,
abortable_parser::Result::Abort(e) | abortable_parser::Result::Fail(e) => {
return Err(format!("Failed to parse as Ingredient {:?}", e))
}
abortable_parser::Result::Incomplete(_) => {
return Err(format!("Incomplete input: {}", s))
}
})
}
}
impl FromStr for Ingredient {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ingredient::parse(s)
}
} }
impl std::fmt::Display for Ingredient { impl std::fmt::Display for Ingredient {

190
recipes/src/parse.rs Normal file
View File

@ -0,0 +1,190 @@
// Copyright 2021 Jeremy Wall
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::str::FromStr;
use abortable_parser::{
ascii_digit, ascii_ws, consume_all, discard, do_each, either, eoi, make_fn, not, optional,
peek, repeat, separated, text_token, trap, until, Result, StrIter,
};
use num_rational::Ratio;
use crate::{
unit::{Measure, Measure::*, Quantity, VolumeMeasure::*},
Ingredient,
};
make_fn!(ws<StrIter, &str>,
consume_all!(either!(
text_token!(" "),
text_token!("\t"),
text_token!("\r")
))
);
make_fn!(nonzero<StrIter, ()>,
peek!(not!(do_each!(
n => consume_all!(text_token!("0")),
_ => ws,
(n)
)))
);
make_fn!(num<StrIter, u32>,
do_each!(
_ => peek!(ascii_digit),
n => consume_all!(ascii_digit),
(u32::from_str(n).unwrap())
)
);
make_fn!(
pub ratio<StrIter, Ratio<u32>>,
do_each!(
// First we assert non-zero numerator
//_ => nonzero,
numer => num,
_ => text_token!("/"),
denom => num,
(Ratio::new(numer, denom))
)
);
make_fn!(unit<StrIter, &str>,
do_each!(
u => either!(
text_token!("tsp"),
text_token!("tbsp"),
text_token!("floz"),
text_token!("ml"),
text_token!("ltr"),
text_token!("cup"),
text_token!("qrt"),
text_token!("pint"),
text_token!("pnt"),
text_token!("gal"),
text_token!("gal"),
text_token!("cnt"),
text_token!("g"),
text_token!("gram")),
(u))
);
make_fn!(
pub quantity<StrIter, Quantity>,
either!(
do_each!(
whole => num,
_ => ws,
frac => ratio,
_ => ws,
(Quantity::Whole(whole) + Quantity::Frac(frac))
),
do_each!(
frac => ratio,
_ => ws,
(Quantity::Frac(frac))
),
do_each!(
whole => num,
_ => ws,
(Quantity::whole(whole))
)
)
);
make_fn!(
pub measure_parts<StrIter, (Quantity, Option<&str>)>,
do_each!(
qty => quantity,
unit => optional!(do_each!(
_ => ws,
unit => unit,
(unit)
)),
_ => ws,
((qty, unit))
)
);
pub fn measure(i: StrIter) -> abortable_parser::Result<StrIter, Measure> {
match measure_parts(i) {
Result::Complete(i, (qty, unit)) => {
return Result::Complete(
i.clone(),
match unit {
Some("tsp") => Volume(Tsp(qty)),
Some("tbsp") => Volume(Tbsp(qty)),
Some("floz") => Volume(Floz(qty)),
Some("ml") => Volume(ML(qty)),
Some("ltr") | Some("liter") => Volume(Ltr(qty)),
Some("cup") | Some("cp") => Volume(Cup(qty)),
Some("qrt") | Some("quart") => Volume(Qrt(qty)),
Some("pint") | Some("pnt") => Volume(Pint(qty)),
Some("cnt") | Some("count") => Count(qty),
Some("g") => Gram(qty),
Some("gram") => Gram(qty),
Some(u) => {
return Result::Abort(abortable_parser::Error::new(
format!("Invalid Unit {}", u),
Box::new(i),
))
}
None => Count(qty),
},
)
}
Result::Fail(e) => {
return Result::Fail(e);
}
Result::Abort(e) => {
return Result::Abort(e);
}
Result::Incomplete(i) => return Result::Incomplete(i),
}
}
make_fn!(
pub ingredient_name<StrIter, &str>,
do_each!(
name => until!(ascii_ws),
_ => ws,
(name)
)
);
make_fn!(
ingredient_modifier<StrIter, &str>,
do_each!(
_ => text_token!("("),
modifier => until!(text_token!(")")),
_ => text_token!(")"),
(modifier)
)
);
make_fn!(
pub ingredient<StrIter, Ingredient>,
do_each!(
_ => optional!(ws),
measure => measure,
name => ingredient_name,
modifier => optional!(ingredient_modifier),
(Ingredient::new(name, modifier.map(|s| s.to_owned()), measure, ""))
)
);
make_fn!(
pub ingredient_list<StrIter, Vec<Ingredient>>,
separated!(text_token!("\n"), ingredient)
);

View File

@ -16,6 +16,7 @@ use VolumeMeasure::*;
use std::convert::Into; use std::convert::Into;
use abortable_parser::{Result as ParseResult, StrIter};
use num_rational::Ratio; use num_rational::Ratio;
#[test] #[test]
@ -233,3 +234,107 @@ fn test_ingredient_display() {
assert_eq!(format!("{}", i), expected); assert_eq!(format!("{}", i), expected);
} }
} }
use Measure::*;
#[test]
fn test_ratio_parse() {
if let ParseResult::Complete(_, rat) = parse::ratio(StrIter::new("1/2")) {
assert_eq!(rat, Ratio::new(1, 2))
} else {
assert!(false)
}
}
#[test]
fn test_quantity_parse() {
for (i, expected) in vec![
("1 ", Quantity::Whole(1)),
("1/2 ", Quantity::Frac(Ratio::new(1, 2))),
("1 1/2 ", Quantity::Frac(Ratio::new(3, 2))),
] {
match parse::quantity(StrIter::new(i)) {
ParseResult::Complete(_, qty) => assert_eq!(qty, expected),
err => assert!(false, "{:?}", err),
}
}
}
#[test]
fn test_ingredient_name_parse() {
for (i, expected) in vec![("flour ", "flour"), ("flour (", "flour")] {
match parse::ingredient_name(StrIter::new(i)) {
ParseResult::Complete(_, n) => assert_eq!(n, expected),
err => assert!(false, "{:?}", err),
}
}
}
#[test]
fn test_ingredient_parse() {
for (i, expected) in vec![
(
"1 cup flour ",
Ingredient::new("flour", None, Volume(Cup(Quantity::Whole(1))), ""),
),
(
"\t1 cup flour ",
Ingredient::new("flour", None, Volume(Cup(Quantity::Whole(1))), ""),
),
(
"1 cup apple (chopped)",
Ingredient::new(
"apple",
Some("chopped".to_owned()),
Volume(Cup(Quantity::Whole(1))),
"",
),
),
(
"1 cup apple (chopped) ",
Ingredient::new(
"apple",
Some("chopped".to_owned()),
Volume(Cup(Quantity::Whole(1))),
"",
),
),
] {
match ingredient(StrIter::new(i)) {
ParseResult::Complete(_, ing) => assert_eq!(ing, expected),
err => assert!(false, "{:?}", err),
}
}
}
#[test]
fn test_ingredient_list_parse() {
for (i, expected) in vec![
(
"1 cup flour ",
vec![Ingredient::new(
"flour",
None,
Volume(Cup(Quantity::Whole(1))),
"",
)],
),
(
"1 cup flour \n1/2 tsp butter ",
vec![
Ingredient::new("flour", None, Volume(Cup(Quantity::Whole(1))), ""),
Ingredient::new(
"butter",
None,
Volume(Tsp(Quantity::Frac(Ratio::new(1, 2)))),
"",
),
],
),
] {
match ingredient_list(StrIter::new(i)) {
ParseResult::Complete(_, ing) => assert_eq!(ing, expected),
err => assert!(false, "{:?}", err),
}
}
}

View File

@ -22,15 +22,15 @@ use std::{
convert::TryFrom, convert::TryFrom,
fmt::Display, fmt::Display,
ops::{Add, Div, Mul, Sub}, ops::{Add, Div, Mul, Sub},
str::FromStr,
}; };
use abortable_parser::{ use abortable_parser::{
self, consume_all, do_each, either, make_fn, not, optional, peek, run, text_token, trap, consume_all, do_each, either, make_fn, not, optional, peek, text_token, trap, Result, StrIter,
Result, StrIter,
}; };
use num_rational::Ratio; use num_rational::Ratio;
use crate::parse::measure;
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
/// Volume Measurements for ingredients in a recipe. /// Volume Measurements for ingredients in a recipe.
pub enum VolumeMeasure { pub enum VolumeMeasure {
@ -285,21 +285,7 @@ impl Measure {
pub fn parse(input: &str) -> std::result::Result<Self, String> { pub fn parse(input: &str) -> std::result::Result<Self, String> {
Ok(match measure(StrIter::new(input)) { Ok(match measure(StrIter::new(input)) {
Result::Complete(_, (qty, Some(unit))) => match unit { Result::Complete(i, measure) => measure,
"tsp" => Volume(Tsp(qty)),
"tbsp" => Volume(Tbsp(qty)),
"floz" => Volume(Floz(qty)),
"ml" => Volume(ML(qty)),
"ltr" => Volume(Ltr(qty)),
"cup" => Volume(Cup(qty)),
"qrt" => Volume(Qrt(qty)),
"pint" | "pnt" => Volume(Pint(qty)),
"cnt" => Count(qty),
"g" => Gram(qty),
"gram" => Gram(qty),
u => return Err(format!("Invalid unit {}", u)),
},
Result::Complete(_, (qty, None)) => Count(qty),
Result::Abort(e) | Result::Fail(e) => { Result::Abort(e) | Result::Fail(e) => {
return Err(format!("Failed to parse as Measure {:?}", e)) return Err(format!("Failed to parse as Measure {:?}", e))
} }
@ -468,98 +454,3 @@ impl Display for Quantity {
} }
} }
} }
make_fn!(nonzero<StrIter, ()>,
peek!(not!(do_each!(
n => consume_all!(text_token!("0")),
_ => optional!(ws),
(n)
)))
);
make_fn!(num<StrIter, u32>,
do_each!(
n => consume_all!(either!(
text_token!("0"),
text_token!("1"),
text_token!("2"),
text_token!("3"),
text_token!("4"),
text_token!("5"),
text_token!("6"),
text_token!("7"),
text_token!("8"),
text_token!("9")
)),
(u32::from_str(n).unwrap())
)
);
make_fn!(ws<StrIter, &str>,
consume_all!(either!(
text_token!(" "),
text_token!("\t")
))
);
make_fn!(ratio<StrIter, Ratio<u32>>,
do_each!(
// First we assert non-zero numerator
_ => nonzero,
numer => num,
_ => optional!(ws),
_ => text_token!("/"),
_ => optional!(ws),
denom => num,
(Ratio::new(numer, denom))
)
);
make_fn!(unit<StrIter, &str>,
do_each!(
u => either!(
text_token!("tsp"),
text_token!("tbsp"),
text_token!("floz"),
text_token!("ml"),
text_token!("ltr"),
text_token!("cup"),
text_token!("qrt"),
text_token!("pint"),
text_token!("pnt"),
text_token!("gal"),
text_token!("gal"),
text_token!("cnt"),
text_token!("g"),
text_token!("gram")),
(u))
);
make_fn!(
quantity<StrIter, Quantity>,
either!(
do_each!(
whole => num,
frac => ratio,
(Quantity::Whole(whole) + Quantity::Frac(frac))
),
do_each!(
frac => ratio,
(Quantity::Frac(frac))
),
do_each!(
whole => num,
(Quantity::whole(whole))
)
)
);
make_fn!(
measure<StrIter, (Quantity, Option<&str>)>,
do_each!(
qty => quantity,
_ => optional!(ws),
unit => optional!(unit),
((qty, unit))
)
);