1use crate::condition::{Condition, Constraint, Interval};
6use crate::diagnostic::Span;
7use crate::text;
8use std::fmt;
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
11pub struct VarId(pub u32);
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub struct EventId(pub u32);
15
16pub const MAX_VALUES: usize = 64;
18
19#[derive(Clone, Debug, PartialEq)]
20pub enum VarKind {
21 Enum(Vec<String>),
23 Number { min: f64, max: f64, unit: Option<String> },
25}
26
27#[derive(Clone, Debug)]
29pub struct Variable {
30 pub id: VarId,
31 pub subject: String,
33 pub key: String,
35 pub kind: VarKind,
36 pub verb: String,
39 pub span: Span,
40 pub set: Option<String>,
43 pub shows: Vec<(Option<String>, String)>,
47}
48
49#[derive(Clone, Debug)]
51pub struct Event {
52 pub id: EventId,
53 pub text: String,
54 pub key: String,
55 pub span: Span,
56 pub say: Option<String>,
60}
61
62#[derive(Clone, Debug, PartialEq)]
64pub enum Value {
65 Enum(String),
66 Number(f64),
67}
68
69impl fmt::Display for Value {
70 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
71 match self {
72 Value::Enum(s) => f.write_str(s),
73 Value::Number(v) => f.write_str(&text::number(*v)),
74 }
75 }
76}
77
78impl From<&str> for Value {
79 fn from(s: &str) -> Self {
80 Value::Enum(s.to_string())
81 }
82}
83
84impl From<String> for Value {
85 fn from(s: String) -> Self {
86 Value::Enum(s)
87 }
88}
89
90impl From<f64> for Value {
91 fn from(v: f64) -> Self {
92 Value::Number(v)
93 }
94}
95
96impl From<f32> for Value {
97 fn from(v: f32) -> Self {
98 Value::Number(v as f64)
99 }
100}
101
102impl Value {
103 pub fn as_str(&self) -> Option<&str> {
104 match self {
105 Value::Enum(s) => Some(s),
106 Value::Number(_) => None,
107 }
108 }
109
110 pub fn as_f64(&self) -> Option<f64> {
111 match self {
112 Value::Number(v) => Some(*v),
113 Value::Enum(_) => None,
114 }
115 }
116}
117
118impl Variable {
119 pub fn shown_shift(&self, shift: crate::spec::Shift) -> Option<String> {
123 let keys: Vec<String> = match shift {
124 crate::spec::Shift::Same => vec!["stays the same".into()],
125 crate::spec::Shift::By(d) => {
126 let way = if d > 0.0 { "up" } else { "down" };
127 let n = text::number(d.abs());
128 let unit = match &self.kind {
129 VarKind::Number { unit: Some(u), .. } => format!(" {u}"),
130 _ => String::new(),
131 };
132 vec![format!("goes {way} by {n}"), format!("goes {way} by {n}{unit}")]
133 }
134 };
135 self.shows
136 .iter()
137 .find(|(v, _)| v.as_ref().is_some_and(|v| keys.iter().any(|k| k.eq_ignore_ascii_case(v))))
138 .map(|(_, t)| t.clone())
139 }
140
141 pub fn shown(&self, c: &Constraint) -> Option<String> {
142 let said = self.predicate(c);
144 let said = ["is ", "are ", "has ", "have "].iter().find_map(|v| said.strip_prefix(v)).unwrap_or(&said);
145 if let Some((_, t)) = self.shows.iter().find(|(v, _)| v.as_ref().is_some_and(|v| v.eq_ignore_ascii_case(said)))
146 {
147 return Some(t.clone());
148 }
149 let value = match (&self.kind, c) {
150 (VarKind::Enum(values), Constraint::OneOf(mask)) if mask.count_ones() == 1 => {
151 values[mask.trailing_zeros() as usize].clone()
152 }
153 (VarKind::Number { .. }, Constraint::Range(r)) if r.lo == r.hi && !r.lo_open && !r.hi_open => {
154 text::number(r.lo)
155 }
156 _ => return None,
157 };
158 let own = self.shows.iter().find(|(v, _)| v.as_ref().is_some_and(|v| v.eq_ignore_ascii_case(&value)));
159 let any = || self.shows.iter().find(|(v, _)| v.is_none());
160 own.or_else(any).map(|(_, t)| t.replace("{value}", &value))
161 }
162
163 pub fn full(&self) -> Constraint {
165 match &self.kind {
166 VarKind::Enum(values) => Constraint::OneOf(mask_all(values.len())),
167 VarKind::Number { min, max, .. } => Constraint::Range(Interval::closed(*min, *max)),
168 }
169 }
170
171 pub fn value_index(&self, name: &str) -> Option<usize> {
172 let VarKind::Enum(values) = &self.kind else { return None };
173 let k = text::normalize(name);
174 values.iter().position(|v| text::normalize(v) == k)
175 }
176
177 pub fn witness(&self, c: &Constraint) -> Option<Value> {
180 match (&self.kind, c) {
181 (VarKind::Enum(values), Constraint::OneOf(mask)) => {
182 (0..values.len()).find(|i| mask & (1 << i) != 0).map(|i| Value::Enum(values[i].clone()))
183 }
184 (VarKind::Number { .. }, Constraint::Range(r)) if !r.is_empty() => Some(Value::Number(r.midpoint())),
185 _ => None,
186 }
187 }
188
189 pub fn admits(&self, c: &Constraint, v: &Value) -> bool {
192 match (&self.kind, c, v) {
193 (_, Constraint::OneOf(mask), Value::Enum(name)) => {
194 self.value_index(name).is_some_and(|i| mask & (1 << i) != 0)
195 }
196 (VarKind::Number { min, max, .. }, Constraint::Range(r), Value::Number(x)) => {
197 let eps = (max - min) * 1e-6;
198 !r.is_empty() && (r.contains(*x) || (r.lo - eps..=r.hi + eps).contains(x))
199 }
200 _ => false,
201 }
202 }
203
204 pub fn describe(&self, c: &Constraint) -> String {
206 format!("{} {}", self.subject, self.predicate(c))
207 }
208
209 pub fn predicate(&self, c: &Constraint) -> String {
211 let p = self.singular_predicate(c);
212 let Some(rest) = p.strip_prefix("is ") else { return p };
213 match self.verb.as_str() {
214 "has" | "have" => {
215 let rest = rest.replacen("above ", "more than ", 1).replacen("below ", "fewer than ", 1);
216 match rest.strip_prefix("not ") {
217 Some(r) => format!("{} {r}", if self.verb == "has" { "doesn't have" } else { "don't have" }),
218 None => format!("{} {rest}", self.verb),
219 }
220 }
221 "are" => format!("are {rest}"),
222 _ => p,
223 }
224 }
225
226 fn singular_predicate(&self, c: &Constraint) -> String {
227 match (&self.kind, c) {
228 (VarKind::Enum(values), Constraint::OneOf(mask)) => {
229 let named = |m: u64| -> Vec<String> {
230 (0..values.len()).filter(|i| m & (1 << i) != 0).map(|i| values[i].clone()).collect()
231 };
232 let yes = named(*mask);
233 let no = named(mask_all(values.len()) & !mask);
234 if yes.is_empty() {
235 "is impossible".into()
236 } else if yes.len() > 1 && no.len() == 1 {
237 format!("is not {}", no[0])
238 } else {
239 format!("is {}", yes.join(" or "))
240 }
241 }
242 (VarKind::Number { min, max, unit }, Constraint::Range(r)) => {
243 let u = |v: f64| match unit {
244 Some(u) if v == 1.0 && u.len() > 1 && u.ends_with('s') => format!("1 {}", &u[..u.len() - 1]),
246 Some(u) => format!("{} {u}", text::number(v)),
247 None => text::number(v),
248 };
249 let open_lo = r.lo <= *min && !r.lo_open;
250 let open_hi = r.hi >= *max && !r.hi_open;
251 if r.is_empty() {
252 "is impossible".into()
253 } else if r.lo == r.hi {
254 format!("is {}", u(r.lo))
255 } else if open_lo && open_hi {
256 "is anything".into()
257 } else if open_lo {
258 format!("is {} {}", if r.hi_open { "below" } else { "at most" }, u(r.hi))
259 } else if open_hi {
260 format!("is {} {}", if r.lo_open { "above" } else { "at least" }, u(r.lo))
261 } else if !r.lo_open && !r.hi_open {
262 format!("is between {} and {}", text::number(r.lo), u(r.hi))
263 } else {
264 let lo = if r.lo_open { "above" } else { "at least" };
265 let hi = if r.hi_open { "below" } else { "at most" };
266 format!("is {lo} {} and {hi} {}", u(r.lo), u(r.hi))
267 }
268 }
269 _ => "is (mismatched constraint)".into(),
270 }
271 }
272}
273
274pub(crate) fn mask_all(n: usize) -> u64 {
275 if n >= 64 { u64::MAX } else { (1u64 << n) - 1 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 #[test]
283 fn numbers_are_compared_with_a_little_tolerance() {
284 let thr = Variable {
285 id: VarId(0),
286 subject: "the dial".into(),
287 key: "dial".into(),
288 kind: VarKind::Number { min: 0.0, max: 100.0, unit: Some("%".into()) },
289 verb: "is".into(),
290 span: Span::new("t", 1),
291 set: None,
292 shows: vec![],
293 };
294 let eighty = Constraint::Range(Interval::point(80.0));
295 assert!(thr.admits(&eighty, &Value::Number(0.8f32 as f64 * 100.0)));
296 assert!(!thr.admits(&eighty, &Value::Number(80.01)));
297 }
298}
299
300#[derive(Clone, Debug, Default)]
301pub struct Vocabulary {
302 pub variables: Vec<Variable>,
303 pub events: Vec<Event>,
304 pub phrases: Vec<crate::spec::Phrase>,
305}
306
307impl Vocabulary {
308 pub fn var(&self, id: VarId) -> &Variable {
309 &self.variables[id.0 as usize]
310 }
311
312 pub fn event(&self, id: EventId) -> &Event {
313 &self.events[id.0 as usize]
314 }
315
316 pub fn find_var(&self, subject: &str) -> Option<&Variable> {
317 let k = text::key(subject);
318 self.variables.iter().find(|v| v.key == k)
319 }
320
321 pub fn phrase(&self, id: crate::spec::PhraseId) -> &crate::spec::Phrase {
322 &self.phrases[id.0 as usize]
323 }
324
325 pub fn find_event(&self, phrase: &str) -> Option<&Event> {
326 let k = text::key(phrase);
327 self.events.iter().find(|e| e.key == k)
328 }
329
330 pub fn facts(&self, c: &Condition) -> Vec<String> {
332 c.iter().map(|(v, k)| self.var(v).describe(k)).collect()
333 }
334
335 pub fn describe(&self, c: &Condition) -> String {
337 let facts = self.facts(c);
338 if facts.is_empty() { "anything".into() } else { text::join_and(&facts) }
339 }
340}