mas_i18n/sprintf/
mod.rs

1// Copyright 2024, 2025 New Vector Ltd.
2// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5// Please see LICENSE files in the repository root for full details.
6
7#![allow(unused_macros)]
8
9mod argument;
10mod formatter;
11mod message;
12mod parser;
13
14pub use self::{
15    argument::{Argument, List as ArgumentList},
16    formatter::{FormatError, FormattedMessage, FormattedMessagePart},
17    message::Message,
18};
19
20macro_rules! arg_list_inner {
21    ($var:ident |) => { };
22    ($var:ident | $name:ident = $($arg:expr)*, $($rest:tt)*) => {{
23        $var.push($crate::sprintf::Argument::from((stringify!($name), ::serde_json::json!($($arg)*))));
24        $crate::sprintf::arg_list_inner!($var | $($rest)* );
25    }};
26    ($var:ident | $name:ident = $($arg:expr)*) => {{
27        $var.push($crate::sprintf::Argument::from((stringify!($name), ::serde_json::json!($($arg)*))));
28    }};
29    ($var:ident | $($arg:expr)*, $($rest:tt)*) => {{
30        $var.push($crate::sprintf::Argument::from(::serde_json::json!($($arg)*)));
31        $crate::sprintf::arg_list_inner!($var | $($rest)* );
32    }};
33    ($var:ident | $($arg:expr)*) => {{
34        $var.push($crate::sprintf::Argument::from(::serde_json::json!($($arg)*)));
35    }};
36}
37
38macro_rules! arg_list {
39    ($($args:tt)*) => {{
40        let mut __args = Vec::<$crate::sprintf::Argument>::new();
41        $crate::sprintf::arg_list_inner!(__args | $($args)* );
42        $crate::sprintf::ArgumentList::from_iter(__args)
43    }}
44}
45
46macro_rules! sprintf {
47    ($message:literal) => {{
48        <$crate::sprintf::Message as ::std::str::FromStr>::from_str($message)
49            .map_err($crate::sprintf::Error::from)
50            .and_then(|message| {
51                let __args = $crate::sprintf::ArgumentList::default();
52                message.format(&__args).map_err($crate::sprintf::Error::from)
53            })
54    }};
55
56    ($message:literal, $($args:tt)*) => {{
57        <$crate::sprintf::Message as ::std::str::FromStr>::from_str($message)
58            .map_err($crate::sprintf::Error::from)
59            .and_then(|message| {
60                let __args = $crate::sprintf::arg_list!($($args)*);
61                message.format(&__args).map_err($crate::sprintf::Error::from)
62            })
63    }};
64}
65
66#[allow(unused_imports)]
67pub(crate) use arg_list;
68#[allow(unused_imports)]
69pub(crate) use arg_list_inner;
70#[allow(unused_imports)]
71pub(crate) use sprintf;
72
73#[derive(Debug, thiserror::Error)]
74#[error(transparent)]
75#[allow(dead_code)]
76enum Error {
77    Format(#[from] self::formatter::FormatError),
78    Parse(Box<self::parser::Error>),
79}
80
81impl From<self::parser::Error> for Error {
82    fn from(err: self::parser::Error) -> Self {
83        Self::Parse(Box::new(err))
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use std::f64::consts::PI;
90
91    #[test]
92    fn test_sprintf() {
93        let res = sprintf!("Hello, %(name)s!", name = "world").unwrap();
94        assert_eq!(res, "Hello, world!");
95        assert_eq!("%", sprintf!("%%").unwrap());
96        assert_eq!("10", sprintf!("%b", 2).unwrap());
97        assert_eq!("A", sprintf!("%c", 65).unwrap());
98        assert_eq!("2", sprintf!("%d", 2).unwrap());
99        assert_eq!("2", sprintf!("%i", 2).unwrap());
100        //assert_eq!("2", sprintf!("%d", "2").unwrap()); -- We don't convert on the fly
101        //assert_eq!("2", sprintf!("%i", "2").unwrap()); -- We don't convert on the fly
102        assert_eq!(
103            r#"{"foo":"bar"}"#,
104            sprintf!("%j", serde_json::json!({"foo": "bar"})).unwrap()
105        );
106        assert_eq!(r#"["foo","bar"]"#, sprintf!("%j", ["foo", "bar"]).unwrap());
107        assert_eq!("2e0", sprintf!("%e", 2).unwrap()); // sprintf-js returns 2e+0
108        assert_eq!("2", sprintf!("%u", 2).unwrap());
109        assert_eq!("4294967294", sprintf!("%u", -2).unwrap());
110        assert_eq!("2.2", sprintf!("%f", 2.2).unwrap());
111        assert_eq!("3.141592653589793", sprintf!("%g", PI).unwrap());
112        assert_eq!("10", sprintf!("%o", 8).unwrap());
113        assert_eq!("37777777770", sprintf!("%o", -8).unwrap());
114        assert_eq!("%s", sprintf!("%s", "%s").unwrap());
115        assert_eq!("ff", sprintf!("%x", 255).unwrap());
116        assert_eq!("ffffff01", sprintf!("%x", -255).unwrap());
117        assert_eq!("FF", sprintf!("%X", 255).unwrap());
118        assert_eq!("FFFFFF01", sprintf!("%X", -255).unwrap());
119        assert_eq!(
120            "Polly wants a cracker",
121            sprintf!("%2$s %3$s a %1$s", "cracker", "Polly", "wants").unwrap()
122        );
123        assert_eq!(
124            "Hello world!",
125            sprintf!("Hello %(who)s!", who = "world").unwrap()
126        );
127
128        assert_eq!("true", sprintf!("%t", true).unwrap());
129        assert_eq!("t", sprintf!("%.1t", true).unwrap());
130        // We don't implement truthiness
131        //assert_eq!("true", sprintf!("%t", "true").unwrap());
132        //assert_eq!("true", sprintf!("%t", 1).unwrap());
133        assert_eq!("false", sprintf!("%t", false).unwrap());
134        assert_eq!("f", sprintf!("%.1t", false).unwrap());
135        //assert_eq!("false", sprintf!("%t", "").unwrap());
136        //assert_eq!("false", sprintf!("%t", 0).unwrap());
137
138        assert_eq!("null", sprintf!("%T", serde_json::json!(null)).unwrap());
139        assert_eq!("boolean", sprintf!("%T", true).unwrap());
140        assert_eq!("number", sprintf!("%T", 42).unwrap());
141        assert_eq!("string", sprintf!("%T", "This is a string").unwrap());
142        assert_eq!("array", sprintf!("%T", [1, 2, 3]).unwrap());
143        assert_eq!(
144            "object",
145            sprintf!("%T", serde_json::json!({"foo": "bar"})).unwrap()
146        );
147    }
148
149    #[test]
150    fn test_complex() {
151        // sign
152        assert_eq!("2", sprintf!("%d", 2).unwrap());
153        assert_eq!("-2", sprintf!("%d", -2).unwrap());
154        assert_eq!("+2", sprintf!("%+d", 2).unwrap());
155        assert_eq!("-2", sprintf!("%+d", -2).unwrap());
156        assert_eq!("2", sprintf!("%i", 2).unwrap());
157        assert_eq!("-2", sprintf!("%i", -2).unwrap());
158        assert_eq!("+2", sprintf!("%+i", 2).unwrap());
159        assert_eq!("-2", sprintf!("%+i", -2).unwrap());
160        assert_eq!("2.2", sprintf!("%f", 2.2).unwrap());
161        assert_eq!("-2.2", sprintf!("%f", -2.2).unwrap());
162        assert_eq!("+2.2", sprintf!("%+f", 2.2).unwrap());
163        assert_eq!("-2.2", sprintf!("%+f", -2.2).unwrap());
164        assert_eq!("-2.3", sprintf!("%+.1f", -2.34).unwrap());
165        assert_eq!("-0.0", sprintf!("%+.1f", -0.01).unwrap());
166
167        assert_eq!("3.14159", sprintf!("%.6g", PI).unwrap());
168        assert_eq!("3.14", sprintf!("%.3g", PI).unwrap());
169        assert_eq!("3", sprintf!("%.1g", PI).unwrap());
170        assert_eq!("3e5", sprintf!("%.1g", 300_000.0).unwrap());
171        assert_eq!("300", sprintf!("%.3g", 300).unwrap());
172
173        assert_eq!("-000000123", sprintf!("%+010d", -123).unwrap());
174        assert_eq!("______-123", sprintf!("%+'_10d", -123).unwrap());
175        assert_eq!("-234.34 123.2", sprintf!("%f %f", -234.34, 123.2).unwrap());
176
177        // padding
178        assert_eq!("-0002", sprintf!("%05d", -2).unwrap());
179        assert_eq!("-0002", sprintf!("%05i", -2).unwrap());
180        assert_eq!("    <", sprintf!("%5s", "<").unwrap());
181        assert_eq!("0000<", sprintf!("%05s", "<").unwrap());
182        assert_eq!("____<", sprintf!("%'_5s", "<").unwrap());
183        assert_eq!(">    ", sprintf!("%-5s", ">").unwrap());
184        assert_eq!(">0000", sprintf!("%0-5s", ">").unwrap());
185        assert_eq!(">____", sprintf!("%'_-5s", ">").unwrap());
186        assert_eq!("xxxxxx", sprintf!("%5s", "xxxxxx").unwrap());
187        assert_eq!("1234", sprintf!("%02u", 1234).unwrap());
188        assert_eq!(" -10.235", sprintf!("%8.3f", -10.23456).unwrap());
189        assert_eq!("-12.34 xxx", sprintf!("%f %s", -12.34, "xxx").unwrap());
190        assert_eq!(
191            r#"{
192  "foo": "bar"
193}"#,
194            sprintf!("%2j", serde_json::json!({"foo": "bar"})).unwrap()
195        );
196        assert_eq!(
197            r#"[
198  "foo",
199  "bar"
200]"#,
201            sprintf!("%2j", ["foo", "bar"]).unwrap()
202        );
203
204        // precision
205        assert_eq!("2.3", sprintf!("%.1f", 2.345).unwrap());
206        assert_eq!("xxxxx", sprintf!("%5.5s", "xxxxxx").unwrap());
207        assert_eq!("    x", sprintf!("%5.1s", "xxxxxx").unwrap());
208    }
209}