1
//! Hash-able floating-point wrappers, percentage values, and CSS size
2
//! metric types used by the CSS property system.
3

            
4
use core::fmt;
5
use std::num::ParseFloatError;
6

            
7
use crate::corety::AzString;
8

            
9
/// Multiplier for floating point accuracy.
10
///
11
/// Elements such as px or %
12
/// are only accurate until a certain number of decimal points, therefore
13
/// they have to be casted to isizes in order to make the f32 values
14
/// hash-able: Css has a relatively low precision here, roughly 3 digits, i.e
15
/// `1.001 == 1.0`
16
pub const FP_PRECISION_MULTIPLIER: f32 = 1000.0;
17
const FP_PRECISION_MULTIPLIER_CONST: isize = crate::cast::f32_to_isize(FP_PRECISION_MULTIPLIER);
18

            
19
/// Wrapper around `FloatValue`, represents a percentage instead
20
/// of just being a regular floating-point value, i.e `5` = `5%`
21
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
22
#[repr(C)]
23
pub struct PercentageValue {
24
    number: FloatValue,
25
}
26

            
27
impl_option!(
28
    PercentageValue,
29
    OptionPercentageValue,
30
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
31
);
32

            
33
impl fmt::Display for PercentageValue {
34
53
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35
53
        write!(f, "{}%", self.normalized() * 100.0)
36
53
    }
37
}
38

            
39
impl PercentageValue {
40
    /// Same as `PercentageValue::new()`, but only accepts whole numbers.
41
    /// Uses isize arithmetic to avoid floating-point in const context.
42
    #[inline]
43
428262
    #[must_use] pub const fn const_new(value: isize) -> Self {
44
428262
        Self {
45
428262
            number: FloatValue::const_new(value),
46
428262
        }
47
428262
    }
48

            
49
    /// Creates a `PercentageValue` from a fractional number in const context.
50
    ///
51
    /// # Arguments
52
    /// * `pre_comma` - The integer part (e.g., 100 for 100.5%)
53
    /// * `post_comma` - The fractional part as digits (e.g., 5 for 0.5%)
54
    ///
55
    /// # Examples
56
    /// ```
57
    /// // 100% = const_new_fractional(100, 0)
58
    /// // 50.5% = const_new_fractional(50, 5)
59
    /// ```
60
    #[inline]
61
3
    #[must_use] pub const fn const_new_fractional(pre_comma: isize, post_comma: isize) -> Self {
62
3
        Self {
63
3
            number: FloatValue::const_new_fractional(pre_comma, post_comma),
64
3
        }
65
3
    }
66

            
67
    #[inline]
68
13632
    #[must_use] pub fn new(value: f32) -> Self {
69
13632
        Self {
70
13632
            number: value.into(),
71
13632
        }
72
13632
    }
73

            
74
    // NOTE: no get() function, to avoid confusion with "150%"
75

            
76
    #[inline]
77
58103
    #[must_use] pub fn normalized(&self) -> f32 {
78
58103
        self.number.get() / 100.0
79
58103
    }
80

            
81
    #[inline]
82
1446
    #[must_use] pub fn interpolate(&self, other: &Self, t: f32) -> Self {
83
1446
        Self {
84
1446
            number: self.number.interpolate(&other.number, t),
85
1446
        }
86
1446
    }
87
}
88

            
89
/// Wrapper around an f32 value that is internally casted to an isize,
90
/// in order to provide hash-ability (to avoid numerical instability).
91
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
92
#[repr(C)]
93
pub struct FloatValue {
94
    pub(crate) number: isize,
95
}
96

            
97
impl fmt::Display for FloatValue {
98
291244
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99
291244
        write!(f, "{}", self.get())
100
291244
    }
101
}
102

            
103
impl ::core::fmt::Debug for FloatValue {
104
5
    fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
105
5
        write!(f, "{self}")
106
5
    }
107
}
108

            
109
impl Default for FloatValue {
110
55746
    fn default() -> Self {
111
        const DEFAULT_FLV: FloatValue = FloatValue::const_new(0);
112
55746
        DEFAULT_FLV
113
55746
    }
114
}
115

            
116
impl FloatValue {
117
    /// Same as `FloatValue::new()`, but only accepts whole numbers.
118
    /// Uses isize arithmetic to avoid floating-point in const context.
119
    #[inline]
120
4472197
    #[must_use] pub const fn const_new(value: isize) -> Self {
121
4472197
        Self {
122
4472197
            number: value * FP_PRECISION_MULTIPLIER_CONST,
123
4472197
        }
124
4472197
    }
125

            
126
    /// Creates a `FloatValue` from a fractional number in const context.
127
    ///
128
    /// This uses integer arithmetic to represent fractional values like 1.5, 0.83, etc.
129
    /// in const context without relying on f32 operations.
130
    ///
131
    /// The function automatically detects the number of decimal places in `post_comma`
132
    /// and supports up to 3 decimal places. If more digits are provided, only the first
133
    /// 3 are used (truncation, not rounding).
134
    ///
135
    /// # Arguments
136
    /// * `pre_comma` - The integer part (e.g., 1 for 1.5)
137
    /// * `post_comma` - The fractional part as digits (e.g., 5 for 0.5, 52 for 0.52, 523 for 0.523)
138
    ///
139
    /// # Examples
140
    /// ```
141
    /// // 1.5 = const_new_fractional(1, 5)
142
    /// // 1.52 = const_new_fractional(1, 52)
143
    /// // 1.523 = const_new_fractional(1, 523)
144
    /// // 0.83 = const_new_fractional(0, 83)
145
    /// // 1.17 = const_new_fractional(1, 17)
146
    /// // 2.123456 -> 2.123 (truncated to 3 decimal places)
147
    /// ```
148
    #[inline]
149
101
    #[must_use] pub const fn const_new_fractional(pre_comma: isize, post_comma: isize) -> Self {
150
        // Get absolute value for digit counting
151
101
        let abs_post = if post_comma < 0 {
152
6
            -post_comma
153
        } else {
154
95
            post_comma
155
        };
156

            
157
        // Determine the number of digits and extract only the first 3
158
        // Note: We limit to values that fit in 32-bit isize for WASM compatibility
159
101
        let (normalized_post, divisor) = if abs_post < 10 {
160
            // 1 digit: 5 → 0.5
161
36
            (abs_post, 10)
162
65
        } else if abs_post < 100 {
163
            // 2 digits: 83 → 0.83
164
31
            (abs_post, 100)
165
34
        } else if abs_post < 1000 {
166
            // 3 digits: 523 → 0.523
167
12
            (abs_post, 1000)
168
        } else {
169
            // 4+ digits: keep only the first 3 (e.g. 5234 → 523 → 0.523).
170
            // A fixed division ladder cannot bound the digit count for
171
            // arbitrarily large `post_comma` (an 11-digit value keeps 4 digits,
172
            // etc.), letting the "fraction" grow past 1.0 and corrupt the
173
            // integer part. Reduce until strictly below 1000 so the result is
174
            // always a proper 3-digit fraction.
175
22
            let mut reduced = abs_post;
176
146
            while reduced >= 1000 {
177
124
                reduced /= 10;
178
124
            }
179
22
            (reduced, 1000)
180
        };
181

            
182
        // Calculate fractional part
183
101
        let fractional_part = normalized_post * (FP_PRECISION_MULTIPLIER_CONST / divisor);
184

            
185
        // Apply sign: if post_comma is negative, negate the fractional part
186
101
        let signed_fractional = if post_comma < 0 {
187
6
            -fractional_part
188
        } else {
189
95
            fractional_part
190
        };
191

            
192
        // For negative pre_comma, the fractional part should also be negative
193
        // E.g., -1.5 = -1 + (-0.5), not -1 + 0.5
194
101
        let final_fractional = if pre_comma < 0 && post_comma >= 0 {
195
5
            -signed_fractional
196
        } else {
197
96
            signed_fractional
198
        };
