Skip to main content

style/typed_om/
sum_value.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! Typed OM Sum Value.
6
7use crate::typed_om::numeric::NoCalcNumeric;
8use crate::typed_om::numeric_type::NumericType;
9use crate::typed_om::{MathSum, MathValue, NumericValue, UnitValue};
10use itertools::Itertools;
11use std::collections::HashMap;
12use style_traits::CssString;
13use thin_vec::ThinVec;
14
15type UnitMap = HashMap<String, i32>;
16
17// <https://drafts.css-houdini.org/css-typed-om-1/#product-of-two-unit-maps>
18fn product_of_two_unit_maps(s: &UnitMap, other: &UnitMap) -> UnitMap {
19    // Step 1.
20    let mut result = s.clone();
21
22    // Step 2.
23    for (unit, power) in other {
24        // Step 2.1 & 2.2.
25        *result.entry(unit.clone()).or_insert(0) += power;
26    }
27
28    // Step 3.
29    result
30}
31
32/// <https://drafts.css-houdini.org/css-typed-om-1/#cssnumericvalue-sum-value>
33#[derive(Clone, Debug)]
34struct SumValueItem {
35    value: f32,
36    unit_map: UnitMap,
37}
38
39impl SumValueItem {
40    /// <https://drafts.css-houdini.org/css-typed-om-1/#create-a-cssunitvalue-from-a-sum-value-item>
41    fn to_unit_value(&self) -> Result<UnitValue, ()> {
42        // Step 1.
43        if self.unit_map.len() > 1 {
44            return Err(());
45        }
46
47        // Step 2.
48        if self.unit_map.is_empty() {
49            return Ok(UnitValue {
50                numeric_type: NumericType::number(),
51                value: self.value,
52                unit: CssString::from("number"),
53            });
54        }
55
56        // Step 3.
57        let (unit, power) = self.unit_map.iter().next().unwrap();
58        if *power != 1 {
59            return Err(());
60        }
61
62        // Step 4.
63        Ok(UnitValue {
64            numeric_type: NumericType::from_unit_unchecked(unit),
65            value: self.value,
66            unit: CssString::from(unit),
67        })
68    }
69}
70
71/// <https://drafts.css-houdini.org/css-typed-om-1/#cssnumericvalue-sum-value>
72#[derive(Clone, Debug)]
73pub struct SumValue(Vec<SumValueItem>);
74
75impl SumValue {
76    /// <https://drafts.css-houdini.org/css-typed-om-1/#create-a-sum-value>
77    pub fn try_from_numeric_value(value: &NumericValue) -> Result<Self, ()> {
78        match value {
79            // CSSUnitValue
80            NumericValue::Unit(unit_value) => {
81                // Step 1.
82                let mut value = unit_value.value;
83                let mut unit = unit_value.unit_str();
84
85                // Step2.
86                let numeric = NoCalcNumeric::parse_unit_value(value, unit)?;
87                if let Some(canonical_unit) = numeric.canonical_unit() {
88                    let canonical = numeric.to(canonical_unit)?;
89                    value = canonical.unitless_value();
90                    unit = canonical.unit();
91                }
92
93                // Step 3.
94                if unit.eq_ignore_ascii_case("number") {
95                    return Ok(Self(vec![SumValueItem {
96                        value,
97                        unit_map: UnitMap::new(),
98                    }]));
99                }
100
101                // Step 4.
102                Ok(Self(vec![SumValueItem {
103                    value,
104                    unit_map: [(unit.to_owned(), 1)].into_iter().collect::<UnitMap>(),
105                }]))
106            },
107
108            // CSSMathSum
109            NumericValue::Math(MathValue::Sum(math_sum)) => {
110                // Step 1.
111                let mut values: Vec<SumValueItem> = Vec::new();
112
113                // Step 2.
114                for item in &math_sum.values {
115                    // Step 2.1.
116                    let value = SumValue::try_from_numeric_value(item)?;
117
118                    // Step 2.2.
119                    for sub_value in value.0 {
120                        // Step 2.2.1.
121                        if let Some(item) = values
122                            .iter_mut()
123                            .find(|item| item.unit_map == sub_value.unit_map)
124                        {
125                            item.value += sub_value.value;
126                            continue;
127                        }
128
129                        // Step 2.2.2.
130                        values.push(sub_value);
131                    }
132                }
133
134                // Step 3.
135
136                // TODO: Create a type [1] from the unit map of each item of
137                // values, and add [2] all the types together. If the result is
138                // failure, return failure.
139                //
140                // [1] https://drafts.css-houdini.org/css-typed-om-1/#create-a-type-from-a-unit-map
141                // [2] https://drafts.css-houdini.org/css-typed-om-1/#cssnumericvalue-add-two-types
142
143                // Step 4.
144                Ok(Self(values))
145            },
146
147            // CSSMathProduct
148            NumericValue::Math(MathValue::Product(math_product)) => {
149                // Step 1.
150                let mut values = vec![SumValueItem {
151                    value: 1.0,
152                    unit_map: Default::default(),
153                }];
154
155                // Step 2.
156                for item in math_product {
157                    // Step 2.1 & 2.2.
158                    let new_values = SumValue::try_from_numeric_value(item)?;
159
160                    let mut temp = Vec::new();
161
162                    // Step 2.3.
163                    for item1 in &values {
164                        // Step 2.3.1.
165                        for item2 in &new_values.0 {
166                            // Step 2.3.1.1.
167                            let mut unit_map =
168                                product_of_two_unit_maps(&item1.unit_map, &item2.unit_map);
169                            unit_map.retain(|_, power| *power != 0);
170                            let item = SumValueItem {
171                                value: item1.value * item2.value,
172                                unit_map,
173                            };
174
175                            // Step 2.3.1.2.
176                            temp.push(item);
177                        }
178                    }
179
180                    // Step 2.4.
181                    values = temp;
182                }
183
184                // Step 3.
185                Ok(Self(values))
186            },
187
188            // CSSMathNegate
189            NumericValue::Math(MathValue::Negate(math_negate)) => {
190                // Step 1 & 2.
191                let mut values = SumValue::try_from_numeric_value(math_negate)?.0;
192
193                // Step 3.
194                for item in &mut values {
195                    item.value = -item.value;
196                }
197
198                // Step 4.
199                Ok(Self(values))
200            },
201
202            // CSSMathInvert
203            NumericValue::Math(MathValue::Invert(math_invert)) => {
204                // Step 1 & 2.
205                let mut values = SumValue::try_from_numeric_value(math_invert)?.0;
206
207                // Step 3.
208                if values.len() != 1 {
209                    return Err(());
210                }
211
212                let item = &mut values[0];
213
214                // Step 4.
215                item.value = 1.0 / item.value;
216                for power in item.unit_map.values_mut() {
217                    *power = -*power;
218                }
219
220                // Step 5.
221                Ok(Self(values))
222            },
223
224            // CSSMathMin
225            NumericValue::Math(MathValue::Min(math_min)) => {
226                // Step 1 & 2.
227                let mut args = Vec::new();
228
229                for item in math_min {
230                    let values = SumValue::try_from_numeric_value(item)?;
231
232                    if values.0.len() > 1 {
233                        return Err(());
234                    }
235
236                    args.push(values);
237                }
238
239                debug_assert!(!args.is_empty());
240
241                // Step 3.
242                if !args.iter().map(|arg| &arg.0[0].unit_map).all_equal() {
243                    return Err(());
244                }
245
246                // Step 4.
247                let min = args
248                    .into_iter()
249                    .map(|arg| arg.0.into_iter().next().unwrap())
250                    .min_by(|a, b| a.value.total_cmp(&b.value))
251                    .ok_or(())?;
252
253                Ok(Self(vec![min]))
254            },
255
256            // CSSMathMax
257            NumericValue::Math(MathValue::Max(math_max)) => {
258                // Step 1 & 2.
259                let mut args = Vec::new();
260
261                for item in math_max {
262                    let values = SumValue::try_from_numeric_value(item)?;
263
264                    if values.0.len() > 1 {
265                        return Err(());
266                    }
267
268                    args.push(values);
269                }
270                debug_assert!(!args.is_empty());
271
272                // Step 3.
273                if !args.iter().map(|arg| &arg.0[0].unit_map).all_equal() {
274                    return Err(());
275                }
276
277                // Step 4.
278                let max = args
279                    .into_iter()
280                    .map(|arg| arg.0.into_iter().next().unwrap())
281                    .max_by(|a, b| a.value.total_cmp(&b.value))
282                    .ok_or(())?;
283
284                Ok(Self(vec![max]))
285            },
286
287            // CSSMathClamp
288            //
289            // TODO: The spec currently does not define "create a sum value"
290            // for CSSMathClamp. The implementation below follows WPT and
291            // existing browser implementations. Proposed spec steps:
292            //
293            // 1. Let args be lower, value, and upper, each replaced by the
294            //    result of creating a sum value from the corresponding
295            //    internal slot.
296            //
297            // 2. If any item of args is failure, or has a length greater than
298            //    one, return failure.
299            //
300            // 3. If not all of the unit maps among the items of args are
301            //    identical, return failure.
302            //
303            // 4. Clamp value's sole item's value between lower's and upper's
304            //    sole item's values, and return value.
305            //
306            // See https://github.com/w3c/csswg-drafts/issues/14038
307            NumericValue::Math(MathValue::Clamp(math_clamp)) => {
308                // Step 1 & 2.
309                let lower = SumValue::try_from_numeric_value(&math_clamp[0])?;
310                let value = SumValue::try_from_numeric_value(&math_clamp[1])?;
311                let upper = SumValue::try_from_numeric_value(&math_clamp[2])?;
312
313                if lower.0.len() > 1 || value.0.len() > 1 || upper.0.len() > 1 {
314                    return Err(());
315                }
316
317                // Step 3.
318                if lower.0[0].unit_map != value.0[0].unit_map
319                    || lower.0[0].unit_map != upper.0[0].unit_map
320                {
321                    return Err(());
322                }
323
324                // Step 4.
325                let mut value = value.0.into_iter().next().unwrap();
326                value.value = value.value.max(lower.0[0].value).min(upper.0[0].value);
327
328                Ok(Self(vec![value]))
329            },
330        }
331    }
332
333    /// Step 3 of:
334    /// https://drafts.css-houdini.org/css-typed-om-1/#dom-cssnumericvalue-to
335    pub fn to_unit(&self, unit: &str) -> Result<UnitValue, ()> {
336        debug_assert!(NumericType::try_from_unit(unit).is_ok());
337
338        if self.0.len() != 1 {
339            return Err(());
340        }
341
342        let sole_item = &self.0[0];
343
344        let item = sole_item.to_unit_value()?;
345
346        let item = {
347            let numeric = NoCalcNumeric::parse_unit_value(item.value, item.unit_str())?;
348            let converted = numeric.to(unit)?;
349
350            UnitValue {
351                numeric_type: NumericType::from_unit_unchecked(converted.unit()),
352                value: converted.unitless_value(),
353                unit: CssString::from(converted.unit()),
354            }
355        };
356
357        Ok(item)
358    }
359
360    /// Step 3-6 of:
361    /// https://drafts.css-houdini.org/css-typed-om-1/#dom-cssnumericvalue-tosum
362    pub fn to_units(&self, units: &[&str]) -> Result<MathSum, ()> {
363        debug_assert!(units
364            .iter()
365            .all(|unit| NumericType::try_from_unit(unit).is_ok()));
366
367        // Step 3.
368        let mut values = self
369            .0
370            .iter()
371            .map(|item| item.to_unit_value())
372            .collect::<Result<Vec<_>, _>>()?;
373
374        // Step 4.
375        if units.is_empty() {
376            values.sort_by(|a, b| a.unit.cmp(&b.unit));
377
378            return Ok(MathSum::from_numeric_values_unchecked(
379                values.into_iter().map(NumericValue::Unit).collect(),
380            ));
381        }
382
383        // Step 5.
384        let mut result = ThinVec::new();
385
386        for unit in units {
387            // Step 5.1.
388            let mut temp = UnitValue {
389                numeric_type: NumericType::from_unit_unchecked(*unit),
390                value: 0.0,
391                unit: CssString::from(*unit),
392            };
393
394            // Step 5.2.
395            let mut i = 0;
396            while i < values.len() {
397                let value = &values[i];
398
399                // Step 5.2.1.
400                let value_unit = value.unit_str();
401
402                // Step 5.2.2 & 5.2.2.1.
403                let numeric = NoCalcNumeric::parse_unit_value(value.value, &value_unit)?;
404                if let Ok(converted) = numeric.to(unit) {
405                    // Step 5.2.2.2.
406                    temp.value += converted.unitless_value();
407
408                    // Step 5.2.2.3.
409                    values.remove(i);
410                } else {
411                    i += 1;
412                }
413            }
414
415            // Step 5.3.
416            result.push(NumericValue::Unit(temp));
417        }
418
419        // Step 6.
420        if !values.is_empty() {
421            return Err(());
422        }
423
424        Ok(MathSum::from_numeric_values_unchecked(result))
425    }
426}