Skip to main content

ferroui_base/data/converters/
string_format_value_converter.rs

1use super::composite_format::format_values_with;
2use super::IValueConverter;
3use crate::data::core::ValueType;
4use crate::data::BindingError;
5use crate::utilities::CultureInfo;
6use crate::BoxedValue;
7use std::rc::Rc;
8
9/// A value converter which formats the value with a composite format string
10/// (see [`composite_format`](super::composite_format)).
11pub struct StringFormatValueConverter {
12    format: String,
13    inner: Option<Rc<dyn IValueConverter>>,
14}
15
16impl StringFormatValueConverter {
17    /// Creates a string format value converter.
18    ///
19    /// `format` is the format string; `inner` is an optional inner converter
20    /// to be called before the format takes place.
21    pub fn new(format: impl Into<String>, inner: Option<Rc<dyn IValueConverter>>) -> Self {
22        Self { format: format.into(), inner }
23    }
24
25    /// Gets an inner value converter which will be called before the string
26    /// format takes place.
27    pub fn inner(&self) -> Option<&Rc<dyn IValueConverter>> {
28        self.inner.as_ref()
29    }
30
31    /// Gets the format string.
32    pub fn format(&self) -> &str {
33        &self.format
34    }
35}
36
37impl IValueConverter for StringFormatValueConverter {
38    fn convert(
39        &self,
40        value: Option<&BoxedValue>,
41        target_type: ValueType,
42        parameter: Option<&BoxedValue>,
43        culture: &CultureInfo,
44    ) -> Result<Option<BoxedValue>, BindingError> {
45        let converted = match &self.inner {
46            Some(inner) => inner.convert(value, target_type, parameter, culture)?,
47            None => None,
48        };
49        let value = converted.or_else(|| value.cloned());
50
51        let result = if self.format.contains('{') {
52            format_values_with(&self.format, &[value], culture)
53        } else {
54            format_values_with(&format!("{{0:{}}}", self.format), &[value], culture)
55        };
56        match result {
57            Ok(text) => Ok(Some(Rc::new(text))),
58            Err(error) => Err(BindingError::new(error)),
59        }
60    }
61
62    fn convert_back(
63        &self,
64        _value: Option<&BoxedValue>,
65        _target_type: ValueType,
66        _parameter: Option<&BoxedValue>,
67        _culture: &CultureInfo,
68    ) -> Result<Option<BoxedValue>, BindingError> {
69        Err(BindingError::message("Two way bindings are not supported with a string format"))
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    //! Upstream has no dedicated tests for this type; these cover the port.
76
77    use super::*;
78    use crate::data::converters::composite_format::FormatError;
79    use crate::data::converters::FuncValueConverter;
80
81    fn boxed<T: PartialEq + 'static>(value: T) -> BoxedValue {
82        Rc::new(value)
83    }
84
85    fn convert(target: &StringFormatValueConverter, value: Option<&BoxedValue>) -> String {
86        let result = target.convert(value, ValueType::of::<String>(), None, &crate::utilities::CultureInfo::invariant_culture()).expect("no error").expect("not null");
87        result.downcast_ref::<String>().cloned().expect("a string")
88    }
89
90    #[test]
91    fn exposes_format_and_inner() {
92        let inner: Rc<dyn IValueConverter> = Rc::new(FuncValueConverter::<i32, i32>::new(|x| x));
93        let target = StringFormatValueConverter::new("{0}", Some(inner.clone()));
94
95        assert_eq!(target.format(), "{0}");
96        assert!(target.inner().is_some_and(|i| Rc::ptr_eq(i, &inner)));
97        assert!(StringFormatValueConverter::new("{0}", None).inner().is_none());
98    }
99
100    #[test]
101    fn formats_value_with_composite_format() {
102        let target = StringFormatValueConverter::new("Hello {0}!", None);
103
104        assert_eq!(convert(&target, Some(&boxed(String::from("world")))), "Hello world!");
105        assert_eq!(convert(&target, Some(&boxed(5i32))), "Hello 5!");
106    }
107
108    #[test]
109    fn format_without_braces_is_the_format_of_the_value() {
110        let target = StringFormatValueConverter::new("0.00", None);
111
112        assert_eq!(convert(&target, Some(&boxed(1.5f64))), "1.50");
113        assert_eq!(convert(&StringFormatValueConverter::new("F1", None), Some(&boxed(2.25f64))), "2.2");
114        assert_eq!(convert(&StringFormatValueConverter::new("X4", None), Some(&boxed(255i32))), "00FF");
115    }
116
117    #[test]
118    fn null_value_formats_as_empty_text() {
119        let target = StringFormatValueConverter::new("Converted: {0}", None);
120
121        assert_eq!(convert(&target, None), "Converted: ");
122    }
123
124    #[test]
125    fn inner_converter_is_called_before_formatting() {
126        let inner: Rc<dyn IValueConverter> = Rc::new(FuncValueConverter::<i32, i32>::new(|x| x * 2));
127        let target = StringFormatValueConverter::new("{0:D3}", Some(inner));
128
129        assert_eq!(convert(&target, Some(&boxed(21i32))), "042");
130    }
131
132    #[test]
133    fn value_is_used_if_inner_converter_returns_null() {
134        let inner: Rc<dyn IValueConverter> = Rc::new(FuncValueConverter::<i32, Option<i32>>::new(|_| None));
135        let target = StringFormatValueConverter::new("{0}", Some(inner));
136
137        assert_eq!(convert(&target, Some(&boxed(21i32))), "21");
138    }
139
140    #[test]
141    fn error_of_inner_converter_is_returned() {
142        struct Failing;
143        impl IValueConverter for Failing {
144            fn convert(
145                &self,
146                _value: Option<&BoxedValue>,
147                _target_type: ValueType,
148                _parameter: Option<&BoxedValue>,
149                _culture: &CultureInfo,
150            ) -> Result<Option<BoxedValue>, BindingError> {
151                Err(BindingError::message("inner failed"))
152            }
153
154            fn convert_back(
155                &self,
156                _value: Option<&BoxedValue>,
157                _target_type: ValueType,
158                _parameter: Option<&BoxedValue>,
159                _culture: &CultureInfo,
160            ) -> Result<Option<BoxedValue>, BindingError> {
161                Err(BindingError::message("inner failed"))
162            }
163        }
164        let target = StringFormatValueConverter::new("{0}", Some(Rc::new(Failing)));
165
166        let result = target.convert(Some(&boxed(1i32)), ValueType::of::<String>(), None, &crate::utilities::CultureInfo::invariant_culture());
167
168        assert_eq!(result.expect_err("an error").to_string(), "inner failed");
169    }
170
171    #[test]
172    fn invalid_format_is_an_error() {
173        let target = StringFormatValueConverter::new("{1}", None);
174
175        let error = target.convert(Some(&boxed(1i32)), ValueType::of::<String>(), None, &crate::utilities::CultureInfo::invariant_culture()).expect_err("an error");
176
177        assert_eq!(error.inner().downcast_ref::<FormatError>(), Some(&FormatError::IndexOutOfRange));
178    }
179
180    #[test]
181    fn convert_back_is_not_supported() {
182        let target = StringFormatValueConverter::new("{0}", None);
183
184        let result = target.convert_back(Some(&boxed(String::from("1"))), ValueType::of::<i32>(), None, &crate::utilities::CultureInfo::invariant_culture());
185
186        assert_eq!(
187            result.expect_err("an error").to_string(),
188            "Two way bindings are not supported with a string format"
189        );
190    }
191}