199

            
200
101
        Self {
201
101
            number: pre_comma * FP_PRECISION_MULTIPLIER_CONST + final_fractional,
202
101
        }
203
101
    }
204

            
205
    #[inline]
206
11420902
    #[must_use] pub fn new(value: f32) -> Self {
207
11420902
        Self {
208
11420902
            number: crate::cast::f32_to_isize(value * FP_PRECISION_MULTIPLIER),
209
11420902
        }
210
11420902
    }
211

            
212
    #[inline]
213
23204749
    #[must_use] pub fn get(&self) -> f32 {
214
23204749
        crate::cast::isize_to_f32(self.number) / FP_PRECISION_MULTIPLIER
215
23204749
    }
216

            
217
    /// Returns the raw encoded `isize` (the f32 value scaled by
218
    /// `FP_PRECISION_MULTIPLIER`). Exposed so external callers can
219
    /// round-trip the value through the compact-cache encoding without
220
    /// re-multiplying through f32.
221
    #[inline]
222
7326
    #[must_use] pub const fn number(&self) -> isize {
223
7326
        self.number
224
7326
    }
225

            
226
    #[inline]
227
    #[allow(clippy::suboptimal_flops)] // explicit FP; mul_add slower without +fma
228
3103
    #[must_use] pub fn interpolate(&self, other: &Self, t: f32) -> Self {
229
3103
        let self_val_f32 = self.get();
230
3103
        let other_val_f32 = other.get();
231
3103
        let interpolated = self_val_f32 + ((other_val_f32 - self_val_f32) * t);
232
3103
        Self::new(interpolated)
233
3103
    }
234
}
235

            
236
impl From<f32> for FloatValue {
237
    #[inline]
238
13632
    fn from(val: f32) -> Self {
239
13632
        Self::new(val)
240
13632
    }
241
}
242

            
243
/// Enum representing the metric associated with a number (px, pt, em, etc.)
244
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
245
#[repr(C)]
246
#[derive(Default)]
247
pub enum SizeMetric {
248
    #[default]
249
    Px,
250
    Pt,
251
    Em,
252
    Rem,
253
    In,
254
    Cm,
255
    Mm,
256
    Percent,
257
    /// Viewport width: 1vw = 1% of viewport width
258
    Vw,
259
    /// Viewport height: 1vh = 1% of viewport height
260
    Vh,
261
    /// Viewport minimum: 1vmin = 1% of smaller viewport dimension
262
    Vmin,
263
    /// Viewport maximum: 1vmax = 1% of larger viewport dimension
264
    Vmax,
265
}
266

            
267

            
268
impl fmt::Display for SizeMetric {
269
290911
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270
        use self::SizeMetric::{Px, Pt, Em, Rem, In, Cm, Mm, Percent, Vw, Vh, Vmin, Vmax};
271
290911
        match self {
272
254526
            Px => write!(f, "px"),
273
136
            Pt => write!(f, "pt"),
274
35139
            Em => write!(f, "em"),
275
133
            Rem => write!(f, "rem"),
276
108
            In => write!(f, "in"),
277
106
            Cm => write!(f, "cm"),
278
106
            Mm => write!(f, "mm"),
279
264
            Percent => write!(f, "%"),
280
105
            Vw => write!(f, "vw"),
281
116
            Vh => write!(f, "vh"),
282
68
            Vmin => write!(f, "vmin"),
283
104
            Vmax => write!(f, "vmax"),
284
        }
285
290911
    }
286
}
287

            
288
/// # Errors
289
///
290
/// Returns an error if `input` is not a valid CSS `float-value` value.
291
2943
pub fn parse_float_value(input: &str) -> Result<FloatValue, ParseFloatError> {
292
2943
    Ok(FloatValue::new(input.trim().parse::<f32>()?))
293
2943
}
294
#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
295
#[derive(Clone, PartialEq, Eq)]
296
#[repr(C, u8)]
297
pub enum PercentageParseError {
298
    ValueParseErr(crate::props::basic::error::ParseFloatError),
299
    NoPercentSign,
300
    InvalidUnit(AzString),
301
}
302

            
303
impl_debug_as_display!(PercentageParseError);
304

            
305
impl From<ParseFloatError> for PercentageParseError {
306
    fn from(e: ParseFloatError) -> Self {
307
        Self::ValueParseErr(crate::props::basic::error::ParseFloatError::from(e))
308
    }
309
}
310

            
311
impl_display! { PercentageParseError, {
312
    ValueParseErr(e) => format!("\"{}\"", e),
313
    NoPercentSign => format!("No percent sign after number"),
314
    InvalidUnit(u) => format!("Error parsing percentage: invalid unit \"{}\"", u.as_str()),
315
}}
316
#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
317
#[derive(Debug, Clone, PartialEq, Eq)]
318
#[repr(C, u8)]
319
pub enum PercentageParseErrorOwned {
320
    ValueParseErr(crate::props::basic::error::ParseFloatError),
321
    NoPercentSign,
322
    InvalidUnit(AzString),
323
}
324

            
325
impl PercentageParseError {
326
14
    #[must_use] pub fn to_contained(&self) -> PercentageParseErrorOwned {
327
14
        match self {
328
3
            Self::ValueParseErr(e) => PercentageParseErrorOwned::ValueParseErr(*e),
329
3
            Self::NoPercentSign => PercentageParseErrorOwned::NoPercentSign,
330
8
            Self::InvalidUnit(u) => PercentageParseErrorOwned::InvalidUnit(u.clone()),
331
        }
332
14
    }
333
}
334

            
335
impl PercentageParseErrorOwned {
336
14
    #[must_use] pub fn to_shared(&self) -> PercentageParseError {
337
14
        match self {
338
3
            Self::ValueParseErr(e) => PercentageParseError::ValueParseErr(*e),
339
3
            Self::NoPercentSign => PercentageParseError::NoPercentSign,
340
8
            Self::InvalidUnit(u) => PercentageParseError::InvalidUnit(u.clone()),
341
        }
342
14
    }
