diff --git a/Cargo.lock b/Cargo.lock index 320c40b..aaa043f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,6 +9,14 @@ dependencies = [ "thiserror", ] +[[package]] +name = "jlabel-question" +version = "0.1.0" +dependencies = [ + "jlabel", + "thiserror", +] + [[package]] name = "proc-macro2" version = "1.0.76" diff --git a/Cargo.toml b/Cargo.toml index d092e80..59d8f28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,11 @@ -[package] -name = "jlabel" -version = "0.1.0" -edition = "2021" +[workspace] +resolver = "2" +members = ["crates/*"] +[workspace.package] +version = "0.1.0" rust-version = "1.65.0" -[dependencies] -thiserror = "1.0.56" +[workspace.dependencies] +jlabel = { path = "crates/jlabel", version = "0.1.0" } +jlabel-question = { path = "crates/jlabel-question", version = "0.1.0" } diff --git a/crates/jlabel-question/Cargo.toml b/crates/jlabel-question/Cargo.toml new file mode 100644 index 0000000..54a71f6 --- /dev/null +++ b/crates/jlabel-question/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "jlabel-question" +edition = "2021" + +version.workspace = true +rust-version.workspace = true + +[dependencies] +thiserror = "1.0.56" +jlabel.workspace = true diff --git a/crates/jlabel-question/src/lib.rs b/crates/jlabel-question/src/lib.rs new file mode 100644 index 0000000..46ba956 --- /dev/null +++ b/crates/jlabel-question/src/lib.rs @@ -0,0 +1,148 @@ +pub mod position; + +use std::num::ParseIntError; + +use position::{ + position, AllPosition, BooleanPosition, CategoryPosition, PhonePosition, Position, + SignedRangePosition, UndefinedPotision, UnsignedRangePosition, +}; + +use jlabel::Label; + +#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)] +pub enum ParseError { + #[error("Failed splitting")] + FailSplitting, + #[error("Position mismatch")] + PositionMismatch, + #[error("Invalid position")] + InvalidPosition, + #[error("Empty patterns or range")] + Empty, + #[error("Incontinuous range")] + IncontinuousRange, + #[error("Failed wildcard: {0}")] + FailWildcard(ParseIntError), + #[error("Failed literal: {0}")] + FailLiteral(ParseIntError), + #[error("Invalid boolean: {0}")] + InvalidBoolean(String), +} + +fn split_pattern(pattern: &str) -> Option<(&str, &str, &str)> { + let start = if pattern.starts_with("*/") { + 4 + } else if pattern.starts_with('*') { + 2 + } else { + 0 + }; + let end = if pattern.ends_with(":*") { + pattern.len().checked_sub(4)? + } else if pattern.ends_with('*') { + pattern.len().checked_sub(2)? + } else { + pattern.len() + }; + if start > end { + return None; + } + + Some((&pattern[..start], &pattern[start..end], &pattern[end..])) +} + +macro_rules! match_position { + ($position:expr, $ranges:expr, [$($name:ident),*]) => { + match $position { + $( + AllPosition::$name(position) => Ok(AllQuestion::$name(Question::new(position, $ranges)?)), + )* + } + }; +} + +pub fn question(patterns: &[&str]) -> Result { + let [first, rest @ ..] = patterns else { + return Err(ParseError::Empty); + }; + let (prefix, range, suffix) = split_pattern(first).ok_or(ParseError::FailSplitting)?; + + let mut ranges = Vec::with_capacity(patterns.len()); + ranges.push(range); + + for pattern in rest { + let (pre, range, suf) = split_pattern(pattern).ok_or(ParseError::FailSplitting)?; + if pre != prefix || suf != suffix { + return Err(ParseError::PositionMismatch); + } + ranges.push(range); + } + + match_position!( + position(prefix, suffix).ok_or(ParseError::InvalidPosition)?, + &ranges, + [ + Phone, + SignedRange, + UnsignedRange, + Boolean, + Category, + Undefined + ] + ) +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AllQuestion { + Phone(Question), + SignedRange(Question), + UnsignedRange(Question), + Boolean(Question), + Category(Question), + Undefined(Question), +} + +impl AllQuestion { + pub fn test(&self, label: &Label) -> bool { + match self { + Self::Phone(q) => q.test(label), + Self::SignedRange(q) => q.test(label), + Self::UnsignedRange(q) => q.test(label), + Self::Boolean(q) => q.test(label), + Self::Category(q) => q.test(label), + Self::Undefined(q) => q.test(label), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Question { + pub position: P, + pub range: Option, +} + +impl Question

