1use std::collections::BTreeMap;
4
5use serde_json::{Map, Value};
6
7use crate::question::{Question, Questions};
8
9#[derive(Clone, Debug, PartialEq)]
15pub struct NoulAnswer {
16 pub noul: f64,
17}
18
19impl NoulAnswer {
20 pub fn yes(&self) -> bool {
22 self.noul > 0.5
23 }
24
25 pub fn yes_above(&self, threshold: f64) -> bool {
27 self.noul > threshold
28 }
29}
30
31#[derive(Clone, Debug, PartialEq)]
38pub struct ChoiceAnswer {
39 pub choice: String,
40 pub probabilities: BTreeMap<String, f64>,
41 pub confidence: f64,
42}
43
44impl ChoiceAnswer {
45 pub fn is(&self, option: &str) -> bool {
47 self.choice == option
48 }
49
50 pub fn probability(&self, option: &str) -> f64 {
52 self.probabilities.get(option).copied().unwrap_or(0.0)
53 }
54
55 pub fn ranked(&self) -> Vec<(&str, f64)> {
58 let mut ranked: Vec<(&str, f64)> = self
59 .probabilities
60 .iter()
61 .map(|(option, &p)| (option.as_str(), p))
62 .collect();
63 ranked.sort_by(|a, b| b.1.total_cmp(&a.1));
64 ranked
65 }
66}
67
68#[derive(Clone, Debug, PartialEq)]
79pub struct ScoreAnswer {
80 pub score: f64,
81 pub legend: BTreeMap<usize, Value>,
82 pub probabilities: BTreeMap<usize, f64>,
83 pub confidence: f64,
84}
85
86impl ScoreAnswer {
87 pub fn normalized(&self) -> f64 {
89 match self.legend.len() {
90 0 | 1 => 0.0,
91 n => self.score / (n - 1) as f64,
92 }
93 }
94
95 pub fn level(&self) -> usize {
97 self.probabilities
98 .iter()
99 .max_by(|a, b| a.1.total_cmp(b.1).then(b.0.cmp(a.0)))
100 .map_or(0, |(&level, _)| level)
101 }
102}
103
104#[derive(Clone, Debug, PartialEq)]
106#[non_exhaustive]
107pub enum Answer {
108 Noul(NoulAnswer),
109 Choice(ChoiceAnswer),
110 Score(ScoreAnswer),
111 Raw(Value),
113}
114
115impl Answer {
116 pub fn as_noul(&self) -> Option<&NoulAnswer> {
117 match self {
118 Answer::Noul(a) => Some(a),
119 _ => None,
120 }
121 }
122
123 pub fn as_choice(&self) -> Option<&ChoiceAnswer> {
124 match self {
125 Answer::Choice(a) => Some(a),
126 _ => None,
127 }
128 }
129
130 pub fn as_score(&self) -> Option<&ScoreAnswer> {
131 match self {
132 Answer::Score(a) => Some(a),
133 _ => None,
134 }
135 }
136
137 pub fn as_raw(&self) -> Option<&Value> {
138 match self {
139 Answer::Raw(a) => Some(a),
140 _ => None,
141 }
142 }
143}
144
145pub(crate) fn decode_all(
149 questions: &Questions,
150 answers: Option<&Value>,
151) -> Result<BTreeMap<String, Answer>, String> {
152 let answers = answers
153 .and_then(Value::as_object)
154 .ok_or_else(|| "answers".to_string())?;
155 questions
156 .iter()
157 .map(|(id, question)| {
158 let raw = answers.get(id).ok_or_else(|| format!("answers.{id}"))?;
159 let answer = decode(question, raw).map_err(|field| format!("answers.{id}.{field}"))?;
160 Ok((id.to_string(), answer))
161 })
162 .collect()
163}
164
165fn decode(question: &Question, raw: &Value) -> Result<Answer, String> {
166 if let Question::Raw(_) = question {
167 return Ok(Answer::Raw(raw.clone()));
168 }
169 let raw = raw.as_object().ok_or_else(|| "type".to_string())?;
170 match question {
171 Question::Noul(_) => {
172 expect_type(raw, "noul")?;
173 Ok(Answer::Noul(NoulAnswer {
174 noul: number(raw, "noul")?,
175 }))
176 }
177 Question::Choice(_) => {
178 expect_type(raw, "choice")?;
179 let choice = raw
180 .get("choice")
181 .and_then(Value::as_str)
182 .ok_or_else(|| "choice".to_string())?;
183 let probabilities = probabilities(raw)?
184 .into_iter()
185 .map(|(option, p)| (option.clone(), p))
186 .collect();
187 Ok(Answer::Choice(ChoiceAnswer {
188 choice: choice.to_string(),
189 probabilities,
190 confidence: number(raw, "confidence")?,
191 }))
192 }
193 Question::Score(_) => {
194 expect_type(raw, "score")?;
195 let score = number(raw, "score")?;
196 let legend = raw
197 .get("legend")
198 .and_then(Value::as_object)
199 .ok_or_else(|| "legend".to_string())?;
200 let legend = index_keys(legend, "legend", |v| Some(v.clone()))?;
201 let probabilities = raw
202 .get("probabilities")
203 .and_then(Value::as_object)
204 .ok_or_else(|| "probabilities".to_string())?;
205 let probabilities = index_keys(probabilities, "probabilities", Value::as_f64)?;
206 Ok(Answer::Score(ScoreAnswer {
207 score,
208 legend,
209 probabilities,
210 confidence: number(raw, "confidence")?,
211 }))
212 }
213 Question::Raw(_) => unreachable!("handled above"),
214 }
215}
216
217fn expect_type(raw: &Map<String, Value>, expected: &str) -> Result<(), String> {
218 match raw.get("type").and_then(Value::as_str) {
219 Some(t) if t == expected => Ok(()),
220 _ => Err("type".into()),
221 }
222}
223
224fn number(raw: &Map<String, Value>, field: &str) -> Result<f64, String> {
225 raw.get(field)
226 .and_then(Value::as_f64)
227 .ok_or_else(|| field.to_string())
228}
229
230fn probabilities(raw: &Map<String, Value>) -> Result<Vec<(&String, f64)>, String> {
231 raw.get("probabilities")
232 .and_then(Value::as_object)
233 .ok_or_else(|| "probabilities".to_string())?
234 .iter()
235 .map(|(option, p)| {
236 p.as_f64()
237 .map(|p| (option, p))
238 .ok_or_else(|| format!("probabilities.{option}"))
239 })
240 .collect()
241}
242
243fn index_keys<T>(
245 map: &Map<String, Value>,
246 field: &str,
247 value: impl Fn(&Value) -> Option<T>,
248) -> Result<BTreeMap<usize, T>, String> {
249 map.iter()
250 .map(|(key, v)| {
251 let bad = || format!("{field}.{key}");
252 let index = key.parse::<usize>().map_err(|_| bad())?;
253 Ok((index, value(v).ok_or_else(bad)?))
254 })
255 .collect()
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261 use crate::{Choice, Noul, Score};
262 use serde_json::json;
263
264 fn questions() -> Questions {
265 Questions::new()
266 .ask("billing", Noul::new("Billing?"))
267 .ask(
268 "tone",
269 Choice::new("Tone?").options(["calm", "frustrated", "angry"]),
270 )
271 .ask(
272 "urgency",
273 Score::new("Urgent?", ["Can wait", "This week", "Today"]),
274 )
275 .ask("future", Question::Raw(json!({"type": "future"})))
276 }
277
278 fn answers() -> Value {
279 json!({
280 "billing": {"type": "noul", "noul": 0.99},
281 "tone": {"type": "choice", "choice": "frustrated",
282 "probabilities": {"calm": 0.0, "frustrated": 0.86, "angry": 0.14},
283 "confidence": 0.78},
284 "urgency": {"type": "score", "score": 1.99,
285 "legend": {"0": "Can wait", "1": "This week", "2": "Today"},
286 "probabilities": {"0": 0.0, "1": 0.01, "2": 0.99},
287 "confidence": 0.99},
288 "future": {"type": "future", "whatever": [1, 2]}
289 })
290 }
291
292 #[test]
293 fn decodes_every_type() {
294 let decoded = decode_all(&questions(), Some(&answers())).unwrap();
295
296 let billing = decoded["billing"].as_noul().unwrap();
297 assert!(billing.yes());
298 assert!(!billing.yes_above(0.995));
299
300 let tone = decoded["tone"].as_choice().unwrap();
301 assert!(tone.is("frustrated"));
302 assert_eq!(tone.probability("angry"), 0.14);
303 assert_eq!(tone.probability("missing"), 0.0);
304 assert_eq!(
305 tone.ranked(),
306 vec![("frustrated", 0.86), ("angry", 0.14), ("calm", 0.0)]
307 );
308
309 let urgency = decoded["urgency"].as_score().unwrap();
310 assert_eq!(urgency.level(), 2);
311 assert_eq!(urgency.legend[&1], json!("This week"));
312 assert!((urgency.normalized() - 0.995).abs() < 1e-9);
313
314 assert_eq!(
315 decoded["future"].as_raw().unwrap(),
316 &json!({"type": "future", "whatever": [1, 2]})
317 );
318 }
319
320 #[test]
321 fn names_the_first_bad_field() {
322 let broken = |path: &str, value: Value| {
323 let mut answers = answers();
324 let (id, field) = path.split_once('.').unwrap();
325 answers[id][field] = value;
326 decode_all(&questions(), Some(&answers)).unwrap_err()
327 };
328
329 assert_eq!(
330 broken("billing.noul", json!("high")),
331 "answers.billing.noul"
332 );
333 assert_eq!(broken("tone.type", json!("noul")), "answers.tone.type");
334 assert_eq!(
335 broken("tone.confidence", Value::Null),
336 "answers.tone.confidence"
337 );
338 assert_eq!(
339 broken("tone.probabilities", json!({"calm": "x"})),
340 "answers.tone.probabilities.calm"
341 );
342 assert_eq!(
343 broken("urgency.legend", json!({"one": "x"})),
344 "answers.urgency.legend.one"
345 );
346
347 let mut missing = answers();
348 missing.as_object_mut().unwrap().remove("tone");
349 assert_eq!(
350 decode_all(&questions(), Some(&missing)).unwrap_err(),
351 "answers.tone"
352 );
353 assert_eq!(decode_all(&questions(), None).unwrap_err(), "answers");
354 }
355
356 #[test]
357 fn level_ties_go_to_the_lower_level() {
358 let score = ScoreAnswer {
359 score: 0.5,
360 legend: BTreeMap::from([(0, json!("a")), (1, json!("b"))]),
361 probabilities: BTreeMap::from([(0, 0.5), (1, 0.5)]),
362 confidence: 0.0,
363 };
364 assert_eq!(score.level(), 0);
365 }
366}