343
}
344

            
345
/// Parse "1.2" or "120%" (similar to `parse_pixel_value`)
346
/// # Errors
347
///
348
/// Returns an error if `input` is not a valid CSS `percentage-value` value.
349
6848
pub fn parse_percentage_value(input: &str) -> Result<PercentageValue, PercentageParseError> {
350
6848
    let input = input.trim();
351

            
352
6848
    if input.is_empty() {
353
15
        return Err(PercentageParseError::ValueParseErr(
354
15
            crate::props::basic::error::ParseFloatError::from("empty string".parse::<f32>().unwrap_err()),
355
15
        ));
356
6833
    }
357

            
358
6833
    let mut split_pos = 0;
359
6833
    let mut found_numeric = false;
360
1791498
    for (idx, ch) in input.char_indices() {
361
1791498
        if ch.is_numeric() || ch == '.' || ch == '-' {
362
420471
            // Advance past the *whole* char: `is_numeric()` matches multi-byte
363
420471
            // Unicode digits (½ U+00BD, ٥ U+0665, 5 U+FF15). Using `idx + 1`
364
420471
            // would land inside the codepoint and panic on the slice below.
365
420471
            split_pos = idx + ch.len_utf8();
366
420471
            found_numeric = true;
367
1375017
        }
368
    }
369

            
370
6833
    if !found_numeric {
371
81
        return Err(PercentageParseError::ValueParseErr(
372
81
            crate::props::basic::error::ParseFloatError::from("no numeric value".parse::<f32>().unwrap_err()),
373
81
        ));
374
6752
    }
375

            
376
6752
    let unit = input[split_pos..].trim();
377
6752
    let mut number = input[..split_pos]
378
6752
        .trim()
379
6752
        .parse::<f32>()
380
6752
        .map_err(|e| PercentageParseError::ValueParseErr(crate::props::basic::error::ParseFloatError::from(e)))?;
381

            
382
6718
    match unit {
383
6718
        "" => {
384
6213
            number *= 100.0;
385
6213
        } // 0.5 => 50%
386
505
        "%" => {} // 50% => PercentageValue(50.0)
387
229
        other => {
388
229
            return Err(PercentageParseError::InvalidUnit(other.to_string().into()));
389
        }
390
    }
391

            
392
6489
    Ok(PercentageValue::new(number))
393
6848
}
394

            
395
#[cfg(all(test, feature = "parser"))]
396
mod tests {
397
    // Tests assert that parsed values equal the exact source literals.
398
    #![allow(clippy::float_cmp)]
399
    use super::*;
400

            
401
    #[test]
402
1
    fn test_parse_float_value() {
403
1
        assert_eq!(parse_float_value("10").unwrap().get(), 10.0);
404
1
        assert_eq!(parse_float_value("2.5").unwrap().get(), 2.5);
405
1
        assert_eq!(parse_float_value("-50.2").unwrap().get(), -50.2);
406
1
        assert_eq!(parse_float_value("  0  ").unwrap().get(), 0.0);
407
1
        assert!(parse_float_value("10a").is_err());
408
1
        assert!(parse_float_value("").is_err());
409
1
    }
410

            
411
    #[test]
412
1
    fn test_parse_percentage_value() {
413
        // With percent sign
414
1
        assert_eq!(parse_percentage_value("50%").unwrap().normalized(), 0.5);
415
1
        assert_eq!(parse_percentage_value("120%").unwrap().normalized(), 1.2);
416
1
        assert_eq!(parse_percentage_value("-25%").unwrap().normalized(), -0.25);
417
1
        assert_eq!(
418
1
            parse_percentage_value("  75.5%  ").unwrap().normalized(),
419
            0.755
420
        );
421

            
422
        // As a ratio
423
1
        assert!((parse_percentage_value("0.5").unwrap().normalized() - 0.5).abs() < 1e-6);
424
1
        assert!((parse_percentage_value("1.2").unwrap().normalized() - 1.2).abs() < 1e-6);
425
1
        assert!((parse_percentage_value("1").unwrap().normalized() - 1.0).abs() < 1e-6);
426

            
427
        // Errors
428
1
        assert!(matches!(
429
1
            parse_percentage_value("50px").err().unwrap(),
430
            PercentageParseError::InvalidUnit(_)
431
        ));
432
1
        assert!(parse_percentage_value("fifty%").is_err());
433
1
        assert!(parse_percentage_value("").is_err());
434
1
    }
435

            
436
    #[test]
437
1
    fn test_const_new_fractional_single_digit() {
438
        // Single digit post_comma (1 decimal place)
439
1
        let val = FloatValue::const_new_fractional(1, 5);
440
1
        assert_eq!(val.get(), 1.5);
441

            
442
1
        let val = FloatValue::const_new_fractional(0, 5);
443
1
        assert_eq!(val.get(), 0.5);
444

            
445
1
        let val = FloatValue::const_new_fractional(2, 3);
446
1
        assert_eq!(val.get(), 2.3);
447

            
448
1
        let val = FloatValue::const_new_fractional(0, 0);
449
1
        assert_eq!(val.get(), 0.0);
450

            
451
1
        let val = FloatValue::const_new_fractional(10, 9);
452
1
        assert_eq!(val.get(), 10.9);
453
1
    }
454

            
455
    #[test]
456
1
    fn test_const_new_fractional_two_digits() {
457
        // Two digits post_comma (2 decimal places)
458
1
        let val = FloatValue::const_new_fractional(0, 83);
459
1
        assert!((val.get() - 0.83).abs() < 0.001);
460

            
461
1
        let val = FloatValue::const_new_fractional(1, 17);
462
1
        assert!((val.get() - 1.17).abs() < 0.001);
463

            
464
1
        let val = FloatValue::const_new_fractional(1, 52);
465
1
        assert!((val.get() - 1.52).abs() < 0.001);
466

            
467
1
        let val = FloatValue::const_new_fractional(0, 33);
468
1
        assert!((val.get() - 0.33).abs() < 0.001);
469

            
470
1
        let val = FloatValue::const_new_fractional(2, 67);
471
1
        assert!((val.get() - 2.67).abs() < 0.001);
472

            
473
1
        let val = FloatValue::const_new_fractional(0, 10);
474
1
        assert!((val.get() - 0.10).abs() < 0.001);
475

            
476
1
        let val = FloatValue::const_new_fractional(0, 99);
477
1
        assert!((val.get() - 0.99).abs() < 0.001);
478
1
    }
479

            
480
    #[test]
481
1
    fn test_const_new_fractional_three_digits() {
482
        // Three digits post_comma (3 decimal places)
483
1
        let val = FloatValue::const_new_fractional(1, 523);
484
1
        assert!((val.get() - 1.523).abs() < 0.001);
485

            
486
1
        let val = FloatValue::const_new_fractional(0, 123);
487
1
        assert!((val.get() - 0.123).abs() < 0.001);
488

            
489
1
        let val = FloatValue::const_new_fractional(2, 999);
490
1
        assert!((val.get() - 2.999).abs() < 0.001);
491

            
492
1
        let val = FloatValue::const_new_fractional(0, 100);
493
1
        assert!((val.get() - 0.100).abs() < 0.001);
494

            
495
1
        let val = FloatValue::const_new_fractional(5, 1);
496
1
        assert!((val.get() - 5.1).abs() < 0.001);
497
1
    }
498

            
499
    #[test]
500
1
    fn test_const_new_fractional_truncation() {
501
        // More than 3 digits should be truncated (not rounded)
502

            
503
        // 4 digits: 5234 → 523 → 0.523
504
1
        let val = FloatValue::const_new_fractional(0, 5234);
505
1
        assert!((val.get() - 0.523).abs() < 0.001);
506

            
507
        // 5 digits: 12345 → 123 → 0.123
508
1
        let val = FloatValue::const_new_fractional(1, 12345);
509
1
        assert!((val.get() - 1.123).abs() < 0.001);
510

            
511
        // 6 digits: 123456 → 123 → 1.123
512
1
        let val = FloatValue::const_new_fractional(1, 123_456);
513
1
        assert!((val.get() - 1.123).abs() < 0.001);
514

            
515
        // 7 digits: 9876543 → 987 → 0.987
516
1
        let val = FloatValue::const_new_fractional(0, 9_876_543);
517
1
        assert!((val.get() - 0.987).abs() < 0.001);
518

            
519
        // 10 digits
520
1
        let val = FloatValue::const_new_fractional(2, 1_234_567_890);
521
1
        assert!((val.get() - 2.123).abs() < 0.001);
522
1
    }
523

            
524
    #[test]
525
1
    fn test_const_new_fractional_negative() {
526
        // Negative pre_comma values
527
1
        let val = FloatValue::const_new_fractional(-1, 5);
528
1
        assert_eq!(val.get(), -1.5);
529

            
530
1
        let val = FloatValue::const_new_fractional(0, 83);
531
1
        assert!((val.get() - 0.83).abs() < 0.001);
532

            
533
1
        let val = FloatValue::const_new_fractional(-2, 123);
534
1
        assert!((val.get() - -2.123).abs() < 0.001);
535

            
536
        // Negative post_comma (unusual case - treated as negative fractional part)
537
1
        let val = FloatValue::const_new_fractional(1, -5);
538
1
        assert_eq!(val.get(), 0.5); // 1 + (-0.5) = 0.5
539

            
540
1
        let val = FloatValue::const_new_fractional(0, -50);
541
1
        assert!((val.get() - -0.5).abs() < 0.001); // 0 + (-0.5) = -0.5
542
1
    }
543

            
544
    #[test]
545
1
    fn test_const_new_fractional_edge_cases() {
546
        // Zero
547
1
        let val = FloatValue::const_new_fractional(0, 0);
548
1
        assert_eq!(val.get(), 0.0);
549

            
550
        // Large integer part
551
1
        let val = FloatValue::const_new_fractional(100, 5);
552
1
        assert_eq!(val.get(), 100.5);
553

            
554
1
        let val = FloatValue::const_new_fractional(1000, 99);
555
1
        assert!((val.get() - 1000.99).abs() < 0.001);
556

            
557
        // Maximum precision (3 digits)
558
1
        let val = FloatValue::const_new_fractional(0, 999);
559
1
        assert!((val.get() - 0.999).abs() < 0.001);
560

            
561
        // Small fractional values
562
1
        let val = FloatValue::const_new_fractional(1, 1);
563
1
        assert!((val.get() - 1.1).abs() < 0.001);
564

            
565
1
        let val = FloatValue::const_new_fractional(1, 10);
566
1
        assert!((val.get() - 1.10).abs() < 0.001);
567
1
    }
568

            
569
    #[test]
570
1
    fn test_const_new_fractional_ua_css_values() {
571
        // Test actual values used in ua_css.rs
572

            
573
        // H1: 2em
574
1
        let val = FloatValue::const_new_fractional(2, 0);
575
1
        assert_eq!(val.get(), 2.0);
576

            
577
        // H2: 1.5em
578
1
        let val = FloatValue::const_new_fractional(1, 5);
579
1
        assert_eq!(val.get(), 1.5);
580

            
581
        // H3: 1.17em
582
1
        let val = FloatValue::const_new_fractional(1, 17);
583
1
        assert!((val.get() - 1.17).abs() < 0.001);
584

            
585
        // H4: 1em
586
1
        let val = FloatValue::const_new_fractional(1, 0);
587
1
        assert_eq!(val.get(), 1.0);
588

            
589
        // H5: 0.83em
590
1
        let val = FloatValue::const_new_fractional(0, 83);
591
1
        assert!((val.get() - 0.83).abs() < 0.001);
592

            
593
        // H6: 0.67em
594
1
        let val = FloatValue::const_new_fractional(0, 67);
595
1
        assert!((val.get() - 0.67).abs() < 0.001);
596

            
597
        // Margins: 0.67em
598
1
        let val = FloatValue::const_new_fractional(0, 67);
599
1
        assert!((val.get() - 0.67).abs() < 0.001);
600

            
601
        // Margins: 0.83em
602
1
        let val = FloatValue::const_new_fractional(0, 83);
603
1
        assert!((val.get() - 0.83).abs() < 0.001);
604

            
605
        // Margins: 1.33em
606
1
        let val = FloatValue::const_new_fractional(1, 33);
607
1
        assert!((val.get() - 1.33).abs() < 0.001);
608

            
609
        // Margins: 1.67em
610
1
        let val = FloatValue::const_new_fractional(1, 67);
611
1
        assert!((val.get() - 1.67).abs() < 0.001);
612

            
613
        // Margins: 2.33em
614
1
        let val = FloatValue::const_new_fractional(2, 33);
615
1
        assert!((val.get() - 2.33).abs() < 0.001);
616
1
    }
617

            
618
    #[test]
619
1
    fn test_const_new_fractional_consistency() {
620
        // Verify consistency between const_new_fractional and new()
621

            
622
1
        let const_val = FloatValue::const_new_fractional(1, 5);
623
1
        let runtime_val = FloatValue::new(1.5);
624
1
        assert_eq!(const_val.get(), runtime_val.get());
625

            
626
1
        let const_val = FloatValue::const_new_fractional(0, 83);
627
1
        let runtime_val = FloatValue::new(0.83);
628
1
        assert!((const_val.get() - runtime_val.get()).abs() < 0.001);
629

            
630
1
        let const_val = FloatValue::const_new_fractional(1, 523);
631
1
        let runtime_val = FloatValue::new(1.523);
632
1
        assert!((const_val.get() - runtime_val.get()).abs() < 0.001);
633

            
634
1
        let const_val = FloatValue::const_new_fractional(2, 99);
635
1
        let runtime_val = FloatValue::new(2.99);
636
1
        assert!((const_val.get() - runtime_val.get()).abs() < 0.001);
637
1
    }
638
}
639

            
640
#[cfg(test)]
641
#[allow(
642
    clippy::float_cmp,
