1use crate::compiler::prelude::*;
2use std::collections::{BTreeMap, HashMap};
3
4fn tally(value: Value) -> Resolved {
5 let value = value.try_array()?;
6 #[allow(clippy::mutable_key_type)] let mut map: HashMap<Bytes, usize> = HashMap::new();
8 for value in value {
9 if let Value::Bytes(value) = value {
10 *map.entry(value).or_insert(0) += 1;
11 } else {
12 return Err(format!("all values must be strings, found: {value:?}").into());
13 }
14 }
15 let map: BTreeMap<_, _> = map
16 .into_iter()
17 .map(|(k, v)| {
18 (
19 String::from_utf8_lossy(&k).into_owned().into(),
20 Value::from(v),
21 )
22 })
23 .collect();
24 Ok(map.into())
25}
26
27#[derive(Clone, Copy, Debug)]
28pub struct Tally;
29
30impl Function for Tally {
31 fn identifier(&self) -> &'static str {
32 "tally"
33 }
34
35 fn usage(&self) -> &'static str {
36 "Counts the occurrences of each string value in the provided array and returns an object with the counts."
37 }
38
39 fn category(&self) -> &'static str {
40 Category::Enumerate.as_ref()
41 }
42
43 fn return_kind(&self) -> u16 {
44 kind::OBJECT
45 }
46
47 fn examples(&self) -> &'static [Example] {
48 &[example! {
49 title: "tally",
50 source: r#"tally!(["foo", "bar", "foo", "baz"])"#,
51 result: Ok(r#"{"foo": 2, "bar": 1, "baz": 1}"#),
52 }]
53 }
54
55 fn compile(
56 &self,
57 _state: &state::TypeState,
58 _ctx: &mut FunctionCompileContext,
59 arguments: ArgumentList,
60 ) -> Compiled {
61 let value = arguments.required("value");
62
63 Ok(TallyFn { value }.as_expr())
64 }
65
66 fn parameters(&self) -> &'static [Parameter] {
67 const PARAMETERS: &[Parameter] = &[Parameter::required(
68 "value",
69 kind::ARRAY,
70 "The array of strings to count occurrences for.",
71 )];
72 PARAMETERS
73 }
74}
75
76#[derive(Debug, Clone)]
77pub(crate) struct TallyFn {
78 value: Box<dyn Expression>,
79}
80
81impl FunctionExpression for TallyFn {
82 fn resolve(&self, ctx: &mut Context) -> Resolved {
83 let value = self.value.resolve(ctx)?;
84 tally(value)
85 }
86
87 fn type_def(&self, _: &state::TypeState) -> TypeDef {
88 TypeDef::object(Collection::from_unknown(Kind::integer())).fallible()
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use crate::value;
96
97 test_function![
98 tally => Tally;
99
100 default {
101 args: func_args![
102 value: value!(["bar", "foo", "baz", "foo"]),
103 ],
104 want: Ok(value!({"bar": 1, "foo": 2, "baz": 1})),
105 tdef: TypeDef::object(Collection::from_unknown(Kind::integer())).fallible(),
106 }
107
108 non_string_values {
109 args: func_args![
110 value: value!(["foo", [1,2,3], "123abc", 1, true, [1,2,3], "foo", true, 1]),
111 ],
112 want: Err("all values must be strings, found: Array([Integer(1), Integer(2), Integer(3)])"),
113 tdef: TypeDef::object(Collection::from_unknown(Kind::integer())).fallible(),
114 }
115 ];
116}