{ + pub fn new(position: P, ranges: &[&str]) -> Result { + match ranges { + ["xx"] => Ok(Self { + range: None, + position, + }), + ranges => Ok(Self { + range: Some(position.range(ranges)?), + position, + }), + } + } + + pub fn test(&self, label: &Label) -> bool { + match (&self.range, self.position.get(label)) { + (Some(range), Some(target)) => self.position.test(range, target), + (None, None) => true, + _ => false, + } + } +} + +#[cfg(test)] +mod tests; diff --git a/src/question/position.rs b/crates/jlabel-question/src/position.rs similarity index 66% rename from src/question/position.rs rename to crates/jlabel-question/src/position.rs index e4aa1c6..fb46f32 100644 --- a/src/question/position.rs +++ b/crates/jlabel-question/src/position.rs @@ -1,5 +1,7 @@ use std::{fmt::Debug, ops::Range}; +use crate::Label; + use super::ParseError; pub fn position(prefix: &str, suffix: &str) -> Option { @@ -87,15 +89,28 @@ pub enum AllPosition { Undefined(UndefinedPotision), } +macro_rules! as_ref_map { + ($label:ident.$block:ident.$prop:ident) => { + $label.$block.as_ref().map(|b| &b.$prop) + }; +} + +macro_rules! as_ref_and_then { + ($label:ident.$block:ident.$prop:ident) => { + $label.$block.as_ref().and_then(|b| b.$prop.as_ref()) + }; +} + pub trait Position { type Target; type Range; fn range(&self, ranges: &[&str]) -> Result; + fn get<'a>(&self, label: &'a Label) -> Option<&'a Self::Target>; fn test(&self, range: &Self::Range, target: &Self::Target) -> bool; } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PhonePosition { P1, P2, @@ -112,12 +127,22 @@ impl Position for PhonePosition { Ok(ranges.iter().map(|s| s.to_string()).collect()) } + fn get<'a>(&self, label: &'a Label) -> Option<&'a Self::Target> { + match self { + Self::P1 => label.phoneme.p1.as_ref(), + Self::P2 => label.phoneme.p2.as_ref(), + Self::P3 => label.phoneme.c.as_ref(), + Self::P4 => label.phoneme.n1.as_ref(), + Self::P5 => label.phoneme.n2.as_ref(), + } + } + fn test(&self, range: &Self::Range, target: &Self::Target) -> bool { range.contains(target) } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SignedRangePosition { A1, } @@ -136,6 +161,12 @@ impl Position for SignedRangePosition { Ok(range) } + fn get<'a>(&self, label: &'a Label) -> Option<&'a Self::Target> { + match self { + Self::A1 => as_ref_map!(label.mora.relative_accent_position), + } + } + fn test(&self, range: &Self::Range, target: &Self::Target) -> bool { range.contains(target) } @@ -161,7 +192,7 @@ fn range_i8(s: &str) -> Result, ParseError> { Ok(range) } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum UnsignedRangePosition { A2, A3, @@ -213,6 +244,38 @@ impl Position for UnsignedRangePosition { Ok(range) } + fn get<'a>(&self, label: &'a Label) -> Option<&'a Self::Target> { + match self { + Self::A2 => as_ref_map!(label.mora.position_forward), + Self::A3 => as_ref_map!(label.mora.position_backward), + Self::E1 => as_ref_map!(label.accent_phrase_prev.mora_count), + Self::E2 => as_ref_map!(label.accent_phrase_prev.accent_position), + Self::F1 => as_ref_map!(label.accent_phrase_curr.mora_count), + Self::F2 => as_ref_map!(label.accent_phrase_curr.accent_position), + Self::F5 => as_ref_map!(label.accent_phrase_curr.accent_phrase_position_forward), + Self::F6 => as_ref_map!(label.accent_phrase_curr.accent_phrase_position_backward), + Self::F7 => as_ref_map!(label.accent_phrase_curr.mora_position_forward), + Self::F8 => as_ref_map!(label.accent_phrase_curr.mora_position_backward), + Self::G1 => as_ref_map!(label.accent_phrase_next.mora_count), + Self::G2 => as_ref_map!(label.accent_phrase_next.accent_position), + Self::H1 => as_ref_map!(label.breath_group_prev.accent_phrase_count), + Self::H2 => as_ref_map!(label.breath_group_prev.mora_count), + Self::I1 => as_ref_map!(label.breath_group_curr.accent_phrase_count), + Self::I2 => as_ref_map!(label.breath_group_curr.mora_count), + Self::I3 => as_ref_map!(label.breath_group_curr.breath_group_position_forward), + Self::I4 => as_ref_map!(label.breath_group_curr.breath_group_position_backward), + Self::I5 => as_ref_map!(label.breath_group_curr.accent_phrase_position_forward), + Self::I6 => as_ref_map!(label.breath_group_curr.accent_phrase_position_backward), + Self::I7 => as_ref_map!(label.breath_group_curr.mora_position_forward), + Self::I8 => as_ref_map!(label.breath_group_curr.mora_position_backward), + Self::J1 => as_ref_map!(label.breath_group_next.accent_phrase_count), + Self::J2 => as_ref_map!(label.breath_group_next.mora_count), + Self::K1 => Some(&label.utterance.breath_group_count), + Self::K2 => Some(&label.utterance.accent_phrase_count), + Self::K3 => Some(&label.utterance.mora_count), + } + } + fn test(&self, range: &Self::Range, target: &Self::Target) -> bool { range.contains(target) } @@ -251,7 +314,7 @@ where } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BooleanPosition { E3, E5, @@ -275,12 +338,22 @@ impl Position for BooleanPosition { } } + fn get<'a>(&self, label: &'a Label) -> Option<&'a Self::Target> { + match self { + Self::E3 => as_ref_map!(label.accent_phrase_prev.is_interrogative), + Self::E5 => as_ref_and_then!(label.accent_phrase_prev.is_pause_insertion), + Self::F3 => as_ref_map!(label.accent_phrase_curr.is_interrogative), + Self::G3 => as_ref_map!(label.accent_phrase_next.is_interrogative), + Self::G5 => as_ref_and_then!(label.accent_phrase_next.is_pause_insertion), + } + } + fn test(&self, range: &Self::Range, target: &Self::Target) -> bool { range == target } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CategoryPosition { B1, B2, @@ -304,12 +377,26 @@ impl Position for CategoryPosition { .collect() } + fn get<'a>(&self, label: &'a Label) -> Option<&'a Self::Target> { + match self { + Self::B1 => as_ref_and_then!(label.word_prev.pos), + Self::B2 => as_ref_and_then!(label.word_prev.ctype), + Self::B3 => as_ref_and_then!(label.word_prev.cform), + Self::C1 => as_ref_and_then!(label.word_curr.pos), + Self::C2 => as_ref_and_then!(label.word_curr.ctype), + Self::C3 => as_ref_and_then!(label.word_curr.cform), + Self::D1 => as_ref_and_then!(label.word_next.pos), + Self::D2 => as_ref_and_then!(label.word_next.ctype), + Self::D3 => as_ref_and_then!(label.word_next.cform), + } + } + fn test(&self, range: &Self::Range, target: &Self::Target) -> bool { range.contains(target) } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum UndefinedPotision { E4, F4, @@ -324,6 +411,10 @@ impl Position for UndefinedPotision { Ok(()) } + fn get<'a>(&self, _: &'a Label) -> Option<&'a Self::Target> { + None + } + fn test(&self, _: &Self::Range, _: &Self::Target) -> bool { true } @@ -331,12 +422,7 @@ impl Position for UndefinedPotision { #[cfg(test)] mod tests { - use crate::question::{ - position::{extend_range, range_u8}, - ParseError, - }; - - use super::range_i8; + use super::*; #[test] fn parse_i8_range() { diff --git a/crates/jlabel-question/src/tests.rs b/crates/jlabel-question/src/tests.rs new file mode 100644 index 0000000..222c659 --- /dev/null +++ b/crates/jlabel-question/src/tests.rs @@ -0,0 +1,369 @@ +use super::*; +use jlabel::{ + AccentPhraseCurrent, AccentPhrasePrevNext, BreathGroupCurrent, BreathGroupPrevNext, Label, + Mora, Phoneme, Utterance, Word, +}; + +#[test] +fn splitter() { + assert_eq!(split_pattern("a^*").unwrap(), ("", "a", "^*")); + assert_eq!(split_pattern("*/A:-??+*").unwrap(), ("*/A:", "-??", "+*")); + assert_eq!(split_pattern("*|?+*").unwrap(), ("*|", "?", "+*")); + assert_eq!(split_pattern("*-1").unwrap(), ("*-", "1", "")); + + assert!(split_pattern("*").is_none()); + assert!(split_pattern(":*").is_none()); + assert!(split_pattern("*/A:*").is_none()); +} + +#[test] +fn parse_question() { + assert_eq!( + question(&["a^*", "A^*"]).unwrap(), + AllQuestion::Phone(Question { + position: PhonePosition::P1, + range: Some(vec!["a".to_string(), "A".to_string()]) + }) + ); + assert_eq!( + question(&["*/A:-3+*"]).unwrap(), + AllQuestion::SignedRange(Question { + position: SignedRangePosition::A1, + range: Some(-3..-2) + }) + ); + assert_eq!( + question(&["*/A:-??+*", "*/A:-?+*", "*/A:?+*", "*/A:10+*", "*/A:11+*",]).unwrap(), + AllQuestion::SignedRange(Question { + position: SignedRangePosition::A1, + range: Some(-99..12) + }) + ); + assert_eq!( + question(&["*_42/I:*"]).unwrap(), + AllQuestion::UnsignedRange(Question { + position: UnsignedRangePosition::H2, + range: Some(42..43) + }) + ); + assert_eq!( + question(&["*_?/I:*", "*_1?/I:*", "*_2?/I:*", "*_30/I:*", "*_31/I:*",]).unwrap(), + AllQuestion::UnsignedRange(Question { + position: UnsignedRangePosition::H2, + range: Some(1..32) + }) + ); + assert_eq!( + question(&["*%1_*"]).unwrap(), + AllQuestion::Boolean(Question { + position: BooleanPosition::G3, + range: Some(true) + }) + ); + assert_eq!( + question(&["*/B:17-*", "*/B:20-*"]).unwrap(), + AllQuestion::Category(Question { + position: CategoryPosition::B1, + range: Some(vec![17, 20]) + }) + ); + assert_eq!( + question(&["*_xx_*"]).unwrap(), + AllQuestion::Undefined(Question { + position: UndefinedPotision::G4, + range: None + }) + ); +} + +#[test] +fn parse_question_err() { + use ParseError::*; + + assert_eq!(question(&[]), Err(Empty)); + assert_eq!(question(&["*/A:*"]), Err(FailSplitting)); + assert_eq!(question(&["*/A:-??+*", "*/A:*"]), Err(FailSplitting)); + assert_eq!(question(&["*/A:-??+*", "*/B:0+*"]), Err(PositionMismatch)); + assert_eq!(question(&["*/A:0/B:*"]), Err(InvalidPosition)); +} + +#[test] +fn query() { + // Note: this Label is created randomly, and is invalid. + let label = Label { + phoneme: Phoneme { + p2: Some("b".to_string()), + p1: Some("o".to_string()), + c: Some("N".to_string()), + n1: Some("s".to_string()), + n2: Some("a".to_string()), + }, + mora: Some(Mora { + relative_accent_position: -6, + position_forward: 2, + position_backward: 8, + }), + word_prev: None, + word_curr: Some(Word { + pos: Some(1), + ctype: None, + cform: None, + }), + word_next: None, + accent_phrase_prev: None, + accent_phrase_curr: None, + accent_phrase_next: None, + breath_group_prev: None, + breath_group_curr: None, + breath_group_next: None, + utterance: Utterance { + breath_group_count: 3, + accent_phrase_count: 6, + mora_count: 10, + }, + }; + + assert!(!question(&["*=i/A:*"]).unwrap().test(&label)); + + assert!(!question(&["*/A:-??+*", "*/A:-9+*"]).unwrap().test(&label)); + assert!(question(&["*/A:-6+*"]).unwrap().test(&label)); + + assert!(question(&["*+8/B:*"]).unwrap().test(&label)); + + assert!(question(&["*-xx_*"]).unwrap().test(&label)); + assert!(question(&["*/C:01_*"]).unwrap().test(&label)); +} + +#[test] +fn all_query() { + let nones = Label { + phoneme: Phoneme { + p2: None, + p1: None, + c: None, + n1: None, + n2: None, + }, + mora: None, + word_prev: None, + word_curr: None, + word_next: None, + accent_phrase_prev: None, + accent_phrase_curr: None, + accent_phrase_next: None, + breath_group_prev: None, + breath_group_curr: None, + breath_group_next: None, + utterance: Utterance { + breath_group_count: 254, + accent_phrase_count: 254, + mora_count: 254, + }, + }; + let zeros = Label { + phoneme: Phoneme { + p2: Some("z".to_string()), + p1: Some("z".to_string()), + c: Some("z".to_string()), + n1: Some("z".to_string()), + n2: Some("z".to_string()), + }, + mora: Some(Mora { + relative_accent_position: 0, + position_forward: 0, + position_backward: 0, + }), + word_prev: Some(Word { + pos: Some(0), + ctype: Some(0), + cform: Some(0), + }), + word_curr: Some(Word { + pos: Some(0), + ctype: Some(0), + cform: Some(0), + }), + word_next: Some(Word { + pos: Some(0), + ctype: Some(0), + cform: Some(0), + }), + accent_phrase_prev: Some(AccentPhrasePrevNext { + mora_count: 0, + accent_position: 0, + is_interrogative: false, + is_pause_insertion: Some(false), + }), + accent_phrase_curr: Some(AccentPhraseCurrent { + mora_count: 0, + accent_position: 0, + is_interrogative: false, + accent_phrase_position_forward: 0, + accent_phrase_position_backward: 0, + mora_position_forward: 0, + mora_position_backward: 0, + }), + accent_phrase_next: Some(AccentPhrasePrevNext { + mora_count: 0, + accent_position: 0, + is_interrogative: false, + is_pause_insertion: Some(false), + }), + breath_group_prev: Some(BreathGroupPrevNext { + accent_phrase_count: 0, + mora_count: 0, + }), + breath_group_curr: Some(BreathGroupCurrent { + accent_phrase_count: 0, + mora_count: 0, + breath_group_position_forward: 0, + breath_group_position_backward: 0, + accent_phrase_position_forward: 0, + accent_phrase_position_backward: 0, + mora_position_forward: 0, + mora_position_backward: 0, + }), + breath_group_next: Some(BreathGroupPrevNext { + accent_phrase_count: 0, + mora_count: 0, + }), + utterance: Utterance { + breath_group_count: 0, + accent_phrase_count: 0, + mora_count: 0, + }, + }; + + for position in [ + PhonePosition::P1, + PhonePosition::P2, + PhonePosition::P3, + PhonePosition::P4, + PhonePosition::P5, + ] { + let q = AllQuestion::Phone(Question { + position, + range: None, + }); + assert!(q.test(&nones)); + let q = AllQuestion::Phone(Question { + position, + range: Some(vec!["z".to_string()]), + }); + assert!(q.test(&zeros)); + } + + for position in [ + CategoryPosition::B1, + CategoryPosition::B2, + CategoryPosition::B3, + CategoryPosition::C1, + CategoryPosition::C2, + CategoryPosition::C3, + CategoryPosition::D1, + CategoryPosition::D2, + CategoryPosition::D3, + ] { + let q = AllQuestion::Category(Question { + position, + range: None, + }); + assert!(q.test(&nones)); + let q = AllQuestion::Category(Question { + position, + range: Some(vec![0]), + }); + assert!(q.test(&zeros)); + } + + let q = AllQuestion::SignedRange(Question { + position: SignedRangePosition::A1, + range: None, + }); + assert!(q.test(&nones)); + let q = AllQuestion::SignedRange(Question { + position: SignedRangePosition::A1, + range: Some(0..1), + }); + assert!(q.test(&zeros)); + + for position in [ + UnsignedRangePosition::A2, + UnsignedRangePosition::A3, + UnsignedRangePosition::E1, + UnsignedRangePosition::E2, + UnsignedRangePosition::F1, + UnsignedRangePosition::F2, + UnsignedRangePosition::F5, + UnsignedRangePosition::F6, + UnsignedRangePosition::F7, + UnsignedRangePosition::F8, + UnsignedRangePosition::G1, + UnsignedRangePosition::G2, + UnsignedRangePosition::H1, + UnsignedRangePosition::H2, + UnsignedRangePosition::I1, + UnsignedRangePosition::I2, + UnsignedRangePosition::I3, + UnsignedRangePosition::I4, + UnsignedRangePosition::I5, + UnsignedRangePosition::I6, + UnsignedRangePosition::I7, + UnsignedRangePosition::I8, + UnsignedRangePosition::J1, + UnsignedRangePosition::J2, + ] { + let q = AllQuestion::UnsignedRange(Question { + position, + range: None, + }); + assert!(q.test(&nones)); + let q = AllQuestion::UnsignedRange(Question { + position, + range: Some(0..1), + }); + assert!(q.test(&zeros)); + } + + for position in [ + BooleanPosition::E3, + BooleanPosition::E5, + BooleanPosition::F3, + BooleanPosition::G3, + BooleanPosition::G5, + ] { + let q = AllQuestion::Boolean(Question { + position, + range: None, + }); + assert!(q.test(&nones)); + let q = AllQuestion::Boolean(Question { + position, + range: Some(false), + }); + assert!(q.test(&zeros)); + } + + for position in [ + UnsignedRangePosition::K1, + UnsignedRangePosition::K2, + UnsignedRangePosition::K3, + ] { + let q = AllQuestion::UnsignedRange(Question { + position, + range: Some(254..255), + }); + assert!(q.test(&nones)); + let q = AllQuestion::UnsignedRange(Question { + position, + range: Some(0..1), + }); + assert!(q.test(&zeros)); + } + + let q = AllQuestion::Undefined(Question { + position: UndefinedPotision::E4, + range: None, + }); + assert!(q.test(&nones)); +} diff --git a/tests/questions/NOTICE b/crates/jlabel-question/tests/NOTICE similarity index 100% rename from tests/questions/NOTICE rename to crates/jlabel-question/tests/NOTICE diff --git a/tests/questions/questions.hed b/crates/jlabel-question/tests/questions.hed similarity index 100% rename from tests/questions/questions.hed rename to crates/jlabel-question/tests/questions.hed diff --git a/tests/questions/main.rs b/crates/jlabel-question/tests/questions.rs similarity index 94% rename from tests/questions/main.rs rename to crates/jlabel-question/tests/questions.rs index 064791c..fedbe74 100644 --- a/tests/questions/main.rs +++ b/crates/jlabel-question/tests/questions.rs @@ -4,11 +4,11 @@ use std::{ ops::Range, }; -use jlabel::{question, AllQuestion}; +use jlabel_question::{question, AllQuestion}; #[test] fn parse_all_questions() { - let file = File::open("tests/questions/questions.hed").unwrap(); + let file = File::open("tests/questions.hed").unwrap(); let reader = BufReader::new(file); for line in reader.lines() { diff --git a/crates/jlabel/Cargo.toml b/crates/jlabel/Cargo.toml new file mode 100644 index 0000000..5baeaf5 --- /dev/null +++ b/crates/jlabel/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "jlabel" +edition = "2021" + +version.workspace = true +rust-version.workspace = true + +[dependencies] +thiserror = "1.0.56" + diff --git a/crates/jlabel/src/fullcontext_label.rs b/crates/jlabel/src/fullcontext_label.rs new file mode 100644 index 0000000..1cad631 --- /dev/null +++ b/crates/jlabel/src/fullcontext_label.rs @@ -0,0 +1,112 @@ +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Label { + pub phoneme: Phoneme, + pub mora: Option, + pub word_prev: Option, + pub word_curr: Option, + pub word_next: Option, + pub accent_phrase_prev: Option, + pub accent_phrase_curr: Option, + pub accent_phrase_next: Option, + pub breath_group_prev: Option, + pub breath_group_curr: Option, + pub breath_group_next: Option, + pub utterance: Utterance, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Phoneme { + pub p2: Option, + pub p1: Option, + pub c: Option, + pub n1: Option, + pub n2: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Mora { + /// the difference between accent type and position of the current mora identity + pub relative_accent_position: i8, + /// position of the current mora identity in the current accent phrase (forward) + pub position_forward: u8, + /// position of the current mora identity in the current accent phrase (backward) + pub position_backward: u8, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Word { + /// pos (part-of-speech) of the word + pub pos: Option, + /// conjugation type of the word + pub ctype: Option, + /// inflected forms of the word + pub cform: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AccentPhraseCurrent { + /// the number of moras in the current accent phrase + pub mora_count: u8, + /// accent type in the current accent phrase + pub accent_position: u8, + /// whether the current accent phrase interrogative or not + pub is_interrogative: bool, + /// position of the current accent phrase identity in the current breath group by the accent phrase (forward) + pub accent_phrase_position_forward: u8, + /// position of the current accent phrase identity in the current breath group by the accent phrase (backward) + pub accent_phrase_position_backward: u8, + /// position of the current accent phrase identity in the current breath group by the mora (forward) + pub mora_position_forward: u8, + /// position of the current accent phrase identity in the current breath group by the mora (backward) + pub mora_position_backward: u8, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AccentPhrasePrevNext { + /// the number of moras in the accent phrase + pub mora_count: u8, + /// accent type in the accent phrase + pub accent_position: u8, + /// whether the accent phrase interrogative or not + pub is_interrogative: bool, + /// whether pause insertion or not in between the accent phrase and the current accent phrase + pub is_pause_insertion: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BreathGroupCurrent { + /// the number of accent phrases in the current breath group + pub accent_phrase_count: u8, + /// the number of moras in the current breath group + pub mora_count: u8, + /// position of the current breath group identity by breath group (forward) + pub breath_group_position_forward: u8, + /// position of the current breath group identity by breath group (backward) + pub breath_group_position_backward: u8, + /// position of the current breath group identity by accent phrase (forward) + pub accent_phrase_position_forward: u8, + /// position of the current breath group identity by accent phrase (backward) + pub accent_phrase_position_backward: u8, + /// position of the current breath group identity by mora (forward) + pub mora_position_forward: u8, + /// position of the current breath group identity by mora (backward) + pub mora_position_backward: u8, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BreathGroupPrevNext { + /// the number of accent phrases in the breath group + pub accent_phrase_count: u8, + /// the number of moras in the breath group + pub mora_count: u8, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Utterance { + /// the number of breath groups in this utterance + pub breath_group_count: u8, + /// the number of accent phrases in this utterance + pub accent_phrase_count: u8, + /// the number of moras in this utterance + pub mora_count: u8, +} diff --git a/src/lib.rs b/crates/jlabel/src/lib.rs similarity index 58% rename from src/lib.rs rename to crates/jlabel/src/lib.rs index 9519160..8f560b7 100644 --- a/src/lib.rs +++ b/crates/jlabel/src/lib.rs @@ -1,7 +1,5 @@ mod fullcontext_label; mod parser; -mod question; mod serializer; pub use fullcontext_label::*; -pub use question::{question, AllQuestion}; diff --git a/src/parser.rs b/crates/jlabel/src/parser.rs similarity index 100% rename from src/parser.rs rename to crates/jlabel/src/parser.rs diff --git a/src/serializer.rs b/crates/jlabel/src/serializer.rs similarity index 100% rename from src/serializer.rs rename to crates/jlabel/src/serializer.rs diff --git a/tests/fixtures.rs b/crates/jlabel/tests/fixtures.rs similarity index 100% rename from tests/fixtures.rs rename to crates/jlabel/tests/fixtures.rs diff --git a/tests/parser.rs b/crates/jlabel/tests/parser.rs similarity index 100% rename from tests/parser.rs rename to crates/jlabel/tests/parser.rs diff --git a/tests/serializer.rs b/crates/jlabel/tests/serializer.rs similarity index 100% rename from tests/serializer.rs rename to crates/jlabel/tests/serializer.rs diff --git a/src/fullcontext_label.rs b/src/fullcontext_label.rs deleted file mode 100644 index a5451d8..0000000 --- a/src/fullcontext_label.rs +++ /dev/null @@ -1,478 +0,0 @@ -use crate::{ - question::position::{ - BooleanPosition, CategoryPosition, PhonePosition, SignedRangePosition, - UnsignedRangePosition, - }, - question::AllQuestion, -}; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Label { - pub phoneme: Phoneme, - pub mora: Option, - pub word_prev: Option, - pub word_curr: Option, - pub word_next: Option, - pub accent_phrase_prev: Option, - pub accent_phrase_curr: Option, - pub accent_phrase_next: Option, - pub breath_group_prev: Option, - pub breath_group_curr: Option, - pub breath_group_next: Option, - pub utterance: Utterance, -} - -impl Label { - pub fn satisfies(&self, question: &AllQuestion) -> bool { - use AllQuestion::*; - match question { - Phone(question) => { - use PhonePosition::*; - match question.position { - P1 => question.test(&self.phoneme.p1), - P2 => question.test(&self.phoneme.p2), - P3 => question.test(&self.phoneme.c), - P4 => question.test(&self.phoneme.n1), - P5 => question.test(&self.phoneme.n2), - } - } - SignedRange(question) => { - use SignedRangePosition::*; - match question.position { - A1 => question.test(&self.mora.as_ref().map(|m| m.relative_accent_position)), - } - } - UnsignedRange(question) => { - use UnsignedRangePosition::*; - match question.position { - A2 => question.test(&self.mora.as_ref().map(|m| m.position_forward)), - A3 => question.test(&self.mora.as_ref().map(|m| m.position_backward)), - E1 => question.test(&self.accent_phrase_prev.as_ref().map(|a| a.mora_count)), - E2 => { - question.test(&self.accent_phrase_prev.as_ref().map(|a| a.accent_position)) - } - F1 => question.test(&self.accent_phrase_curr.as_ref().map(|a| a.mora_count)), - F2 => { - question.test(&self.accent_phrase_curr.as_ref().map(|a| a.accent_position)) - } - F5 => question.test( - &self - .accent_phrase_curr - .as_ref() - .map(|a| a.accent_phrase_position_forward), - ), - F6 => question.test( - &self - .accent_phrase_curr - .as_ref() - .map(|a| a.accent_phrase_position_backward), - ), - F7 => question.test( - &self - .accent_phrase_curr - .as_ref() - .map(|a| a.mora_position_forward), - ), - F8 => question.test( - &self - .accent_phrase_curr - .as_ref() - .map(|a| a.mora_position_backward), - ), - G1 => question.test(&self.accent_phrase_next.as_ref().map(|a| a.mora_count)), - G2 => { - question.test(&self.accent_phrase_next.as_ref().map(|a| a.accent_position)) - } - H1 => question.test( - &self - .breath_group_prev - .as_ref() - .map(|b| b.accent_phrase_count), - ), - H2 => question.test(&self.breath_group_prev.as_ref().map(|b| b.mora_count)), - I1 => question.test( - &self - .breath_group_curr - .as_ref() - .map(|b| b.accent_phrase_count), - ), - I2 => question.test(&self.breath_group_curr.as_ref().map(|b| b.mora_count)), - I3 => question.test( - &self - .breath_group_curr - .as_ref() - .map(|b| b.breath_group_position_forward), - ), - I4 => question.test( - &self - .breath_group_curr - .as_ref() - .map(|b| b.breath_group_position_backward), - ), - I5 => question.test( - &self - .breath_group_curr - .as_ref() - .map(|b| b.accent_phrase_position_forward), - ), - I6 => question.test( - &self - .breath_group_curr - .as_ref() - .map(|b| b.accent_phrase_position_backward), - ), - I7 => question.test( - &self - .breath_group_curr - .as_ref() - .map(|b| b.mora_position_forward), - ), - I8 => question.test( - &self - .breath_group_curr - .as_ref() - .map(|b| b.mora_position_backward), - ), - J1 => question.test( - &self - .breath_group_next - .as_ref() - .map(|b| b.accent_phrase_count), - ), - J2 => question.test(&self.breath_group_next.as_ref().map(|b| b.mora_count)), - K1 => question.test(&Some(self.utterance.breath_group_count)), - K2 => question.test(&Some(self.utterance.accent_phrase_count)), - K3 => question.test(&Some(self.utterance.mora_count)), - } - } - Boolean(question) => { - use BooleanPosition::*; - match question.position { - E3 => { - question.test(&self.accent_phrase_prev.as_ref().map(|a| a.is_interrogative)) - } - E5 => question.test( - &self - .accent_phrase_prev - .as_ref() - .and_then(|a| a.is_pause_insertion), - ), - F3 => { - question.test(&self.accent_phrase_curr.as_ref().map(|a| a.is_interrogative)) - } - G3 => { - question.test(&self.accent_phrase_next.as_ref().map(|a| a.is_interrogative)) - } - G5 => question.test( - &self - .accent_phrase_next - .as_ref() - .and_then(|a| a.is_pause_insertion), - ), - } - } - Category(question) => { - use CategoryPosition::*; - match question.position { - B1 => question.test(&self.word_prev.as_ref().and_then(|w| w.pos)), - B2 => question.test(&self.word_prev.as_ref().and_then(|w| w.ctype)), - B3 => question.test(&self.word_prev.as_ref().and_then(|w| w.cform)), - C1 => question.test(&self.word_curr.as_ref().and_then(|w| w.pos)), - C2 => question.test(&self.word_curr.as_ref().and_then(|w| w.ctype)), - C3 => question.test(&self.word_curr.as_ref().and_then(|w| w.cform)), - D1 => question.test(&self.word_next.as_ref().and_then(|w| w.pos)), - D2 => question.test(&self.word_next.as_ref().and_then(|w| w.ctype)), - D3 => question.test(&self.word_next.as_ref().and_then(|w| w.cform)), - } - } - Undefined(_) => true, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Phoneme { - pub p2: Option, - pub p1: Option, - pub c: Option, - pub n1: Option, - pub n2: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Mora { - /// the difference between accent type and position of the current mora identity - pub relative_accent_position: i8, - /// position of the current mora identity in the current accent phrase (forward) - pub position_forward: u8, - /// position of the current mora identity in the current accent phrase (backward) - pub position_backward: u8, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Word { - /// pos (part-of-speech) of the word - pub pos: Option, - /// conjugation type of the word - pub ctype: Option, - /// inflected forms of the word - pub cform: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct AccentPhraseCurrent { - /// the number of moras in the current accent phrase - pub mora_count: u8, - /// accent type in the current accent phrase - pub accent_position: u8, - /// whether the current accent phrase interrogative or not - pub is_interrogative: bool, - /// position of the current accent phrase identity in the current breath group by the accent phrase (forward) - pub accent_phrase_position_forward: u8, - /// position of the current accent phrase identity in the current breath group by the accent phrase (backward) - pub accent_phrase_position_backward: u8, - /// position of the current accent phrase identity in the current breath group by the mora (forward) - pub mora_position_forward: u8, - /// position of the current accent phrase identity in the current breath group by the mora (backward) - pub mora_position_backward: u8, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct AccentPhrasePrevNext { - /// the number of moras in the accent phrase - pub mora_count: u8, - /// accent type in the accent phrase - pub accent_position: u8, - /// whether the accent phrase interrogative or not - pub is_interrogative: bool, - /// whether pause insertion or not in between the accent phrase and the current accent phrase - pub is_pause_insertion: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct BreathGroupCurrent { - /// the number of accent phrases in the current breath group - pub accent_phrase_count: u8, - /// the number of moras in the current breath group - pub mora_count: u8, - /// position of the current breath group identity by breath group (forward) - pub breath_group_position_forward: u8, - /// position of the current breath group identity by breath group (backward) - pub breath_group_position_backward: u8, - /// position of the current breath group identity by accent phrase (forward) - pub accent_phrase_position_forward: u8, - /// position of the current breath group identity by accent phrase (backward) - pub accent_phrase_position_backward: u8, - /// position of the current breath group identity by mora (forward) - pub mora_position_forward: u8, - /// position of the current breath group identity by mora (backward) - pub mora_position_backward: u8, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct BreathGroupPrevNext { - /// the number of accent phrases in the breath group - pub accent_phrase_count: u8, - /// the number of moras in the breath group - pub mora_count: u8, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Utterance { - /// the number of breath groups in this utterance - pub breath_group_count: u8, - /// the number of accent phrases in this utterance - pub accent_phrase_count: u8, - /// the number of moras in this utterance - pub mora_count: u8, -} - -#[cfg(test)] -mod tests { - use crate::{ - fullcontext_label::{Mora, Word}, - question::{question, AllQuestion, Question}, - Label, - }; - - use super::{Phoneme, Utterance}; - - #[test] - fn query() { - // Note: this Label is created randomly, and is invalid. - let label = Label { - phoneme: Phoneme { - p2: Some("b".to_string()), - p1: Some("o".to_string()), - c: Some("N".to_string()), - n1: Some("s".to_string()), - n2: Some("a".to_string()), - }, - mora: Some(Mora { - relative_accent_position: -6, - position_forward: 2, - position_backward: 8, - }), - word_prev: None, - word_curr: Some(Word { - pos: Some(1), - ctype: None, - cform: None, - }), - word_next: None, - accent_phrase_prev: None, - accent_phrase_curr: None, - accent_phrase_next: None, - breath_group_prev: None, - breath_group_curr: None, - breath_group_next: None, - utterance: Utterance { - breath_group_count: 3, - accent_phrase_count: 6, - mora_count: 10, - }, - }; - - assert!(!label.satisfies(&question(&["*=i/A:*"]).unwrap())); - - assert!(!label.satisfies(&question(&["*/A:-??+*", "*/A:-9+*"]).unwrap())); - assert!(label.satisfies(&question(&["*/A:-6+*"]).unwrap())); - - assert!(label.satisfies(&question(&["*+8/B:*"]).unwrap())); - - assert!(label.satisfies(&question(&["*-xx_*"]).unwrap())); - assert!(label.satisfies(&question(&["*/C:01_*"]).unwrap())); - } - - #[test] - fn all_query() { - let label = Label { - phoneme: Phoneme { - p2: None, - p1: None, - c: None, - n1: None, - n2: None, - }, - mora: None, - word_prev: None, - word_curr: None, - word_next: None, - accent_phrase_prev: None, - accent_phrase_curr: None, - accent_phrase_next: None, - breath_group_prev: None, - breath_group_curr: None, - breath_group_next: None, - utterance: Utterance { - breath_group_count: 3, - accent_phrase_count: 6, - mora_count: 10, - }, - }; - - use crate::question::position::*; - - for position in [ - PhonePosition::P1, - PhonePosition::P2, - PhonePosition::P3, - PhonePosition::P4, - PhonePosition::P5, - ] { - assert!(label.satisfies(&AllQuestion::Phone(Question { - position, - range: None, - }))); - } - - for position in [ - CategoryPosition::B1, - CategoryPosition::B2, - CategoryPosition::B3, - CategoryPosition::C1, - CategoryPosition::C2, - CategoryPosition::C3, - CategoryPosition::D1, - CategoryPosition::D2, - CategoryPosition::D3, - ] { - assert!(label.satisfies(&AllQuestion::Category(Question { - position, - range: None, - }))); - } - - assert!(label.satisfies(&AllQuestion::SignedRange(Question { - position: SignedRangePosition::A1, - range: None, - }))); - - for position in [ - UnsignedRangePosition::A2, - UnsignedRangePosition::A3, - UnsignedRangePosition::E1, - UnsignedRangePosition::E2, - UnsignedRangePosition::F1, - UnsignedRangePosition::F2, - UnsignedRangePosition::F5, - UnsignedRangePosition::F6, - UnsignedRangePosition::F7, - UnsignedRangePosition::F8, - UnsignedRangePosition::G1, - UnsignedRangePosition::G2, - UnsignedRangePosition::H1, - UnsignedRangePosition::H2, - UnsignedRangePosition::I1, - UnsignedRangePosition::I2, - UnsignedRangePosition::I3, - UnsignedRangePosition::I4, - UnsignedRangePosition::I5, - UnsignedRangePosition::I6, - UnsignedRangePosition::I7, - UnsignedRangePosition::I8, - UnsignedRangePosition::J1, - UnsignedRangePosition::J2, - ] { - assert!(label.satisfies(&AllQuestion::UnsignedRange(Question { - position: position.clone(), - range: None, - }))); - assert!(!label.satisfies(&AllQuestion::UnsignedRange(Question { - position: position.clone(), - range: Some(1..2), - }))); - } - - for position in [ - BooleanPosition::E3, - BooleanPosition::E5, - BooleanPosition::F3, - BooleanPosition::G3, - BooleanPosition::G5, - ] { - assert!(label.satisfies(&AllQuestion::Boolean(Question { - position, - range: None, - }))); - } - - assert!(label.satisfies(&AllQuestion::UnsignedRange(Question { - position: UnsignedRangePosition::K1, - range: Some(3..4), - }))); - assert!(label.satisfies(&AllQuestion::UnsignedRange(Question { - position: UnsignedRangePosition::K2, - range: Some(6..7), - }))); - assert!(label.satisfies(&AllQuestion::UnsignedRange(Question { - position: UnsignedRangePosition::K3, - range: Some(5..11), - }))); - - assert!(label.satisfies(&AllQuestion::Undefined(Question { - position: UndefinedPotision::E4, - range: None, - }))); - } -} diff --git a/src/question/mod.rs b/src/question/mod.rs deleted file mode 100644 index d037503..0000000 --- a/src/question/mod.rs +++ /dev/null @@ -1,211 +0,0 @@ -pub mod position; - -use std::num::ParseIntError; - -use position::{ - position, AllPosition, BooleanPosition, CategoryPosition, PhonePosition, Position, - SignedRangePosition, UndefinedPotision, UnsignedRangePosition, -}; - -#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)] -pub enum ParseError { - #[error("Failed splitting")] - FailSplitting, - #[error("Position mismatch")] - PositionMismatch, - #[error("Invalid position")] - InvalidPosition, - #[error("Empty patterns or range")] - Empty, - #[error("Incontinuous range")] - IncontinuousRange, - #[error("Failed wildcard: {0}")] - FailWildcard(ParseIntError), - #[error("Failed literal: {0}")] - FailLiteral(ParseIntError), - #[error("Invalid boolean: {0}")] - InvalidBoolean(String), -} - -fn split_pattern(pattern: &str) -> Option<(&str, &str, &str)> { - if !pattern.len() < 4 { - return None; - } - - let start = if pattern.starts_with("*/") { - 4 - } else if pattern.starts_with('*') { - 2 - } else { - 0 - }; - let end = if pattern.ends_with(":*") { - pattern.len() - 4 - } else if pattern.ends_with('*') { - pattern.len() - 2 - } else { - pattern.len() - }; - - Some((&pattern[..start], &pattern[start..end], &pattern[end..])) -} - -macro_rules! question_arm { - ($name:ident, $position:expr, $triplets:expr) => { - if $triplets.len() == 1 && $triplets[0].1 == "xx" { - Ok(AllQuestion::$name(Question::new_xx($position))) - } else { - let range = - $position.range(&$triplets.into_iter().map(|(_, r, _)| r).collect::>())?; - Ok(AllQuestion::$name(Question::new($position, range))) - } - }; -} - -pub fn question(patterns: &[&str]) -> Result { - let mut triplets = Vec::new(); - for pattern in patterns { - triplets.push(split_pattern(pattern).ok_or(ParseError::FailSplitting)?); - } - - let (prefix, _, suffix) = triplets.first().ok_or(ParseError::Empty)?; - if !triplets - .iter() - .all(|(pre, _, post)| pre == prefix && post == suffix) - { - return Err(ParseError::PositionMismatch); - } - - let position = position(prefix, suffix).ok_or(ParseError::InvalidPosition)?; - - use AllPosition::*; - match position { - Phone(position) => question_arm!(Phone, position, triplets), - SignedRange(position) => question_arm!(SignedRange, position, triplets), - UnsignedRange(position) => question_arm!(UnsignedRange, position, triplets), - Boolean(position) => question_arm!(Boolean, position, triplets), - Category(position) => question_arm!(Category, position, triplets), - Undefined(position) => question_arm!(Undefined, position, triplets), - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum AllQuestion { - Phone(Question), - SignedRange(Question), - UnsignedRange(Question), - Boolean(Question), - Category(Question), - Undefined(Question), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Question { - pub position: P, - pub range: Option, -} - -impl Question