643
    clippy::unreadable_literal,
644
    clippy::excessive_precision
645
)]
646
mod autotest_generated {
647
    use std::{
648
        collections::{hash_map::DefaultHasher, HashSet},
649
        hash::{Hash, Hasher},
650
    };
651

            
652
    use super::*;
653
    use crate::props::basic::error::ParseFloatError as CssParseFloatError;
654

            
655
    /// Largest `isize` that `const_new` can scale by `FP_PRECISION_MULTIPLIER`
656
    /// without overflowing the multiplication.
657
    const MAX_SAFE_CONST_NEW: isize = isize::MAX / 1000;
658
    const MIN_SAFE_CONST_NEW: isize = isize::MIN / 1000;
659

            
660
    fn hash_of<T: Hash>(v: &T) -> u64 {
661
        let mut h = DefaultHasher::new();
662
        v.hash(&mut h);
663
        h.finish()
664
    }
665

            
666
    // ------------------------------------------------------- FloatValue::new ---
667

            
668
    #[test]
669
    fn float_value_new_never_produces_a_non_finite_get() {
670
        // `get()` decodes an isize, so it must be finite for *every* input,
671
        // including the ones that overflow the f32 multiply inside `new()`.
672
        for v in [
673
            f32::NAN,
674
            f32::INFINITY,
675
            f32::NEG_INFINITY,
676
            f32::MAX,
677
            f32::MIN,
678
            f32::MIN_POSITIVE,
679
            -f32::MIN_POSITIVE,
680
            0.0,
681
            -0.0,
682
            1e30,
683
            -1e30,
684
        ] {
685
            let got = FloatValue::new(v).get();
686
            assert!(
687
                got.is_finite(),
688
                "FloatValue::new({v}).get() leaked a non-finite value: {got}"
689
            );
690
        }
691
    }
692

            
693
    #[test]
694
    fn float_value_new_saturates_at_the_isize_bounds() {
695
        // f32 -> isize `as` casts saturate; +inf/-inf and anything that overflows
696
        // the *1000 multiply must clamp instead of wrapping.
697
        assert_eq!(FloatValue::new(f32::INFINITY).number(), isize::MAX);
698
        assert_eq!(FloatValue::new(f32::NEG_INFINITY).number(), isize::MIN);
699
        // f32::MAX * 1000.0 overflows to +inf before the cast.
700
        assert_eq!(FloatValue::new(f32::MAX).number(), isize::MAX);
701
        assert_eq!(FloatValue::new(f32::MIN).number(), isize::MIN);
702
    }
703

            
704
    #[test]
705
    fn float_value_new_collapses_nan_to_zero() {
706
        // NaN `as isize` is defined to be 0 — assert it, so a future hand-rolled
707
        // cast that panics or wraps is caught.
708
        let nan = FloatValue::new(f32::NAN);
709
        assert_eq!(nan.number(), 0);
710
        assert_eq!(nan.get(), 0.0);
711
        // ...and NaN is therefore *equal* to the default value, not unequal-to-itself.
712
        assert_eq!(nan, FloatValue::default());
713
        assert_eq!(hash_of(&nan), hash_of(&FloatValue::default()));
714
    }
715

            
716
    #[test]
717
    fn float_value_new_does_not_leak_negative_zero() {
718
        let neg_zero = FloatValue::new(-0.0);
719
        assert_eq!(neg_zero.number(), 0);
720
        assert!(
721
            neg_zero.get().is_sign_positive(),
722
            "-0.0 round-tripped back out as a negative zero"
723
        );
724
        assert_eq!(neg_zero, FloatValue::new(0.0));
725
    }
726

            
727
    #[test]
728
    fn float_value_new_underflows_subnormals_to_zero() {
729
        // Anything below 1/1000 truncates away entirely.
730
        assert_eq!(FloatValue::new(f32::MIN_POSITIVE).number(), 0);
731
        assert_eq!(FloatValue::new(1e-30).number(), 0);
732
        assert_eq!(FloatValue::new(0.0009).number(), 0);
733
    }
734

            
735
    #[test]
736
    fn float_value_new_truncates_toward_zero_not_to_nearest() {
737
        // Encoding is `(v * 1000) as isize`, i.e. truncation — 0.0019 must NOT
738
        // round up to 0.002, and the negative side must truncate toward zero too.
739
        assert_eq!(FloatValue::new(0.0019).number(), 1);
740
        assert_eq!(FloatValue::new(0.0019).get(), 0.001);
741
        assert_eq!(FloatValue::new(-0.0019).number(), -1);
742
        assert_eq!(FloatValue::new(-0.0019).get(), -0.001);
743
    }
744

            
745
    #[test]
746
    fn float_value_quantizes_below_the_precision_limit() {
747
        // The type's whole purpose: sub-precision differences collapse, so that
748
        // Eq/Hash are stable. 4th decimal is dropped, 3rd is kept.
749
        assert_eq!(FloatValue::new(1.0001), FloatValue::new(1.0));
750
        assert_ne!(FloatValue::new(1.001), FloatValue::new(1.0));
751
    }
752

            
753
    #[test]
754
    fn float_value_eq_implies_equal_hash() {
755
        // Eq + Hash must agree — the type exists purely to be hash-able.
756
        for (a, b) in [
757
            (1.0_f32, 1.0004_f32),
758
            (-2.5, -2.5001),
759
            (0.0, -0.0),
760
            (f32::NAN, f32::NAN),
761
        ] {
762
            let (a, b) = (FloatValue::new(a), FloatValue::new(b));
763
            assert_eq!(a, b, "expected {a:?} == {b:?}");
764
            assert_eq!(hash_of(&a), hash_of(&b), "{a:?} == {b:?} but hashes differ");
765
        }
766
    }
767

            
768
    #[test]
769
    fn float_value_ord_agrees_with_get() {
770
        // Ord is derived on the encoded isize; it must stay monotonic w.r.t. get().
771
        let mut vals: Vec<FloatValue> = [3.5_f32, -1.0, 0.0, 100.25, -0.001, 2.0]
772
            .into_iter()
773
            .map(FloatValue::new)
774
            .collect();
775
        vals.sort();
776
        for w in vals.windows(2) {
777
            assert!(
778
                w[0].get() <= w[1].get(),
779
                "sort order disagrees with get(): {:?} then {:?}",
780
                w[0],
781
                w[1]
782
            );
783
        }
784
    }
785

            
786
    // -------------------------------------------------- FloatValue::const_new ---
787

            
788
    #[test]
789
    fn const_new_matches_the_documented_encoding() {
790
        assert_eq!(FP_PRECISION_MULTIPLIER, 1000.0);
791
        assert_eq!(FloatValue::const_new(0).number(), 0);
792
        assert_eq!(FloatValue::const_new(1).number(), 1000);
793
        assert_eq!(FloatValue::const_new(-1).number(), -1000);
794
        assert_eq!(FloatValue::const_new(0), FloatValue::default());
795
    }
796

            
797
    #[test]
798
    fn const_new_agrees_with_new_for_whole_numbers() {
799
        for n in [-1000_isize, -7, -1, 0, 1, 7, 1000, 65_536] {
800
            let c = FloatValue::const_new(n);
801
            let r = FloatValue::new(n as f32);
802
            assert_eq!(
803
                c, r,
804
                "const_new({n}) = {c:?} disagrees with new({n}.0) = {r:?}"
805
            );
806
        }
807
    }
808

            
809
    #[test]
810
    fn const_new_survives_the_largest_non_overflowing_inputs() {
811
        // `const_new` is a bare `value * 1000`, so isize::MAX/1000 is the last
812
        // input it can take without overflowing. Pin that boundary: anything at
813
        // or below it must be exact and must not panic.
814
        let hi = FloatValue::const_new(MAX_SAFE_CONST_NEW);
815
        assert_eq!(hi.number(), MAX_SAFE_CONST_NEW * 1000);
816
        assert!(hi.get().is_finite());
817

            
818
        let lo = FloatValue::const_new(MIN_SAFE_CONST_NEW);
819
        assert_eq!(lo.number(), MIN_SAFE_CONST_NEW * 1000);
820
        assert!(lo.get().is_finite());
821

            
822
        assert!(lo < hi);
823
    }
824

            
825
    // --------------------------------------- FloatValue::const_new_fractional ---
826

            
827
    #[test]
828
    fn const_new_fractional_zero_and_sign_handling() {
829
        assert_eq!(FloatValue::const_new_fractional(0, 0).number(), 0);
830
        // Negative pre_comma pulls the fraction negative too (-1.5, not -0.5).
831
        assert_eq!(FloatValue::const_new_fractional(-1, 5).number(), -1500);
832
        // Negative post_comma subtracts from a positive pre_comma.
833
        assert_eq!(FloatValue::const_new_fractional(1, -5).number(), 500);
834
        assert_eq!(FloatValue::const_new_fractional(0, -50).number(), -500);
835
    }
836

            
837
    #[test]
838
    fn const_new_fractional_never_panics_on_extreme_post_comma() {
839
        // post_comma is an unbounded isize; the digit-count ladder must not
840
        // divide by zero, overflow, or produce a non-finite decode.
841
        for post in [
842
            9_isize,
843
            99,
844
            999,
845
            9_999,
846
            99_999,
847
            999_999,
848
            9_999_999,
849
            99_999_999,
850
            999_999_999,
851
            isize::MAX,
852
        ] {
853
            let v = FloatValue::const_new_fractional(0, post);
854
            assert!(
855
                v.get().is_finite(),
856
                "const_new_fractional(0, {post}) decoded to a non-finite value"
857
            );
858
        }
859
    }
860

            
861
    #[test]
862
    fn const_new_fractional_truncates_to_three_decimals() {
863
        // Documented: only the first 3 digits of post_comma are used, truncated.
864
        assert_eq!(FloatValue::const_new_fractional(0, 5234).number(), 523);
865
        assert_eq!(FloatValue::const_new_fractional(1, 123_456).number(), 1123);
866
        // 10 digits is the largest post_comma the ladder still truncates correctly.
867
        assert_eq!(
868
            FloatValue::const_new_fractional(2, 1_234_567_890).number(),
869
            2123
870
        );
871
    }
872

            
873
    #[test]
874
    fn const_new_fractional_boundary_between_digit_buckets() {
875
        // Every `abs_post < 10^k` bucket edge: 9/10, 99/100, 999/1000.
876
        assert_eq!(FloatValue::const_new_fractional(0, 9).get(), 0.9);
877
        assert_eq!(FloatValue::const_new_fractional(0, 10).get(), 0.1);
878
        assert_eq!(FloatValue::const_new_fractional(0, 99).get(), 0.99);
879
        assert_eq!(FloatValue::const_new_fractional(0, 100).get(), 0.1);
880
        assert_eq!(FloatValue::const_new_fractional(0, 999).get(), 0.999);
881
    }
882

            
883
    #[test]
884
    fn const_new_fractional_cannot_express_a_leading_zero_fraction() {
885
        // The bucket is picked from the *digit count* of post_comma, so a leading
886
        // zero is unrepresentable in an integer argument: 0.05 has no spelling.
887
        // Both of the obvious attempts land on 0.5 instead. Pin the footgun so a
888
        // caller writing `(0, 50)` for "0.05em" is caught by this test, not by a
889
        // 10x-too-large margin on screen.
890
        assert_eq!(FloatValue::const_new_fractional(0, 5).get(), 0.5);
891
        assert_eq!(FloatValue::const_new_fractional(0, 50).get(), 0.5);
892
        assert_eq!(FloatValue::const_new_fractional(0, 500).get(), 0.5);
893
    }
894

            
895
    // ------------------------------------------------- FloatValue::interpolate ---
896

            
897
    #[test]
898
    fn interpolate_endpoints_are_exact() {
899
        let a = FloatValue::new(0.0);
900
        let b = FloatValue::new(10.0);
901
        assert_eq!(a.interpolate(&b, 0.0), a);
902
        assert_eq!(a.interpolate(&b, 1.0), b);
903
        assert_eq!(a.interpolate(&b, 0.5).get(), 5.0);
904
        // Reversed direction.
905
        assert_eq!(b.interpolate(&a, 0.5).get(), 5.0);
906
    }
907

            
908
    #[test]
909
    fn interpolate_extrapolates_outside_zero_one() {
910
        // t is not clamped — assert the (documented-by-absence) extrapolation
911
        // rather than silently assuming a clamp that isn't there.
912
        let a = FloatValue::new(0.0);
913
        let b = FloatValue::new(10.0);
914
        assert_eq!(a.interpolate(&b, 2.0).get(), 20.0);
915
        assert_eq!(a.interpolate(&b, -1.0).get(), -10.0);
916
    }
917

            
918
    #[test]
919
    fn interpolate_with_nan_or_infinite_t_stays_finite() {
920
        let a = FloatValue::new(0.0);
921
        let b = FloatValue::new(10.0);
922

            
923
        // NaN t -> NaN interpolant -> `as isize` collapses to 0.
924
        assert_eq!(a.interpolate(&b, f32::NAN).number(), 0);
925

            
926
        // +inf t with a non-zero delta -> +inf -> saturates to isize::MAX.
927
        assert_eq!(a.interpolate(&b, f32::INFINITY).number(), isize::MAX);
928
        assert_eq!(a.interpolate(&b, f32::NEG_INFINITY).number(), isize::MIN);
929

            
930
        // inf * 0.0 delta is NaN -> collapses to 0 (self is NOT preserved here).
931
        assert_eq!(a.interpolate(&a, f32::INFINITY).number(), 0);
932

            
933
        for t in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, f32::MAX, f32::MIN] {
934
            assert!(
935
                a.interpolate(&b, t).get().is_finite(),
936
                "interpolate(t = {t}) leaked a non-finite value"
937
            );
938
        }
939
    }
