-
Notifications
You must be signed in to change notification settings - Fork 0
research wafer inc duckling api types
Source files:
All types listed here are re-exported from src/lib.rs and are part of the
public API surface.
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct Entity {
pub body: String, // matched substring of the input text
pub start: usize, // byte offset of match start
pub end: usize, // byte offset of match end
pub value: DimensionValue, // resolved structured value
pub latent: Option<bool>, // Some(true) if latent/ambiguous; Some(false) otherwise
}The latent field is always Some(_) in practice — the resolve layer sets
it unconditionally. It serialises with #[serde(skip_serializing_if = "Option::is_none")].
Example:
assert_eq!(
parse_en("I need 42 widgets", &[DimensionKind::Numeral]),
vec![Entity {
body: "42".into(), start: 7, end: 9,
latent: Some(false),
value: DimensionValue::Numeral(42.0),
}]
);#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DimensionKind {
Numeral,
Ordinal,
Temperature,
Distance,
Volume,
Quantity,
AmountOfMoney,
Email,
PhoneNumber,
Url,
CreditCardNumber,
TimeGrain,
Duration,
Time,
}Implements Display — the string values used in serialisation:
| Variant | Display string |
|---|---|
Numeral |
"number" |
Ordinal |
"ordinal" |
Temperature |
"temperature" |
Distance |
"distance" |
Volume |
"volume" |
Quantity |
"quantity" |
AmountOfMoney |
"amount-of-money" |
Email |
"email" |
PhoneNumber |
"phone-number" |
Url |
"url" |
CreditCardNumber |
"credit-card-number" |
TimeGrain |
"time-grain" |
Duration |
"duration" |
Time |
"time" |
Passing &[] (empty slice) to parse enables all 14 dimensions.
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub enum DimensionValue {
Numeral(f64),
Ordinal(i64),
Temperature(MeasurementValue),
Distance(MeasurementValue),
Volume(MeasurementValue),
Quantity { measurement: MeasurementValue, product: Option<String> },
AmountOfMoney(MeasurementValue),
Email(String),
PhoneNumber(String),
Url { value: String, domain: String },
CreditCardNumber { value: String, issuer: String },
TimeGrain(Grain),
Duration { value: i64, grain: Grain, normalized_seconds: i64 },
Time(TimeValue),
}DimensionValue also exposes:
pub fn dim_kind(&self) -> DimensionKindwhich returns the corresponding DimensionKind for the variant.
graph TD
DV[DimensionValue]
DV --> Numeral["Numeral(f64)"]
DV --> Ordinal["Ordinal(i64)"]
DV --> Temperature["Temperature(MeasurementValue)"]
DV --> Distance["Distance(MeasurementValue)"]
DV --> Volume["Volume(MeasurementValue)"]
DV --> Quantity["Quantity { measurement: MeasurementValue, product: Option<String> }"]
DV --> AmountOfMoney["AmountOfMoney(MeasurementValue)"]
DV --> Email["Email(String)"]
DV --> PhoneNumber["PhoneNumber(String)"]
DV --> Url["Url { value: String, domain: String }"]
DV --> CreditCardNumber["CreditCardNumber { value: String, issuer: String }"]
DV --> TimeGrain["TimeGrain(Grain)"]
DV --> Duration["Duration { value: i64, grain: Grain, normalized_seconds: i64 }"]
DV --> Time["Time(TimeValue)"]
-
Duration.normalized_seconds— the count normalised to seconds using fixed grain multipliers (day=86400, week=604800, month=2592000, quarter=7776000, year=31536000). -
Quantity.product— present when the text names what is being measured, e.g."5 pounds of sugar"→product: Some("sugar"). -
CreditCardNumber.issuer— e.g."visa","mastercard".
Used by Temperature, Distance, Volume, Quantity, and AmountOfMoney.
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub enum MeasurementValue {
Value { value: f64, unit: String },
Interval { from: Option<MeasurementPoint>, to: Option<MeasurementPoint> },
}
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct MeasurementPoint {
pub value: f64,
pub unit: String,
}Example:
// "$42.50" → AmountOfMoney(Value { value: 42.5, unit: "USD" })
// "between 3 and 5 dollars" → AmountOfMoney(Interval { from: Some(...), to: Some(...) })#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub enum TimeValue {
Single {
value: TimePoint,
values: Vec<TimePoint>, // up to 3 next occurrences, primary is first
holiday: Option<String>, // serialised as "holidayBeta" when Some
},
Interval {
from: Option<TimePoint>,
to: Option<TimePoint>,
values: Vec<IntervalEndpoints>, // up to 3 next occurrences
holiday: Option<String>, // serialised as "holidayBeta" when Some
},
}The holiday field matches Haskell Duckling's holiday :: Maybe Text on
TimeValue. It serialises under the JSON key "holidayBeta" and is
omitted when None.
The values field for Single contains up to 3 TimePoint elements (the
primary value is the first); for Interval it contains up to 3
IntervalEndpoints — matching Haskell's TimeValue (SimpleValue v) [v1, v2, v3] holiday idiom.
No corpus example currently asserts on values — the corpus check helpers
(e.g. datetime() in
src/corpus/mod.rs)
match against value only and ignore values/holiday with ... The
behavior below is derived directly from
generate_extra_values
and
series::generate_series,
not from a written example.
values is populated by the series generator, which produces up to 3
future occurrences (falling back to past occurrences if none are in the
future) for recurring/unanchored time forms — expressions that don't pin
down a single instant. Confirmed recurring forms (series::generate_series
match arms): DayOfWeek, Month, Hour/HourMinute, DayOfMonth, Year,
PartOfDay, Weekend, Season, Holiday (only when no explicit year is
given), DateMDY (month/day without year), and Composed combinations of
these.
Concretely: "christmas" resolves as Holiday("Christmas", year_opt: None).
series_holiday
shows the fixed-year case (year_opt: Some(_)) short-circuits to a single
value with (vec![], vec![obj]) — so "christmas 2026" would NOT get
extra occurrences. Only the unpinned "christmas" walks forward year by year,
giving values: [<this year's Dec 25>, <next year>, <year after>].
Conversely, a fully anchored date like "July 1st 2026" (DateMDY with
year: Some(2026)) goes through the Some(y) branch of
series_date_mdy,
which returns a single-element series rather than an empty one — so values
still ends up with exactly 1 element, just via the "series produced one
result" path rather than the "series was empty, fall back to primary" path
in generate_extra_values. Either way: a year-pinned date never yields 3
occurrences, only an unpinned/recurring form does.
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub enum TimePoint {
Instant { value: DateTime<FixedOffset>, grain: Grain },
Naive { value: NaiveDateTime, grain: Grain },
}TimePoint also exposes:
pub fn grain(&self) -> GrainThis distinction is critical for the Ruby type mapping.
flowchart TD
expr["Input expression"] --> rel{"Relative to 'now'<br/>or explicit TZ?"}
rel -->|No — wall-clock / calendar| N["TimePoint::Naive\n(NaiveDateTime, grain)\nNo timezone baked in"]
rel -->|Yes — anchored to reference time| I["TimePoint::Instant\n(DateTime<FixedOffset>, grain)\nPinned to an absolute offset"]
N --> ne["Examples:\n'tomorrow at 3pm'\n'next Monday'\n'March 15th'\n'5pm'"]
I --> ie["Examples:\n'now'\n'in 2 hours'\n'5pm EST'"]
Naive — wall-clock / calendar expressions that carry no timezone. The
parser evaluates them using the reference time's calendar date but does not
embed the reference time's offset into the result. The Ruby binding will need
to decide how to handle timezone attachment (e.g. use the caller-supplied
timezone or leave the value as naive).
Instant — expressions relative to "now" or that name an explicit
timezone. The result is a DateTime<FixedOffset> carrying the resolved
absolute moment, including the offset from the reference time or the named TZ.
Verified by source-level integration tests in src/lib.rs:
// "15/2" (DMY in EN-GB locale) → Naive { value: 2013-02-15T00:00:00, grain: Day }
// "in one hour" → Instant { value: 2013-02-12T05:30:00-02:00, grain: Minute }#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct IntervalEndpoints {
pub from: Option<TimePoint>,
pub to: Option<TimePoint>,
}Used in TimeValue::Interval.values to represent alternative occurrences of
the interval. Both endpoints may be None for open-ended intervals.
Source: src/dimensions/time_grain/mod.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
pub enum Grain {
NoGrain,
Second,
Minute,
Hour,
Day,
Week,
Month,
Quarter,
Year,
}Implements PartialOrd + Ord ordered smallest-to-largest
(NoGrain < Second < Minute < Hour < Day < Week < Month < Quarter < Year).
pub fn as_str(&self) -> &'static str
pub fn from_str(s: &str) -> Grain // unknown strings fall back to Grain::Second
pub fn grain(&self) -> Grain // on TimePoint — returns the grain field
pub fn lower(&self) -> Grain // next finer grain level
pub fn in_seconds(&self, n: i64) -> Option<i64> // n units → seconds (checked mul)
pub fn one_in_seconds_f64(&self) -> f64| Grain | as_str() |
|---|---|
NoGrain |
"no_grain" |
Second |
"second" |
Minute |
"minute" |
Hour |
"hour" |
Day |
"day" |
Week |
"week" |
Month |
"month" |
Quarter |
"quarter" |
Year |
"year" |
Note: NoGrain → "no_grain" (with underscore). from_str("no_grain") round-trips correctly; unrecognised strings fall back to Grain::Second.
NoGrain is used only for now to mark it as a reference instant with no
precision grain. Its in_seconds(n) returns Some(n) (treated as seconds).