1use crate::compiler::prelude::*;
2
3fn truncate(value: &Value, limit: Value, suffix: &Value) -> Resolved {
4 let mut value = value.try_bytes_utf8_lossy()?.into_owned();
5 let limit = limit.try_integer()?;
6 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
8 let limit = if limit < 0 { 0 } else { limit as usize };
9 let suffix = suffix.try_bytes_utf8_lossy()?.to_string();
10 let pos = if let Some((pos, chr)) = value.char_indices().take(limit).last() {
11 pos + chr.len_utf8()
14 } else {
15 0
17 };
18 if value.len() > pos {
19 value.truncate(pos);
20 if !suffix.is_empty() {
21 value.push_str(&suffix);
22 }
23 }
24 Ok(value.into())
25}
26
27#[derive(Clone, Copy, Debug)]
28pub struct Truncate;
29
30impl Function for Truncate {
31 fn identifier(&self) -> &'static str {
32 "truncate"
33 }
34
35 fn usage(&self) -> &'static str {
36 "Truncates the `value` string up to the `limit` number of characters."
37 }
38
39 fn category(&self) -> &'static str {
40 Category::String.as_ref()
41 }
42
43 fn return_kind(&self) -> u16 {
44 kind::BYTES
45 }
46
47 fn return_rules(&self) -> &'static [&'static str] {
48 &["The string is returned unchanged its length is less than `limit`."]
49 }
50
51 fn parameters(&self) -> &'static [Parameter] {
52 const PARAMETERS: &[Parameter] = &[
53 Parameter::required("value", kind::BYTES, "The string to truncate."),
54 Parameter::required(
55 "limit",
56 kind::INTEGER,
57 "The number of characters to truncate the string after.",
58 ),
59 Parameter::optional(
60 "suffix",
61 kind::BYTES,
62 indoc! {"
63 A custom suffix to be appended to truncated strings. If a custom `suffix` is
64 provided, the total length of the string will be `limit + <suffix length>`.
65 "},
66 ),
67 ];
68 PARAMETERS
69 }
70
71 fn examples(&self) -> &'static [Example] {
72 &[
73 example! {
74 title: "Truncate a string",
75 source: r#"truncate("A rather long sentence.", limit: 11, suffix: "...")"#,
76 result: Ok("A rather lo..."),
77 },
78 example! {
79 title: "Truncate a string (custom suffix)",
80 source: r#"truncate("A rather long sentence.", limit: 11, suffix: "[TRUNCATED]")"#,
81 result: Ok("A rather lo[TRUNCATED]"),
82 },
83 example! {
84 title: "Truncate",
85 source: r#"truncate("foobar", 3)"#,
86 result: Ok("foo"),
87 },
88 ]
89 }
90
91 fn compile(
92 &self,
93 _state: &TypeState,
94 _ctx: &mut FunctionCompileContext,
95 arguments: ArgumentList,
96 ) -> Compiled {
97 let value = arguments.required("value");
98 let limit = arguments.required("limit");
99 let suffix = arguments.optional("suffix").unwrap_or(expr!(""));
100
101 Ok(TruncateFn {
102 value,
103 limit,
104 suffix,
105 }
106 .as_expr())
107 }
108}
109
110#[derive(Debug, Clone)]
111struct TruncateFn {
112 value: Box<dyn Expression>,
113 limit: Box<dyn Expression>,
114 suffix: Box<dyn Expression>,
115}
116
117impl FunctionExpression for TruncateFn {
118 fn resolve(&self, ctx: &mut Context) -> Resolved {
119 let value = self.value.resolve(ctx)?;
120 let limit = self.limit.resolve(ctx)?;
121 let suffix = self.suffix.resolve(ctx)?;
122
123 truncate(&value, limit, &suffix)
124 }
125
126 fn type_def(&self, _: &state::TypeState) -> TypeDef {
127 TypeDef::bytes().infallible()
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 test_function![
136 truncate => Truncate;
137
138 empty {
139 args: func_args![value: "Super",
140 limit: 0,
141 ],
142 want: Ok(""),
143 tdef: TypeDef::bytes().infallible(),
144 }
145
146 ellipsis {
147 args: func_args![value: "Super",
148 limit: 0,
149 suffix: "..."
150 ],
151 want: Ok("..."),
152 tdef: TypeDef::bytes().infallible(),
153 }
154
155 complete {
156 args: func_args![value: "Super",
157 limit: 10
158 ],
159 want: Ok("Super"),
160 tdef: TypeDef::bytes().infallible(),
161 }
162
163 exact {
164 args: func_args![value: "Super",
165 limit: 5,
166 suffix: "."
167 ],
168 want: Ok("Super"),
169 tdef: TypeDef::bytes().infallible(),
170 }
171
172 big {
173 args: func_args![value: "Supercalifragilisticexpialidocious",
174 limit: 5
175 ],
176 want: Ok("Super"),
177 tdef: TypeDef::bytes().infallible(),
178 }
179
180 big_ellipsis {
181 args: func_args![value: "Supercalifragilisticexpialidocious",
182 limit: 5,
183 suffix: "..."
184 ],
185 want: Ok("Super..."),
186 tdef: TypeDef::bytes().infallible(),
187 }
188
189 unicode {
190 args: func_args![value: "♔♕♖♗♘♙♚♛♜♝♞♟",
191 limit: 6,
192 suffix: "..."
193 ],
194 want: Ok("♔♕♖♗♘♙..."),
195 tdef: TypeDef::bytes().infallible(),
196 }
197
198 alternative_suffix {
199 args: func_args![value: "Super",
200 limit: 1,
201 suffix: "[TRUNCATED]"
202 ],
203 want: Ok("S[TRUNCATED]"),
204 tdef: TypeDef::bytes().infallible(),
205 }
206 ];
207}