940

            
941
    #[test]
942
    fn interpolate_between_saturated_extremes_does_not_panic() {
943
        let lo = FloatValue::new(f32::NEG_INFINITY); // isize::MIN
944
        let hi = FloatValue::new(f32::INFINITY); // isize::MAX
945
        for t in [0.0, 0.5, 1.0, -1.0, 2.0, f32::NAN] {
946
            assert!(lo.interpolate(&hi, t).get().is_finite());
947
            assert!(hi.interpolate(&lo, t).get().is_finite());
948
        }
949
    }
950

            
951
    // -------------------------------------------------------- round-tripping ---
952

            
953
    #[test]
954
    fn float_value_round_trips_through_display_and_parse() {
955
        // encode == decode: every value that is exactly representable at 3
956
        // decimals must survive Display -> parse_float_value -> FloatValue.
957
        for v in [0.0_f32, 1.5, -2.25, 100.0, 0.001, -0.001, 999.999, -0.5] {
958
            let fv = FloatValue::new(v);
959
            let round_tripped = parse_float_value(&fv.to_string())
960
                .unwrap_or_else(|e| panic!("Display of {fv:?} did not re-parse: {e}"));
961
            assert_eq!(
962
                fv, round_tripped,
963
                "round-trip changed {fv:?} into {round_tripped:?}"
964
            );
965
        }
966
    }