{ - pub fn new(position: P, range: P::Range) -> Self { - Self { - position, - range: Some(range), - } - } - - pub fn new_xx(position: P) -> Self { - Self { - position, - range: None, - } - } - - pub fn test(&self, target: &Option) -> bool { - match (&self.range, target) { - (Some(range), Some(target)) => self.position.test(range, target), - (None, None) => true, - _ => false, - } - } -} - -#[cfg(test)] -mod tests { - use crate::question::{ - position::{ - BooleanPosition, CategoryPosition, PhonePosition, SignedRangePosition, - UndefinedPotision, UnsignedRangePosition, - }, - question, AllQuestion, Question, - }; - - use super::split_pattern; - - #[test] - fn splitter() { - assert_eq!(split_pattern("a^*"), Some(("", "a", "^*"))); - assert_eq!(split_pattern("*/A:-??+*"), Some(("*/A:", "-??", "+*"))); - assert_eq!(split_pattern("*|?+*"), Some(("*|", "?", "+*"))); - assert_eq!(split_pattern("*-1"), Some(("*-", "1", ""))); - } - - #[test] - fn parse_question() { - assert_eq!( - question(&["a^*", "A^*"]).unwrap(), - AllQuestion::Phone(Question { - position: PhonePosition::P1, - range: Some(vec!["a".to_string(), "A".to_string()]) - }) - ); - assert_eq!( - question(&["*/A:-3+*"]).unwrap(), - AllQuestion::SignedRange(Question { - position: SignedRangePosition::A1, - range: Some(-3..-2) - }) - ); - assert_eq!( - question(&["*/A:-??+*", "*/A:-?+*", "*/A:?+*", "*/A:10+*", "*/A:11+*",]).unwrap(), - AllQuestion::SignedRange(Question { - position: SignedRangePosition::A1, - range: Some(-99..12) - }) - ); - assert_eq!( - question(&["*_42/I:*"]).unwrap(), - AllQuestion::UnsignedRange(Question { - position: UnsignedRangePosition::H2, - range: Some(42..43) - }) - ); - assert_eq!( - question(&["*_?/I:*", "*_1?/I:*", "*_2?/I:*", "*_30/I:*", "*_31/I:*",]).unwrap(), - AllQuestion::UnsignedRange(Question { - position: UnsignedRangePosition::H2, - range: Some(1..32) - }) - ); - assert_eq!( - question(&["*%1_*"]).unwrap(), - AllQuestion::Boolean(Question { - position: BooleanPosition::G3, - range: Some(true) - }) - ); - assert_eq!( - question(&["*/B:17-*", "*/B:20-*"]).unwrap(), - AllQuestion::Category(Question { - position: CategoryPosition::B1, - range: Some(vec![17, 20]) - }) - ); - assert_eq!( - question(&["*_xx_*"]).unwrap(), - AllQuestion::Undefined(Question { - position: UndefinedPotision::G4, - range: None - }) - ); - } -}