Skip to main content

ferroui_base/data/converters/
string_format_multi_value_converter.rs

1use super::composite_format::format_values_with;
2use super::IMultiValueConverter;
3use crate::data::core::ValueType;
4use crate::data::BindingError;
5use crate::utilities::CultureInfo;
6use crate::BoxedValue;
7use std::rc::Rc;
8
9/// A multi-value converter which formats the values with a composite format
10/// string (see [`composite_format`](super::composite_format)).
11pub struct StringFormatMultiValueConverter {
12    format: String,
13    inner: Option<Rc<dyn IMultiValueConverter>>,
14}
15
16impl StringFormatMultiValueConverter {
17    /// Creates a string format multi-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 IMultiValueConverter>>) -> 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 IMultiValueConverter>> {
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 IMultiValueConverter for StringFormatMultiValueConverter {
38    fn convert(
39        &self,
40        values: &[Option<BoxedValue>],
41        target_type: ValueType,
42        parameter: Option<&BoxedValue>,
43        culture: &CultureInfo,
44    ) -> Result<Option<BoxedValue>, BindingError> {
45        let result = match &self.inner {
46            None => format_values_with(&self.format, values, culture),
47            Some(inner) => {
48                format_values_with(&self.format, &[inner.convert(values, target_type, parameter, culture)?], culture)
49            }
50        };
51        match result {
52            Ok(text) => Ok(Some(Rc::new(text))),
53            Err(error) => Err(BindingError::new(error)),
54        }
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    //! Upstream tests this type through multi-bindings only; these cover the
61    //! port.
62
63    use super::*;
64    use crate::data::converters::composite_format::FormatError;
65    use crate::data::converters::FuncMultiValueConverter;
66
67    fn boxed<T: PartialEq + 'static>(value: T) -> Option<BoxedValue> {
68        Some(Rc::new(value))
69    }
70
71    fn convert(target: &StringFormatMultiValueConverter, values: &[Option<BoxedValue>]) -> String {
72        let result = target.convert(values, ValueType::of::<String>(), None, &crate::utilities::CultureInfo::invariant_culture()).expect("no error").expect("not null");
73        result.downcast_ref::<String>().cloned().expect("a string")
74    }
75
76    #[test]
77    fn exposes_format_and_inner() {
78        let inner: Rc<dyn IMultiValueConverter> = Rc::new(FuncMultiValueConverter::<i32, i32>::new(|x| x[0]));
79        let target = StringFormatMultiValueConverter::new("{0}", Some(inner.clone()));
80
81        assert_eq!(target.format(), "{0}");
82        assert!(target.inner().is_some_and(|i| Rc::ptr_eq(i, &inner)));
83        assert!(StringFormatMultiValueConverter::new("{0}", None).inner().is_none());
84    }
85
86    #[test]
87    fn formats_values_with_composite_format() {
88        let target = StringFormatMultiValueConverter::new("{0}+{1}={2:F1}", None);
89
90        assert_eq!(convert(&target, &[boxed(1i32), boxed(String::from("b")), boxed(2.5f64)]), "1+b=2.5");
91    }
92
93    /// The format part of the upstream multi-binding test
94    /// `Should_Update_When_Null_Value_In_Bindings_With_StringFormat`.
95    #[test]
96    fn null_value_formats_as_empty_text() {
97        let target = StringFormatMultiValueConverter::new("Converted: {0}", None);
98
99        assert_eq!(convert(&target, &[boxed(String::from("Foo"))]), "Converted: Foo");
100        assert_eq!(convert(&target, &[None]), "Converted: ");
101    }
102
103    #[test]
104    fn format_without_braces_is_literal_text() {
105        let target = StringFormatMultiValueConverter::new("0.00", None);
106
107        assert_eq!(convert(&target, &[boxed(1.5f64)]), "0.00");
108    }
109
110    #[test]
111    fn result_of_inner_converter_is_the_only_argument() {
112        let inner: Rc<dyn IMultiValueConverter> =
113            Rc::new(FuncMultiValueConverter::<i32, i32>::new(|x| x.iter().sum()));
114        let target = StringFormatMultiValueConverter::new("Sum: {0:D3}", Some(inner.clone()));
115
116        assert_eq!(convert(&target, &[boxed(1i32), boxed(2i32), boxed(3i32)]), "Sum: 006");
117
118        let target = StringFormatMultiValueConverter::new("{1}", Some(inner));
119        let error = target.convert(&[boxed(1i32), boxed(2i32)], ValueType::of::<String>(), None, &crate::utilities::CultureInfo::invariant_culture()).expect_err("an error");
120        assert_eq!(error.inner().downcast_ref::<FormatError>(), Some(&FormatError::IndexOutOfRange));
121    }
122
123    #[test]
124    fn missing_value_is_an_error() {
125        let target = StringFormatMultiValueConverter::new("{0} {1}", None);
126
127        let error = target.convert(&[boxed(1i32)], ValueType::of::<String>(), None, &crate::utilities::CultureInfo::invariant_culture()).expect_err("an error");
128
129        assert_eq!(error.inner().downcast_ref::<FormatError>(), Some(&FormatError::IndexOutOfRange));
130    }
131}