967

            
968
    #[test]
969
    fn float_value_number_round_trips_through_get() {
970
        // number() is the compact-cache encoding; get() must be its exact inverse
971
        // (scaled) for values inside the f32-exact integer range.
972
        for raw in [0_isize, 1, -1, 1500, -1500, 999_999, -999_999] {
973
            let fv = FloatValue::new(raw as f32 / 1000.0);
974
            assert_eq!(fv.number(), raw, "number() lost the encoding for {raw}");
975
        }
976
    }
977

            
978
    #[test]
979
    fn float_value_display_and_debug_agree() {
980
        // Debug is hand-written to forward to Display; a divergence means the
981
        // manual impl drifted.
982
        for v in [0.0_f32, -1.25, 1e6, f32::INFINITY, f32::NAN] {
983
            let fv = FloatValue::new(v);
984
            assert_eq!(format!("{fv:?}"), format!("{fv}"));
985
            assert!(!format!("{fv}").is_empty());
986
            // Whatever we print must itself be a parseable float.
987
            assert!(fv.to_string().parse::<f32>().is_ok());
988
        }
989
        assert_eq!(FloatValue::default().to_string(), "0");
990
    }
991

            
992
    // ---------------------------------------------------------- SizeMetric ---
993

            
994
    #[test]
995
    fn size_metric_display_is_non_empty_and_unique() {
996
        use SizeMetric::{Cm, Em, In, Mm, Percent, Pt, Px, Rem, Vh, Vmax, Vmin, Vw};
997

            
998
        let all = [Px, Pt, Em, Rem, In, Cm, Mm, Percent, Vw, Vh, Vmin, Vmax];
999
        let mut seen = HashSet::new();
        for m in all {
            let s = m.to_string();
            assert!(!s.is_empty(), "{m:?} renders as an empty string");
            assert!(
                seen.insert(s.clone()),
                "two SizeMetric variants both render as {s:?} (copy-paste in Display)"
            );
        }
        assert_eq!(seen.len(), all.len());
    }
    #[test]
    fn size_metric_display_matches_the_css_unit_tokens() {
        assert_eq!(SizeMetric::Px.to_string(), "px");
        assert_eq!(SizeMetric::Pt.to_string(), "pt");
        assert_eq!(SizeMetric::Em.to_string(), "em");
        assert_eq!(SizeMetric::Rem.to_string(), "rem");
        assert_eq!(SizeMetric::In.to_string(), "in");
        assert_eq!(SizeMetric::Cm.to_string(), "cm");
        assert_eq!(SizeMetric::Mm.to_string(), "mm");
        assert_eq!(SizeMetric::Percent.to_string(), "%");
        assert_eq!(SizeMetric::Vw.to_string(), "vw");
        assert_eq!(SizeMetric::Vh.to_string(), "vh");
        assert_eq!(SizeMetric::Vmin.to_string(), "vmin");
        assert_eq!(SizeMetric::Vmax.to_string(), "vmax");
    }
    #[test]
    fn size_metric_default_is_px() {
        assert_eq!(SizeMetric::default(), SizeMetric::Px);
        assert_eq!(SizeMetric::default().to_string(), "px");
    }
    // -------------------------------------------------------- PercentageValue ---
    #[test]
    fn percentage_value_normalized_divides_by_a_hundred() {
        assert_eq!(PercentageValue::new(50.0).normalized(), 0.5);
        assert_eq!(PercentageValue::new(0.0).normalized(), 0.0);
        assert_eq!(PercentageValue::new(-25.0).normalized(), -0.25);
        assert_eq!(PercentageValue::const_new(100).normalized(), 1.0);
        assert_eq!(PercentageValue::default().normalized(), 0.0);
    }
    #[test]
    fn percentage_value_normalized_is_always_finite() {
        for v in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, f32::MAX, f32::MIN] {
            let n = PercentageValue::new(v).normalized();
            assert!(
                n.is_finite(),
                "PercentageValue::new({v}).normalized() leaked {n}"
            );
        }
        // NaN collapses to the default, exactly like FloatValue.
        assert_eq!(PercentageValue::new(f32::NAN), PercentageValue::default());
    }
    #[test]
    fn percentage_value_const_new_boundaries_do_not_panic() {
        assert_eq!(PercentageValue::const_new(0), PercentageValue::default());
        assert!(PercentageValue::const_new(MAX_SAFE_CONST_NEW)
            .normalized()
            .is_finite());
        assert!(PercentageValue::const_new(MIN_SAFE_CONST_NEW)
            .normalized()
            .is_finite());
        assert!(
            PercentageValue::const_new(MIN_SAFE_CONST_NEW)
                < PercentageValue::const_new(MAX_SAFE_CONST_NEW)
        );
    }
    #[test]
    fn percentage_value_const_new_fractional_matches_the_docs() {
        // 100% = const_new_fractional(100, 0); 50.5% = const_new_fractional(50, 5)
        assert_eq!(
            PercentageValue::const_new_fractional(100, 0).normalized(),
            1.0
        );
        assert!((PercentageValue::const_new_fractional(50, 5).normalized() - 0.505).abs() < 1e-5);
        assert_eq!(
            PercentageValue::const_new_fractional(100, 0),
            PercentageValue::const_new(100)
        );
    }
    #[test]
    fn percentage_value_interpolate_endpoints_and_nan() {
        let a = PercentageValue::new(0.0);
        let b = PercentageValue::new(100.0);
        assert_eq!(a.interpolate(&b, 0.0), a);
        assert_eq!(a.interpolate(&b, 1.0), b);
        assert_eq!(a.interpolate(&b, 0.5).normalized(), 0.5);
        // NaN / inf t must not panic and must stay finite.
        assert_eq!(a.interpolate(&b, f32::NAN).normalized(), 0.0);
        assert!(a.interpolate(&b, f32::INFINITY).normalized().is_finite());
        assert!(a.interpolate(&b, f32::NEG_INFINITY).normalized().is_finite());
    }
    #[test]
    fn percentage_value_display_round_trips_through_the_parser() {
        for v in [0.0_f32, 50.0, 100.0, 150.0, -25.0, 75.5, 0.5] {
            let p = PercentageValue::new(v);
            let s = p.to_string();
            assert!(s.ends_with('%'), "Display lost the percent sign: {s:?}");
            let back = parse_percentage_value(&s)
                .unwrap_or_else(|e| panic!("Display of {p:?} ({s:?}) did not re-parse: {e}"));
            assert!(
                (back.normalized() - p.normalized()).abs() < 1e-4,
                "round-trip drifted: {p:?} -> {s:?} -> {back:?}"
            );
        }
    }
    // ----------------------------------------------------- parse_float_value ---
    #[test]
    fn parse_float_value_positive_control() {
        assert_eq!(parse_float_value("0").unwrap().number(), 0);
        assert_eq!(parse_float_value("1.5").unwrap().number(), 1500);
        assert_eq!(parse_float_value("-1.5").unwrap().number(), -1500);
        assert_eq!(parse_float_value("+2").unwrap().number(), 2000);
        // Rust's f32 parser accepts these shorthand forms.
        assert_eq!(parse_float_value(".5").unwrap().number(), 500);
        assert_eq!(parse_float_value("5.").unwrap().number(), 5000);
    }
    #[test]
    fn parse_float_value_rejects_empty_and_whitespace() {
        assert!(parse_float_value("").is_err());
        assert!(parse_float_value("   ").is_err());
        assert!(parse_float_value("\t\n\r ").is_err());
    }
    #[test]
    fn parse_float_value_rejects_garbage() {
        for input in [
            "abc", "1_000", "1,5", "0x10", "1.2.3", "--1", "1e", "e5", "5 5", "1/2", ";", "\0",
            "5;garbage", "50px", "5%",
        ] {
            assert!(
                parse_float_value(input).is_err(),
                "garbage input {input:?} was accepted"
            );
        }
    }
    #[test]
    fn parse_float_value_trims_but_does_not_tolerate_inner_junk() {
        assert_eq!(parse_float_value("  1.5  ").unwrap().number(), 1500);
        assert!(parse_float_value("1.5 garbage").is_err());
    }
    #[test]
    fn parse_float_value_boundary_numbers_saturate_instead_of_panicking() {
        // -0 must not leak a negative zero out of the encoding.
        assert_eq!(parse_float_value("-0").unwrap().number(), 0);
        assert!(parse_float_value("-0").unwrap().get().is_sign_positive());
        // Rust parses "NaN"/"inf" successfully; the encoding must then defuse them.
        assert_eq!(parse_float_value("NaN").unwrap().number(), 0);
        assert_eq!(parse_float_value("inf").unwrap().number(), isize::MAX);
        assert_eq!(parse_float_value("infinity").unwrap().number(), isize::MAX);
        assert_eq!(parse_float_value("-inf").unwrap().number(), isize::MIN);
        // Overflow of the f32 parse itself is Ok(inf) in Rust, then saturates.
        assert_eq!(parse_float_value("1e400").unwrap().number(), isize::MAX);
        assert_eq!(parse_float_value("-1e400").unwrap().number(), isize::MIN);
        // Underflow is Ok(0.0).
        assert_eq!(parse_float_value("1e-400").unwrap().number(), 0);
        // i64::MAX / f64::MAX as literals: no panic, still finite after decode.
        for input in [
            "9223372036854775807",
            "-9223372036854775808",
            "179769313486231570000000000000000000000000000000000",
        ] {
            let v = parse_float_value(input)
                .unwrap_or_else(|e| panic!("{input:?} should parse as f32, got {e}"));
            assert!(v.get().is_finite(), "{input:?} decoded to {}", v.get());
        }
    }
    #[test]
    fn parse_float_value_unicode_does_not_panic() {
        // Multi-byte input must be rejected, never sliced mid-codepoint.
        for input in [
            "\u{1F600}",  // emoji
            "5\u{1F600}", // digit + emoji
            "\u{0665}",   // ARABIC-INDIC DIGIT FIVE (is_numeric() == true)
            "5\u{0301}",  // digit + combining acute
            "\u{00BD}",   // ½ (No category, is_numeric() == true)
            "\u{FF15}",   // FULLWIDTH DIGIT FIVE
            "\u{200B}5",  // zero-width space + digit
            "\u{2212}5",  // U+2212 MINUS SIGN (not ASCII '-')
        ] {
            assert!(
                parse_float_value(input).is_err(),
                "non-ASCII input {input:?} was accepted as a float"
            );
        }
    }
    #[test]
    fn parse_float_value_extremely_long_input_terminates() {
        // 200k digits: must not hang, must not panic; Rust yields Ok(inf), which
        // then saturates in the encoding.
        let huge = "9".repeat(200_000);
        // Rejecting is acceptable too — just don't panic/hang on the huge input.
        if let Ok(v) = parse_float_value(&huge) {
            assert!(v.get().is_finite(), "200k digits decoded to {}", v.get());
        }
        // Long *garbage* must be rejected rather than scanned quadratically.
        let long_junk = "a".repeat(200_000);
        assert!(parse_float_value(&long_junk).is_err());
    }
    #[test]
    fn parse_float_value_deeply_nested_input_does_not_stack_overflow() {
        let nested = "(".repeat(10_000);
        assert!(parse_float_value(&nested).is_err());
        let nested_pair = format!("{}5{}", "(".repeat(10_000), ")".repeat(10_000));
        assert!(parse_float_value(&nested_pair).is_err());
    }
    // ------------------------------------------------ parse_percentage_value ---
    #[test]
    fn parse_percentage_value_positive_control() {
        assert_eq!(parse_percentage_value("50%").unwrap().normalized(), 0.5);
        assert_eq!(parse_percentage_value("0%").unwrap().normalized(), 0.0);
        assert_eq!(parse_percentage_value("-25%").unwrap().normalized(), -0.25);
        // A bare number is a *ratio*, not a percent: "0.5" == "50%".
        assert_eq!(
            parse_percentage_value("0.5").unwrap(),
            parse_percentage_value("50%").unwrap()
        );
    }
    #[test]
    fn parse_percentage_value_bare_number_is_multiplied_by_a_hundred() {
        // Easy to misread: "50" (no sign) is 5000%, not 50%.
        assert_eq!(parse_percentage_value("50").unwrap().normalized(), 50.0);
        assert_ne!(
            parse_percentage_value("50").unwrap(),
            parse_percentage_value("50%").unwrap()
        );
    }
    #[test]
    fn parse_percentage_value_rejects_empty_and_whitespace() {
        assert!(matches!(
            parse_percentage_value(""),
            Err(PercentageParseError::ValueParseErr(_))
        ));
        assert!(matches!(
            parse_percentage_value("   "),
            Err(PercentageParseError::ValueParseErr(_))
        ));
        assert!(matches!(
            parse_percentage_value("\t\n"),
            Err(PercentageParseError::ValueParseErr(_))
        ));
        assert!(parse_percentage_value("%").is_err());
    }
    #[test]
    fn parse_percentage_value_rejects_garbage_without_panicking() {
        for input in [
            "abc", "fifty%", "%50", "50%%", "5 0 %", "--5%", "1.2.3%", ";", "\0", "NaN", "inf",
            "-inf",
        ] {
            assert!(
                parse_percentage_value(input).is_err(),
                "garbage input {input:?} was accepted"
            );
        }
    }
    #[test]
    fn parse_percentage_value_reports_invalid_units() {
        for (input, unit) in [("50px", "px"), ("50em", "em"), ("1.5rem", "rem")] {
            match parse_percentage_value(input) {
                Err(PercentageParseError::InvalidUnit(u)) => assert_eq!(u.as_str(), unit),
                other => panic!("{input:?} should be InvalidUnit({unit:?}), got {other:?}"),
            }
        }
    }
    #[test]
    fn parse_percentage_value_trims_leading_and_trailing_whitespace() {
        assert_eq!(
            parse_percentage_value("  75.5%  ").unwrap().normalized(),
            0.755
        );
        // Whitespace *between* the number and the unit is trimmed as well.
        assert_eq!(parse_percentage_value("50 %").unwrap().normalized(), 0.5);
    }
    #[test]
    fn parse_percentage_value_boundary_numbers_stay_finite() {
        // -0 must not leak a negative zero.
        let neg_zero = parse_percentage_value("-0%").unwrap();
        assert_eq!(neg_zero.normalized(), 0.0);
        assert!(neg_zero.normalized().is_sign_positive());
        // Overflowing exponent parses to inf, then saturates in the encoding.
        let huge = parse_percentage_value("1e400%").unwrap();
        assert!(
            huge.normalized().is_finite(),
            "1e400% leaked {}",
            huge.normalized()
        );
        let huge_neg = parse_percentage_value("-1e400%").unwrap();
        assert!(huge_neg.normalized().is_finite());
        // Underflowing exponent parses to 0.
        assert_eq!(parse_percentage_value("1e-400%").unwrap().normalized(), 0.0);
        // i64::MAX-sized literal: no panic, still finite.
        let big = parse_percentage_value("9223372036854775807%").unwrap();
        assert!(big.normalized().is_finite());
    }
    #[test]
    fn parse_percentage_value_ascii_unicode_neighbours_do_not_panic() {
        // Multi-byte chars that are NOT `char::is_numeric()` are safe to slice
        // around; they must be rejected, not panic.
        for input in [
            "\u{1F600}",  // emoji only
            "50\u{1F600}", // digits then emoji -> InvalidUnit
            "\u{20AC}50",  // €50 -> unparseable number
            "abc\u{00E9}%",
            "\u{200B}%", // zero-width space
        ] {
            assert!(
                parse_percentage_value(input).is_err(),
                "{input:?} was accepted"
            );
        }
        // The emoji suffix is reported as an invalid unit, not a parse error.
        assert!(matches!(
            parse_percentage_value("50\u{1F600}"),
            Err(PercentageParseError::InvalidUnit(_))
        ));
    }
    #[test]
    fn parse_percentage_value_extremely_long_input_terminates() {
        let huge = format!("{}%", "9".repeat(200_000));
        if let Ok(v) = parse_percentage_value(&huge) { assert!(v.normalized().is_finite()) }
        let long_junk = format!("{}%", "a".repeat(200_000));
        assert!(parse_percentage_value(&long_junk).is_err());
    }
    #[test]
    fn parse_percentage_value_deeply_nested_input_does_not_stack_overflow() {
        assert!(parse_percentage_value(&"(".repeat(10_000)).is_err());
        // A numeric char buried behind 10k brackets: the scanner must still just
        // split and fail on the number, not recurse.
        let nested = format!("{}5%", "(".repeat(10_000));
        assert!(parse_percentage_value(&nested).is_err());
    }
    // --------------------------------------------- PercentageParseError glue ---
    #[test]
    fn percentage_parse_error_round_trips_through_owned() {
        let variants = [
            PercentageParseError::ValueParseErr(CssParseFloatError::Empty),
            PercentageParseError::ValueParseErr(CssParseFloatError::Invalid),
            PercentageParseError::NoPercentSign,
            PercentageParseError::InvalidUnit(String::new().into()),
            PercentageParseError::InvalidUnit("px".to_string().into()),
            // A unit that is itself multi-byte must survive the AzString clone.
            PercentageParseError::InvalidUnit("\u{1F600}".to_string().into()),
        ];
        for e in variants {
            let round_tripped = e.to_contained().to_shared();
            assert_eq!(
                e, round_tripped,
                "to_contained/to_shared is not the identity for {e:?}"
            );
        }
    }
    #[test]
    fn percentage_parse_error_owned_round_trips_through_shared() {
        let variants = [
            PercentageParseErrorOwned::ValueParseErr(CssParseFloatError::Invalid),
            PercentageParseErrorOwned::NoPercentSign,
            PercentageParseErrorOwned::InvalidUnit("vh".to_string().into()),
        ];
        for e in variants {
            assert_eq!(e.to_shared().to_contained(), e);
        }
    }
    #[test]
    fn percentage_parse_error_display_is_non_empty() {
        // Debug forwards to Display (impl_debug_as_display); neither may be empty
        // nor panic, including for an empty invalid unit.
        for e in [
            PercentageParseError::ValueParseErr(CssParseFloatError::Empty),
            PercentageParseError::NoPercentSign,
            PercentageParseError::InvalidUnit(String::new().into()),
        ] {
            let shown = e.to_string();
            assert!(!shown.is_empty(), "{e:?} renders as an empty message");
            assert_eq!(format!("{e:?}"), shown);
        }
    }
    // ---------------------------------------------------- former known bugs ---
    //
    // The two regression tests below pin behaviour these functions used to get
    // wrong (a multi-byte-digit slice panic and a fraction escaping [0, 1)).
    // Both are now fixed and asserted un-ignored.
    #[test]
    fn known_bug_percentage_multibyte_numeric_char_panics() {
        // `char::is_numeric()` is true for Nd/Nl/No — including multi-byte chars
        // like '½' (U+00BD, 2 bytes) and '٥' (U+0665, 2 bytes). The scanner
        // records their *start* byte index, then slices at `split_pos + 1`, which
        // lands inside the codepoint => `input[split_pos..]` panics.
        //
        // Reachable from any author stylesheet (`width: ½%`), so this panics the
        // CSS parser on untrusted input.
        for input in ["\u{00BD}%", "\u{0665}%", "5\u{00BD}", "\u{FF15}%"] {
            assert!(
                parse_percentage_value(input).is_err(),
                "{input:?} should be rejected"
            );
        }
    }
    #[test]
    #[cfg(target_pointer_width = "64")]
    fn known_bug_const_new_fractional_huge_post_comma_escapes_the_fraction() {
        // The digit-count ladder's last arm divides by 10_000_000, which only
        // truncates a 10-digit post_comma down to 3 digits. An 11-digit value
        // keeps 4 digits, a 12-digit value keeps 5, ... so the "fractional" part
        // grows past 1.0 and corrupts the integer part.
        for post in [12_345_678_901_isize, 123_456_789_012, isize::MAX] {
            let frac = FloatValue::const_new_fractional(0, post).get();
            assert!(
                (0.0..1.0).contains(&frac),
                "const_new_fractional(0, {post}) produced {frac}, which is not a fraction"
            );
        }
    }
}