1use core::fmt;
5use std::num::ParseFloatError;
6
7use crate::corety::AzString;
8
9pub const FP_PRECISION_MULTIPLIER: f32 = 1000.0;
17const FP_PRECISION_MULTIPLIER_CONST: isize = crate::cast::f32_to_isize(FP_PRECISION_MULTIPLIER);
18
19#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
22#[repr(C)]
23pub struct PercentageValue {
24 number: FloatValue,
25}
26
27impl_option!(
28 PercentageValue,
29 OptionPercentageValue,
30 [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
31);
32
33impl fmt::Display for PercentageValue {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 write!(f, "{}%", self.normalized() * 100.0)
36 }
37}
38
39impl PercentageValue {
40 #[inline]
43 #[must_use]
44 pub const fn const_new(value: isize) -> Self {
45 Self {
46 number: FloatValue::const_new(value),
47 }
48 }
49
50 #[inline]
62 #[must_use]
63 pub const fn const_new_fractional(pre_comma: isize, post_comma: isize) -> Self {
64 Self {
65 number: FloatValue::const_new_fractional(pre_comma, post_comma),
66 }
67 }
68
69 #[inline]
70 #[must_use]
71 pub fn new(value: f32) -> Self {
72 Self {
73 number: value.into(),
74 }
75 }
76
77 #[inline]
80 #[must_use]
81 pub fn normalized(&self) -> f32 {
82 self.number.get() / 100.0
83 }
84
85 #[inline]
86 #[must_use]
87 pub fn interpolate(&self, other: &Self, t: f32) -> Self {
88 Self {
89 number: self.number.interpolate(&other.number, t),
90 }
91 }
92}
93
94#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
97#[repr(C)]
98pub struct FloatValue {
99 pub(crate) number: isize,
100}
101
102impl fmt::Display for FloatValue {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 write!(f, "{}", self.get())
105 }
106}
107
108impl ::core::fmt::Debug for FloatValue {
109 fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
110 write!(f, "{self}")
111 }
112}
113
114impl Default for FloatValue {
115 fn default() -> Self {
116 const DEFAULT_FLV: FloatValue = FloatValue::const_new(0);
117 DEFAULT_FLV
118 }
119}
120
121impl FloatValue {
122 #[inline]
125 #[must_use]
126 pub const fn const_new(value: isize) -> Self {
127 Self {
128 number: value * FP_PRECISION_MULTIPLIER_CONST,
129 }
130 }
131
132 #[inline]
155 #[must_use]
156 pub const fn const_new_fractional(pre_comma: isize, post_comma: isize) -> Self {
157 let abs_post = if post_comma < 0 {
159 -post_comma
160 } else {
161 post_comma
162 };
163
164 let (normalized_post, divisor) = if abs_post < 10 {
167 (abs_post, 10)
169 } else if abs_post < 100 {
170 (abs_post, 100)
172 } else if abs_post < 1000 {
173 (abs_post, 1000)
175 } else {
176 let mut reduced = abs_post;
183 while reduced >= 1000 {
184 reduced /= 10;
185 }
186 (reduced, 1000)
187 };
188
189 let fractional_part = normalized_post * (FP_PRECISION_MULTIPLIER_CONST / divisor);
191
192 let signed_fractional = if post_comma < 0 {
194 -fractional_part
195 } else {
196 fractional_part
197 };
198
199 let final_fractional = if pre_comma < 0 && post_comma >= 0 {
202 -signed_fractional
203 } else {
204 signed_fractional
205 };
206
207 Self {
208 number: pre_comma * FP_PRECISION_MULTIPLIER_CONST + final_fractional,
209 }
210 }
211
212 #[inline]
213 #[must_use]
214 pub fn new(value: f32) -> Self {
215 Self {
216 number: crate::cast::f32_to_isize(value * FP_PRECISION_MULTIPLIER),
217 }
218 }
219
220 #[inline]
221 #[must_use]
222 pub fn get(&self) -> f32 {
223 crate::cast::isize_to_f32(self.number) / FP_PRECISION_MULTIPLIER
224 }
225
226 #[inline]
231 #[must_use]
232 pub const fn number(&self) -> isize {
233 self.number
234 }
235
236 #[inline]
237 #[allow(clippy::suboptimal_flops)] #[must_use]
239 pub fn interpolate(&self, other: &Self, t: f32) -> Self {
240 let self_val_f32 = self.get();
241 let other_val_f32 = other.get();
242 let interpolated = self_val_f32 + ((other_val_f32 - self_val_f32) * t);
243 Self::new(interpolated)
244 }
245}
246
247impl From<f32> for FloatValue {
248 #[inline]
249 fn from(val: f32) -> Self {
250 Self::new(val)
251 }
252}
253
254#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
256#[repr(C)]
257#[derive(Default)]
258pub enum SizeMetric {
259 #[default]
260 Px,
261 Pt,
262 Em,
263 Rem,
264 In,
265 Cm,
266 Mm,
267 Percent,
268 Vw,
270 Vh,
272 Vmin,
274 Vmax,
276}
277
278impl fmt::Display for SizeMetric {
279 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
280 use self::SizeMetric::{Cm, Em, In, Mm, Percent, Pt, Px, Rem, Vh, Vmax, Vmin, Vw};
281 match self {
282 Px => write!(f, "px"),
283 Pt => write!(f, "pt"),
284 Em => write!(f, "em"),
285 Rem => write!(f, "rem"),
286 In => write!(f, "in"),
287 Cm => write!(f, "cm"),
288 Mm => write!(f, "mm"),
289 Percent => write!(f, "%"),
290 Vw => write!(f, "vw"),
291 Vh => write!(f, "vh"),
292 Vmin => write!(f, "vmin"),
293 Vmax => write!(f, "vmax"),
294 }
295 }
296}
297
298pub fn parse_float_value(input: &str) -> Result<FloatValue, ParseFloatError> {
302 Ok(FloatValue::new(input.trim().parse::<f32>()?))
303}
304#[allow(variant_size_differences)]
305#[derive(Clone, PartialEq, Eq)]
307#[repr(C, u8)]
308pub enum PercentageParseError {
309 ValueParseErr(crate::props::basic::error::ParseFloatError),
310 NoPercentSign,
311 InvalidUnit(AzString),
312}
313
314impl_debug_as_display!(PercentageParseError);
315
316impl From<ParseFloatError> for PercentageParseError {
317 fn from(e: ParseFloatError) -> Self {
318 Self::ValueParseErr(crate::props::basic::error::ParseFloatError::from(e))
319 }
320}
321
322impl_display! { PercentageParseError, {
323 ValueParseErr(e) => format!("\"{}\"", e),
324 NoPercentSign => format!("No percent sign after number"),
325 InvalidUnit(u) => format!("Error parsing percentage: invalid unit \"{}\"", u.as_str()),
326}}
327#[allow(variant_size_differences)]
328#[derive(Debug, Clone, PartialEq, Eq)]
330#[repr(C, u8)]
331pub enum PercentageParseErrorOwned {
332 ValueParseErr(crate::props::basic::error::ParseFloatError),
333 NoPercentSign,
334 InvalidUnit(AzString),
335}
336
337impl PercentageParseError {
338 #[must_use]
339 pub fn to_contained(&self) -> PercentageParseErrorOwned {
340 match self {
341 Self::ValueParseErr(e) => PercentageParseErrorOwned::ValueParseErr(*e),
342 Self::NoPercentSign => PercentageParseErrorOwned::NoPercentSign,
343 Self::InvalidUnit(u) => PercentageParseErrorOwned::InvalidUnit(u.clone()),
344 }
345 }
346}
347
348impl PercentageParseErrorOwned {
349 #[must_use]
350 pub fn to_shared(&self) -> PercentageParseError {
351 match self {
352 Self::ValueParseErr(e) => PercentageParseError::ValueParseErr(*e),
353 Self::NoPercentSign => PercentageParseError::NoPercentSign,
354 Self::InvalidUnit(u) => PercentageParseError::InvalidUnit(u.clone()),
355 }
356 }
357}
358
359pub fn parse_percentage_value(input: &str) -> Result<PercentageValue, PercentageParseError> {
364 let input = input.trim();
365
366 if input.is_empty() {
367 return Err(PercentageParseError::ValueParseErr(
368 crate::props::basic::error::ParseFloatError::from(
369 "empty string".parse::<f32>().unwrap_err(),
370 ),
371 ));
372 }
373
374 let mut split_pos = 0;
375 let mut found_numeric = false;
376 for (idx, ch) in input.char_indices() {
377 if ch.is_numeric() || ch == '.' || ch == '-' {
378 split_pos = idx + ch.len_utf8();
382 found_numeric = true;
383 }
384 }
385
386 if !found_numeric {
387 return Err(PercentageParseError::ValueParseErr(
388 crate::props::basic::error::ParseFloatError::from(
389 "no numeric value".parse::<f32>().unwrap_err(),
390 ),
391 ));
392 }
393
394 let unit = input[split_pos..].trim();
395 let mut number = input[..split_pos].trim().parse::<f32>().map_err(|e| {
396 PercentageParseError::ValueParseErr(crate::props::basic::error::ParseFloatError::from(e))
397 })?;
398
399 match unit {
400 "" => {
401 number *= 100.0;
402 } "%" => {} other => {
405 return Err(PercentageParseError::InvalidUnit(other.to_string().into()));
406 }
407 }
408
409 Ok(PercentageValue::new(number))
410}
411
412#[cfg(all(test, feature = "parser"))]
413mod tests {
414 #![allow(clippy::float_cmp)]
416 use super::*;
417
418 #[test]
419 fn test_parse_float_value() {
420 assert_eq!(parse_float_value("10").unwrap().get(), 10.0);
421 assert_eq!(parse_float_value("2.5").unwrap().get(), 2.5);
422 assert_eq!(parse_float_value("-50.2").unwrap().get(), -50.2);
423 assert_eq!(parse_float_value(" 0 ").unwrap().get(), 0.0);
424 assert!(parse_float_value("10a").is_err());
425 assert!(parse_float_value("").is_err());
426 }
427
428 #[test]
429 fn test_parse_percentage_value() {
430 assert_eq!(parse_percentage_value("50%").unwrap().normalized(), 0.5);
432 assert_eq!(parse_percentage_value("120%").unwrap().normalized(), 1.2);
433 assert_eq!(parse_percentage_value("-25%").unwrap().normalized(), -0.25);
434 assert_eq!(
435 parse_percentage_value(" 75.5% ").unwrap().normalized(),
436 0.755
437 );
438
439 assert!((parse_percentage_value("0.5").unwrap().normalized() - 0.5).abs() < 1e-6);
441 assert!((parse_percentage_value("1.2").unwrap().normalized() - 1.2).abs() < 1e-6);
442 assert!((parse_percentage_value("1").unwrap().normalized() - 1.0).abs() < 1e-6);
443
444 assert!(matches!(
446 parse_percentage_value("50px").err().unwrap(),
447 PercentageParseError::InvalidUnit(_)
448 ));
449 assert!(parse_percentage_value("fifty%").is_err());
450 assert!(parse_percentage_value("").is_err());
451 }
452
453 #[test]
454 fn test_const_new_fractional_single_digit() {
455 let val = FloatValue::const_new_fractional(1, 5);
457 assert_eq!(val.get(), 1.5);
458
459 let val = FloatValue::const_new_fractional(0, 5);
460 assert_eq!(val.get(), 0.5);
461
462 let val = FloatValue::const_new_fractional(2, 3);
463 assert_eq!(val.get(), 2.3);
464
465 let val = FloatValue::const_new_fractional(0, 0);
466 assert_eq!(val.get(), 0.0);
467
468 let val = FloatValue::const_new_fractional(10, 9);
469 assert_eq!(val.get(), 10.9);
470 }
471
472 #[test]
473 fn test_const_new_fractional_two_digits() {
474 let val = FloatValue::const_new_fractional(0, 83);
476 assert!((val.get() - 0.83).abs() < 0.001);
477
478 let val = FloatValue::const_new_fractional(1, 17);
479 assert!((val.get() - 1.17).abs() < 0.001);
480
481 let val = FloatValue::const_new_fractional(1, 52);
482 assert!((val.get() - 1.52).abs() < 0.001);
483
484 let val = FloatValue::const_new_fractional(0, 33);
485 assert!((val.get() - 0.33).abs() < 0.001);
486
487 let val = FloatValue::const_new_fractional(2, 67);
488 assert!((val.get() - 2.67).abs() < 0.001);
489
490 let val = FloatValue::const_new_fractional(0, 10);
491 assert!((val.get() - 0.10).abs() < 0.001);
492
493 let val = FloatValue::const_new_fractional(0, 99);
494 assert!((val.get() - 0.99).abs() < 0.001);
495 }
496
497 #[test]
498 fn test_const_new_fractional_three_digits() {
499 let val = FloatValue::const_new_fractional(1, 523);
501 assert!((val.get() - 1.523).abs() < 0.001);
502
503 let val = FloatValue::const_new_fractional(0, 123);
504 assert!((val.get() - 0.123).abs() < 0.001);
505
506 let val = FloatValue::const_new_fractional(2, 999);
507 assert!((val.get() - 2.999).abs() < 0.001);
508
509 let val = FloatValue::const_new_fractional(0, 100);
510 assert!((val.get() - 0.100).abs() < 0.001);
511
512 let val = FloatValue::const_new_fractional(5, 1);
513 assert!((val.get() - 5.1).abs() < 0.001);
514 }
515
516 #[test]
517 fn test_const_new_fractional_truncation() {
518 let val = FloatValue::const_new_fractional(0, 5234);
522 assert!((val.get() - 0.523).abs() < 0.001);
523
524 let val = FloatValue::const_new_fractional(1, 12345);
526 assert!((val.get() - 1.123).abs() < 0.001);
527
528 let val = FloatValue::const_new_fractional(1, 123_456);
530 assert!((val.get() - 1.123).abs() < 0.001);
531
532 let val = FloatValue::const_new_fractional(0, 9_876_543);
534 assert!((val.get() - 0.987).abs() < 0.001);
535
536 let val = FloatValue::const_new_fractional(2, 1_234_567_890);
538 assert!((val.get() - 2.123).abs() < 0.001);
539 }
540
541 #[test]
542 fn test_const_new_fractional_negative() {
543 let val = FloatValue::const_new_fractional(-1, 5);
545 assert_eq!(val.get(), -1.5);
546
547 let val = FloatValue::const_new_fractional(0, 83);
548 assert!((val.get() - 0.83).abs() < 0.001);
549
550 let val = FloatValue::const_new_fractional(-2, 123);
551 assert!((val.get() - -2.123).abs() < 0.001);
552
553 let val = FloatValue::const_new_fractional(1, -5);
555 assert_eq!(val.get(), 0.5); let val = FloatValue::const_new_fractional(0, -50);
558 assert!((val.get() - -0.5).abs() < 0.001); }
560
561 #[test]
562 fn test_const_new_fractional_edge_cases() {
563 let val = FloatValue::const_new_fractional(0, 0);
565 assert_eq!(val.get(), 0.0);
566
567 let val = FloatValue::const_new_fractional(100, 5);
569 assert_eq!(val.get(), 100.5);
570
571 let val = FloatValue::const_new_fractional(1000, 99);
572 assert!((val.get() - 1000.99).abs() < 0.001);
573
574 let val = FloatValue::const_new_fractional(0, 999);
576 assert!((val.get() - 0.999).abs() < 0.001);
577
578 let val = FloatValue::const_new_fractional(1, 1);
580 assert!((val.get() - 1.1).abs() < 0.001);
581
582 let val = FloatValue::const_new_fractional(1, 10);
583 assert!((val.get() - 1.10).abs() < 0.001);
584 }
585
586 #[test]
587 fn test_const_new_fractional_ua_css_values() {
588 let val = FloatValue::const_new_fractional(2, 0);
592 assert_eq!(val.get(), 2.0);
593
594 let val = FloatValue::const_new_fractional(1, 5);
596 assert_eq!(val.get(), 1.5);
597
598 let val = FloatValue::const_new_fractional(1, 17);
600 assert!((val.get() - 1.17).abs() < 0.001);
601
602 let val = FloatValue::const_new_fractional(1, 0);
604 assert_eq!(val.get(), 1.0);
605
606 let val = FloatValue::const_new_fractional(0, 83);
608 assert!((val.get() - 0.83).abs() < 0.001);
609
610 let val = FloatValue::const_new_fractional(0, 67);
612 assert!((val.get() - 0.67).abs() < 0.001);
613
614 let val = FloatValue::const_new_fractional(0, 67);
616 assert!((val.get() - 0.67).abs() < 0.001);
617
618 let val = FloatValue::const_new_fractional(0, 83);
620 assert!((val.get() - 0.83).abs() < 0.001);
621
622 let val = FloatValue::const_new_fractional(1, 33);
624 assert!((val.get() - 1.33).abs() < 0.001);
625
626 let val = FloatValue::const_new_fractional(1, 67);
628 assert!((val.get() - 1.67).abs() < 0.001);
629
630 let val = FloatValue::const_new_fractional(2, 33);
632 assert!((val.get() - 2.33).abs() < 0.001);
633 }
634
635 #[test]
636 fn test_const_new_fractional_consistency() {
637 let const_val = FloatValue::const_new_fractional(1, 5);
640 let runtime_val = FloatValue::new(1.5);
641 assert_eq!(const_val.get(), runtime_val.get());
642
643 let const_val = FloatValue::const_new_fractional(0, 83);
644 let runtime_val = FloatValue::new(0.83);
645 assert!((const_val.get() - runtime_val.get()).abs() < 0.001);
646
647 let const_val = FloatValue::const_new_fractional(1, 523);
648 let runtime_val = FloatValue::new(1.523);
649 assert!((const_val.get() - runtime_val.get()).abs() < 0.001);
650
651 let const_val = FloatValue::const_new_fractional(2, 99);
652 let runtime_val = FloatValue::new(2.99);
653 assert!((const_val.get() - runtime_val.get()).abs() < 0.001);
654 }
655}
656
657#[cfg(test)]
658#[allow(
659 clippy::float_cmp,
660 clippy::unreadable_literal,
661 clippy::excessive_precision
662)]
663mod autotest_generated {
664 use std::{
665 collections::{hash_map::DefaultHasher, HashSet},
666 hash::{Hash, Hasher},
667 };
668
669 use super::*;
670 use crate::props::basic::error::ParseFloatError as CssParseFloatError;
671
672 const MAX_SAFE_CONST_NEW: isize = isize::MAX / 1000;
675 const MIN_SAFE_CONST_NEW: isize = isize::MIN / 1000;
676
677 fn hash_of<T: Hash>(v: &T) -> u64 {
678 let mut h = DefaultHasher::new();
679 v.hash(&mut h);
680 h.finish()
681 }
682
683 #[test]
686 fn float_value_new_never_produces_a_non_finite_get() {
687 for v in [
690 f32::NAN,
691 f32::INFINITY,
692 f32::NEG_INFINITY,
693 f32::MAX,
694 f32::MIN,
695 f32::MIN_POSITIVE,
696 -f32::MIN_POSITIVE,
697 0.0,
698 -0.0,
699 1e30,
700 -1e30,
701 ] {
702 let got = FloatValue::new(v).get();
703 assert!(
704 got.is_finite(),
705 "FloatValue::new({v}).get() leaked a non-finite value: {got}"
706 );
707 }
708 }
709
710 #[test]
711 fn float_value_new_saturates_at_the_isize_bounds() {
712 assert_eq!(FloatValue::new(f32::INFINITY).number(), isize::MAX);
715 assert_eq!(FloatValue::new(f32::NEG_INFINITY).number(), isize::MIN);
716 assert_eq!(FloatValue::new(f32::MAX).number(), isize::MAX);
718 assert_eq!(FloatValue::new(f32::MIN).number(), isize::MIN);
719 }
720
721 #[test]
722 fn float_value_new_collapses_nan_to_zero() {
723 let nan = FloatValue::new(f32::NAN);
726 assert_eq!(nan.number(), 0);
727 assert_eq!(nan.get(), 0.0);
728 assert_eq!(nan, FloatValue::default());
730 assert_eq!(hash_of(&nan), hash_of(&FloatValue::default()));
731 }
732
733 #[test]
734 fn float_value_new_does_not_leak_negative_zero() {
735 let neg_zero = FloatValue::new(-0.0);
736 assert_eq!(neg_zero.number(), 0);
737 assert!(
738 neg_zero.get().is_sign_positive(),
739 "-0.0 round-tripped back out as a negative zero"
740 );
741 assert_eq!(neg_zero, FloatValue::new(0.0));
742 }
743
744 #[test]
745 fn float_value_new_underflows_subnormals_to_zero() {
746 assert_eq!(FloatValue::new(f32::MIN_POSITIVE).number(), 0);
748 assert_eq!(FloatValue::new(1e-30).number(), 0);
749 assert_eq!(FloatValue::new(0.0009).number(), 0);
750 }
751
752 #[test]
753 fn float_value_new_truncates_toward_zero_not_to_nearest() {
754 assert_eq!(FloatValue::new(0.0019).number(), 1);
757 assert_eq!(FloatValue::new(0.0019).get(), 0.001);
758 assert_eq!(FloatValue::new(-0.0019).number(), -1);
759 assert_eq!(FloatValue::new(-0.0019).get(), -0.001);
760 }
761
762 #[test]
763 fn float_value_quantizes_below_the_precision_limit() {
764 assert_eq!(FloatValue::new(1.0001), FloatValue::new(1.0));
767 assert_ne!(FloatValue::new(1.001), FloatValue::new(1.0));
768 }
769
770 #[test]
771 fn float_value_eq_implies_equal_hash() {
772 for (a, b) in [
774 (1.0_f32, 1.0004_f32),
775 (-2.5, -2.5001),
776 (0.0, -0.0),
777 (f32::NAN, f32::NAN),
778 ] {
779 let (a, b) = (FloatValue::new(a), FloatValue::new(b));
780 assert_eq!(a, b, "expected {a:?} == {b:?}");
781 assert_eq!(hash_of(&a), hash_of(&b), "{a:?} == {b:?} but hashes differ");
782 }
783 }
784
785 #[test]
786 fn float_value_ord_agrees_with_get() {
787 let mut vals: Vec<FloatValue> = [3.5_f32, -1.0, 0.0, 100.25, -0.001, 2.0]
789 .into_iter()
790 .map(FloatValue::new)
791 .collect();
792 vals.sort();
793 for w in vals.windows(2) {
794 assert!(
795 w[0].get() <= w[1].get(),
796 "sort order disagrees with get(): {:?} then {:?}",
797 w[0],
798 w[1]
799 );
800 }
801 }
802
803 #[test]
806 fn const_new_matches_the_documented_encoding() {
807 assert_eq!(FP_PRECISION_MULTIPLIER, 1000.0);
808 assert_eq!(FloatValue::const_new(0).number(), 0);
809 assert_eq!(FloatValue::const_new(1).number(), 1000);
810 assert_eq!(FloatValue::const_new(-1).number(), -1000);
811 assert_eq!(FloatValue::const_new(0), FloatValue::default());
812 }
813
814 #[test]
815 fn const_new_agrees_with_new_for_whole_numbers() {
816 for n in [-1000_isize, -7, -1, 0, 1, 7, 1000, 65_536] {
817 let c = FloatValue::const_new(n);
818 let r = FloatValue::new(n as f32);
819 assert_eq!(
820 c, r,
821 "const_new({n}) = {c:?} disagrees with new({n}.0) = {r:?}"
822 );
823 }
824 }
825
826 #[test]
827 fn const_new_survives_the_largest_non_overflowing_inputs() {
828 let hi = FloatValue::const_new(MAX_SAFE_CONST_NEW);
832 assert_eq!(hi.number(), MAX_SAFE_CONST_NEW * 1000);
833 assert!(hi.get().is_finite());
834
835 let lo = FloatValue::const_new(MIN_SAFE_CONST_NEW);
836 assert_eq!(lo.number(), MIN_SAFE_CONST_NEW * 1000);
837 assert!(lo.get().is_finite());
838
839 assert!(lo < hi);
840 }
841
842 #[test]
845 fn const_new_fractional_zero_and_sign_handling() {
846 assert_eq!(FloatValue::const_new_fractional(0, 0).number(), 0);
847 assert_eq!(FloatValue::const_new_fractional(-1, 5).number(), -1500);
849 assert_eq!(FloatValue::const_new_fractional(1, -5).number(), 500);
851 assert_eq!(FloatValue::const_new_fractional(0, -50).number(), -500);
852 }
853
854 #[test]
855 fn const_new_fractional_never_panics_on_extreme_post_comma() {
856 for post in [
859 9_isize,
860 99,
861 999,
862 9_999,
863 99_999,
864 999_999,
865 9_999_999,
866 99_999_999,
867 999_999_999,
868 isize::MAX,
869 ] {
870 let v = FloatValue::const_new_fractional(0, post);
871 assert!(
872 v.get().is_finite(),
873 "const_new_fractional(0, {post}) decoded to a non-finite value"
874 );
875 }
876 }
877
878 #[test]
879 fn const_new_fractional_truncates_to_three_decimals() {
880 assert_eq!(FloatValue::const_new_fractional(0, 5234).number(), 523);
882 assert_eq!(FloatValue::const_new_fractional(1, 123_456).number(), 1123);
883 assert_eq!(
885 FloatValue::const_new_fractional(2, 1_234_567_890).number(),
886 2123
887 );
888 }
889
890 #[test]
891 fn const_new_fractional_boundary_between_digit_buckets() {
892 assert_eq!(FloatValue::const_new_fractional(0, 9).get(), 0.9);
894 assert_eq!(FloatValue::const_new_fractional(0, 10).get(), 0.1);
895 assert_eq!(FloatValue::const_new_fractional(0, 99).get(), 0.99);
896 assert_eq!(FloatValue::const_new_fractional(0, 100).get(), 0.1);
897 assert_eq!(FloatValue::const_new_fractional(0, 999).get(), 0.999);
898 }
899
900 #[test]
901 fn const_new_fractional_cannot_express_a_leading_zero_fraction() {
902 assert_eq!(FloatValue::const_new_fractional(0, 5).get(), 0.5);
908 assert_eq!(FloatValue::const_new_fractional(0, 50).get(), 0.5);
909 assert_eq!(FloatValue::const_new_fractional(0, 500).get(), 0.5);
910 }
911
912 #[test]
915 fn interpolate_endpoints_are_exact() {
916 let a = FloatValue::new(0.0);
917 let b = FloatValue::new(10.0);
918 assert_eq!(a.interpolate(&b, 0.0), a);
919 assert_eq!(a.interpolate(&b, 1.0), b);
920 assert_eq!(a.interpolate(&b, 0.5).get(), 5.0);
921 assert_eq!(b.interpolate(&a, 0.5).get(), 5.0);
923 }
924
925 #[test]
926 fn interpolate_extrapolates_outside_zero_one() {
927 let a = FloatValue::new(0.0);
930 let b = FloatValue::new(10.0);
931 assert_eq!(a.interpolate(&b, 2.0).get(), 20.0);
932 assert_eq!(a.interpolate(&b, -1.0).get(), -10.0);
933 }
934
935 #[test]
936 fn interpolate_with_nan_or_infinite_t_stays_finite() {
937 let a = FloatValue::new(0.0);
938 let b = FloatValue::new(10.0);
939
940 assert_eq!(a.interpolate(&b, f32::NAN).number(), 0);
942
943 assert_eq!(a.interpolate(&b, f32::INFINITY).number(), isize::MAX);
945 assert_eq!(a.interpolate(&b, f32::NEG_INFINITY).number(), isize::MIN);
946
947 assert_eq!(a.interpolate(&a, f32::INFINITY).number(), 0);
949
950 for t in [
951 f32::NAN,
952 f32::INFINITY,
953 f32::NEG_INFINITY,
954 f32::MAX,
955 f32::MIN,
956 ] {
957 assert!(
958 a.interpolate(&b, t).get().is_finite(),
959 "interpolate(t = {t}) leaked a non-finite value"
960 );
961 }
962 }
963
964 #[test]
965 fn interpolate_between_saturated_extremes_does_not_panic() {
966 let lo = FloatValue::new(f32::NEG_INFINITY); let hi = FloatValue::new(f32::INFINITY); for t in [0.0, 0.5, 1.0, -1.0, 2.0, f32::NAN] {
969 assert!(lo.interpolate(&hi, t).get().is_finite());
970 assert!(hi.interpolate(&lo, t).get().is_finite());
971 }
972 }
973
974 #[test]
977 fn float_value_round_trips_through_display_and_parse() {
978 for v in [0.0_f32, 1.5, -2.25, 100.0, 0.001, -0.001, 999.999, -0.5] {
981 let fv = FloatValue::new(v);
982 let round_tripped = parse_float_value(&fv.to_string())
983 .unwrap_or_else(|e| panic!("Display of {fv:?} did not re-parse: {e}"));
984 assert_eq!(
985 fv, round_tripped,
986 "round-trip changed {fv:?} into {round_tripped:?}"
987 );
988 }
989 }
990
991 #[test]
992 fn float_value_number_round_trips_through_get() {
993 for raw in [0_isize, 1, -1, 1500, -1500, 999_999, -999_999] {
996 let fv = FloatValue::new(raw as f32 / 1000.0);
997 assert_eq!(fv.number(), raw, "number() lost the encoding for {raw}");
998 }
999 }
1000
1001 #[test]
1002 fn float_value_display_and_debug_agree() {
1003 for v in [0.0_f32, -1.25, 1e6, f32::INFINITY, f32::NAN] {
1006 let fv = FloatValue::new(v);
1007 assert_eq!(format!("{fv:?}"), format!("{fv}"));
1008 assert!(!format!("{fv}").is_empty());
1009 assert!(fv.to_string().parse::<f32>().is_ok());
1011 }
1012 assert_eq!(FloatValue::default().to_string(), "0");
1013 }
1014
1015 #[test]
1018 fn size_metric_display_is_non_empty_and_unique() {
1019 use SizeMetric::{Cm, Em, In, Mm, Percent, Pt, Px, Rem, Vh, Vmax, Vmin, Vw};
1020
1021 let all = [Px, Pt, Em, Rem, In, Cm, Mm, Percent, Vw, Vh, Vmin, Vmax];
1022 let mut seen = HashSet::new();
1023 for m in all {
1024 let s = m.to_string();
1025 assert!(!s.is_empty(), "{m:?} renders as an empty string");
1026 assert!(
1027 seen.insert(s.clone()),
1028 "two SizeMetric variants both render as {s:?} (copy-paste in Display)"
1029 );
1030 }
1031 assert_eq!(seen.len(), all.len());
1032 }
1033
1034 #[test]
1035 fn size_metric_display_matches_the_css_unit_tokens() {
1036 assert_eq!(SizeMetric::Px.to_string(), "px");
1037 assert_eq!(SizeMetric::Pt.to_string(), "pt");
1038 assert_eq!(SizeMetric::Em.to_string(), "em");
1039 assert_eq!(SizeMetric::Rem.to_string(), "rem");
1040 assert_eq!(SizeMetric::In.to_string(), "in");
1041 assert_eq!(SizeMetric::Cm.to_string(), "cm");
1042 assert_eq!(SizeMetric::Mm.to_string(), "mm");
1043 assert_eq!(SizeMetric::Percent.to_string(), "%");
1044 assert_eq!(SizeMetric::Vw.to_string(), "vw");
1045 assert_eq!(SizeMetric::Vh.to_string(), "vh");
1046 assert_eq!(SizeMetric::Vmin.to_string(), "vmin");
1047 assert_eq!(SizeMetric::Vmax.to_string(), "vmax");
1048 }
1049
1050 #[test]
1051 fn size_metric_default_is_px() {
1052 assert_eq!(SizeMetric::default(), SizeMetric::Px);
1053 assert_eq!(SizeMetric::default().to_string(), "px");
1054 }
1055
1056 #[test]
1059 fn percentage_value_normalized_divides_by_a_hundred() {
1060 assert_eq!(PercentageValue::new(50.0).normalized(), 0.5);
1061 assert_eq!(PercentageValue::new(0.0).normalized(), 0.0);
1062 assert_eq!(PercentageValue::new(-25.0).normalized(), -0.25);
1063 assert_eq!(PercentageValue::const_new(100).normalized(), 1.0);
1064 assert_eq!(PercentageValue::default().normalized(), 0.0);
1065 }
1066
1067 #[test]
1068 fn percentage_value_normalized_is_always_finite() {
1069 for v in [
1070 f32::NAN,
1071 f32::INFINITY,
1072 f32::NEG_INFINITY,
1073 f32::MAX,
1074 f32::MIN,
1075 ] {
1076 let n = PercentageValue::new(v).normalized();
1077 assert!(
1078 n.is_finite(),
1079 "PercentageValue::new({v}).normalized() leaked {n}"
1080 );
1081 }
1082 assert_eq!(PercentageValue::new(f32::NAN), PercentageValue::default());
1084 }
1085
1086 #[test]
1087 fn percentage_value_const_new_boundaries_do_not_panic() {
1088 assert_eq!(PercentageValue::const_new(0), PercentageValue::default());
1089 assert!(PercentageValue::const_new(MAX_SAFE_CONST_NEW)
1090 .normalized()
1091 .is_finite());
1092 assert!(PercentageValue::const_new(MIN_SAFE_CONST_NEW)
1093 .normalized()
1094 .is_finite());
1095 assert!(
1096 PercentageValue::const_new(MIN_SAFE_CONST_NEW)
1097 < PercentageValue::const_new(MAX_SAFE_CONST_NEW)
1098 );
1099 }
1100
1101 #[test]
1102 fn percentage_value_const_new_fractional_matches_the_docs() {
1103 assert_eq!(
1105 PercentageValue::const_new_fractional(100, 0).normalized(),
1106 1.0
1107 );
1108 assert!((PercentageValue::const_new_fractional(50, 5).normalized() - 0.505).abs() < 1e-5);
1109 assert_eq!(
1110 PercentageValue::const_new_fractional(100, 0),
1111 PercentageValue::const_new(100)
1112 );
1113 }
1114
1115 #[test]
1116 fn percentage_value_interpolate_endpoints_and_nan() {
1117 let a = PercentageValue::new(0.0);
1118 let b = PercentageValue::new(100.0);
1119 assert_eq!(a.interpolate(&b, 0.0), a);
1120 assert_eq!(a.interpolate(&b, 1.0), b);
1121 assert_eq!(a.interpolate(&b, 0.5).normalized(), 0.5);
1122 assert_eq!(a.interpolate(&b, f32::NAN).normalized(), 0.0);
1124 assert!(a.interpolate(&b, f32::INFINITY).normalized().is_finite());
1125 assert!(a
1126 .interpolate(&b, f32::NEG_INFINITY)
1127 .normalized()
1128 .is_finite());
1129 }
1130
1131 #[test]
1132 fn percentage_value_display_round_trips_through_the_parser() {
1133 for v in [0.0_f32, 50.0, 100.0, 150.0, -25.0, 75.5, 0.5] {
1134 let p = PercentageValue::new(v);
1135 let s = p.to_string();
1136 assert!(s.ends_with('%'), "Display lost the percent sign: {s:?}");
1137 let back = parse_percentage_value(&s)
1138 .unwrap_or_else(|e| panic!("Display of {p:?} ({s:?}) did not re-parse: {e}"));
1139 assert!(
1140 (back.normalized() - p.normalized()).abs() < 1e-4,
1141 "round-trip drifted: {p:?} -> {s:?} -> {back:?}"
1142 );
1143 }
1144 }
1145
1146 #[test]
1149 fn parse_float_value_positive_control() {
1150 assert_eq!(parse_float_value("0").unwrap().number(), 0);
1151 assert_eq!(parse_float_value("1.5").unwrap().number(), 1500);
1152 assert_eq!(parse_float_value("-1.5").unwrap().number(), -1500);
1153 assert_eq!(parse_float_value("+2").unwrap().number(), 2000);
1154 assert_eq!(parse_float_value(".5").unwrap().number(), 500);
1156 assert_eq!(parse_float_value("5.").unwrap().number(), 5000);
1157 }
1158
1159 #[test]
1160 fn parse_float_value_rejects_empty_and_whitespace() {
1161 assert!(parse_float_value("").is_err());
1162 assert!(parse_float_value(" ").is_err());
1163 assert!(parse_float_value("\t\n\r ").is_err());
1164 }
1165
1166 #[test]
1167 fn parse_float_value_rejects_garbage() {
1168 for input in [
1169 "abc",
1170 "1_000",
1171 "1,5",
1172 "0x10",
1173 "1.2.3",
1174 "--1",
1175 "1e",
1176 "e5",
1177 "5 5",
1178 "1/2",
1179 ";",
1180 "\0",
1181 "5;garbage",
1182 "50px",
1183 "5%",
1184 ] {
1185 assert!(
1186 parse_float_value(input).is_err(),
1187 "garbage input {input:?} was accepted"
1188 );
1189 }
1190 }
1191
1192 #[test]
1193 fn parse_float_value_trims_but_does_not_tolerate_inner_junk() {
1194 assert_eq!(parse_float_value(" 1.5 ").unwrap().number(), 1500);
1195 assert!(parse_float_value("1.5 garbage").is_err());
1196 }
1197
1198 #[test]
1199 fn parse_float_value_boundary_numbers_saturate_instead_of_panicking() {
1200 assert_eq!(parse_float_value("-0").unwrap().number(), 0);
1202 assert!(parse_float_value("-0").unwrap().get().is_sign_positive());
1203
1204 assert_eq!(parse_float_value("NaN").unwrap().number(), 0);
1206 assert_eq!(parse_float_value("inf").unwrap().number(), isize::MAX);
1207 assert_eq!(parse_float_value("infinity").unwrap().number(), isize::MAX);
1208 assert_eq!(parse_float_value("-inf").unwrap().number(), isize::MIN);
1209
1210 assert_eq!(parse_float_value("1e400").unwrap().number(), isize::MAX);
1212 assert_eq!(parse_float_value("-1e400").unwrap().number(), isize::MIN);
1213 assert_eq!(parse_float_value("1e-400").unwrap().number(), 0);
1215
1216 for input in [
1218 "9223372036854775807",
1219 "-9223372036854775808",
1220 "179769313486231570000000000000000000000000000000000",
1221 ] {
1222 let v = parse_float_value(input)
1223 .unwrap_or_else(|e| panic!("{input:?} should parse as f32, got {e}"));
1224 assert!(v.get().is_finite(), "{input:?} decoded to {}", v.get());
1225 }
1226 }
1227
1228 #[test]
1229 fn parse_float_value_unicode_does_not_panic() {
1230 for input in [
1232 "\u{1F600}", "5\u{1F600}", "\u{0665}", "5\u{0301}", "\u{00BD}", "\u{FF15}", "\u{200B}5", "\u{2212}5", ] {
1241 assert!(
1242 parse_float_value(input).is_err(),
1243 "non-ASCII input {input:?} was accepted as a float"
1244 );
1245 }
1246 }
1247
1248 #[test]
1249 fn parse_float_value_extremely_long_input_terminates() {
1250 let huge = "9".repeat(200_000);
1253 if let Ok(v) = parse_float_value(&huge) {
1255 assert!(v.get().is_finite(), "200k digits decoded to {}", v.get());
1256 }
1257
1258 let long_junk = "a".repeat(200_000);
1260 assert!(parse_float_value(&long_junk).is_err());
1261 }
1262
1263 #[test]
1264 fn parse_float_value_deeply_nested_input_does_not_stack_overflow() {
1265 let nested = "(".repeat(10_000);
1266 assert!(parse_float_value(&nested).is_err());
1267 let nested_pair = format!("{}5{}", "(".repeat(10_000), ")".repeat(10_000));
1268 assert!(parse_float_value(&nested_pair).is_err());
1269 }
1270
1271 #[test]
1274 fn parse_percentage_value_positive_control() {
1275 assert_eq!(parse_percentage_value("50%").unwrap().normalized(), 0.5);
1276 assert_eq!(parse_percentage_value("0%").unwrap().normalized(), 0.0);
1277 assert_eq!(parse_percentage_value("-25%").unwrap().normalized(), -0.25);
1278 assert_eq!(
1280 parse_percentage_value("0.5").unwrap(),
1281 parse_percentage_value("50%").unwrap()
1282 );
1283 }
1284
1285 #[test]
1286 fn parse_percentage_value_bare_number_is_multiplied_by_a_hundred() {
1287 assert_eq!(parse_percentage_value("50").unwrap().normalized(), 50.0);
1289 assert_ne!(
1290 parse_percentage_value("50").unwrap(),
1291 parse_percentage_value("50%").unwrap()
1292 );
1293 }
1294
1295 #[test]
1296 fn parse_percentage_value_rejects_empty_and_whitespace() {
1297 assert!(matches!(
1298 parse_percentage_value(""),
1299 Err(PercentageParseError::ValueParseErr(_))
1300 ));
1301 assert!(matches!(
1302 parse_percentage_value(" "),
1303 Err(PercentageParseError::ValueParseErr(_))
1304 ));
1305 assert!(matches!(
1306 parse_percentage_value("\t\n"),
1307 Err(PercentageParseError::ValueParseErr(_))
1308 ));
1309 assert!(parse_percentage_value("%").is_err());
1310 }
1311
1312 #[test]
1313 fn parse_percentage_value_rejects_garbage_without_panicking() {
1314 for input in [
1315 "abc", "fifty%", "%50", "50%%", "5 0 %", "--5%", "1.2.3%", ";", "\0", "NaN", "inf",
1316 "-inf",
1317 ] {
1318 assert!(
1319 parse_percentage_value(input).is_err(),
1320 "garbage input {input:?} was accepted"
1321 );
1322 }
1323 }
1324
1325 #[test]
1326 fn parse_percentage_value_reports_invalid_units() {
1327 for (input, unit) in [("50px", "px"), ("50em", "em"), ("1.5rem", "rem")] {
1328 match parse_percentage_value(input) {
1329 Err(PercentageParseError::InvalidUnit(u)) => assert_eq!(u.as_str(), unit),
1330 other => panic!("{input:?} should be InvalidUnit({unit:?}), got {other:?}"),
1331 }
1332 }
1333 }
1334
1335 #[test]
1336 fn parse_percentage_value_trims_leading_and_trailing_whitespace() {
1337 assert_eq!(
1338 parse_percentage_value(" 75.5% ").unwrap().normalized(),
1339 0.755
1340 );
1341 assert_eq!(parse_percentage_value("50 %").unwrap().normalized(), 0.5);
1343 }
1344
1345 #[test]
1346 fn parse_percentage_value_boundary_numbers_stay_finite() {
1347 let neg_zero = parse_percentage_value("-0%").unwrap();
1349 assert_eq!(neg_zero.normalized(), 0.0);
1350 assert!(neg_zero.normalized().is_sign_positive());
1351
1352 let huge = parse_percentage_value("1e400%").unwrap();
1354 assert!(
1355 huge.normalized().is_finite(),
1356 "1e400% leaked {}",
1357 huge.normalized()
1358 );
1359 let huge_neg = parse_percentage_value("-1e400%").unwrap();
1360 assert!(huge_neg.normalized().is_finite());
1361 assert_eq!(parse_percentage_value("1e-400%").unwrap().normalized(), 0.0);
1363
1364 let big = parse_percentage_value("9223372036854775807%").unwrap();
1366 assert!(big.normalized().is_finite());
1367 }
1368
1369 #[test]
1370 fn parse_percentage_value_ascii_unicode_neighbours_do_not_panic() {
1371 for input in [
1374 "\u{1F600}", "50\u{1F600}", "\u{20AC}50", "abc\u{00E9}%",
1378 "\u{200B}%", ] {
1380 assert!(
1381 parse_percentage_value(input).is_err(),
1382 "{input:?} was accepted"
1383 );
1384 }
1385 assert!(matches!(
1387 parse_percentage_value("50\u{1F600}"),
1388 Err(PercentageParseError::InvalidUnit(_))
1389 ));
1390 }
1391
1392 #[test]
1393 fn parse_percentage_value_extremely_long_input_terminates() {
1394 let huge = format!("{}%", "9".repeat(200_000));
1395 if let Ok(v) = parse_percentage_value(&huge) {
1396 assert!(v.normalized().is_finite())
1397 }
1398 let long_junk = format!("{}%", "a".repeat(200_000));
1399 assert!(parse_percentage_value(&long_junk).is_err());
1400 }
1401
1402 #[test]
1403 fn parse_percentage_value_deeply_nested_input_does_not_stack_overflow() {
1404 assert!(parse_percentage_value(&"(".repeat(10_000)).is_err());
1405 let nested = format!("{}5%", "(".repeat(10_000));
1408 assert!(parse_percentage_value(&nested).is_err());
1409 }
1410
1411 #[test]
1414 fn percentage_parse_error_round_trips_through_owned() {
1415 let variants = [
1416 PercentageParseError::ValueParseErr(CssParseFloatError::Empty),
1417 PercentageParseError::ValueParseErr(CssParseFloatError::Invalid),
1418 PercentageParseError::NoPercentSign,
1419 PercentageParseError::InvalidUnit(String::new().into()),
1420 PercentageParseError::InvalidUnit("px".to_string().into()),
1421 PercentageParseError::InvalidUnit("\u{1F600}".to_string().into()),
1423 ];
1424 for e in variants {
1425 let round_tripped = e.to_contained().to_shared();
1426 assert_eq!(
1427 e, round_tripped,
1428 "to_contained/to_shared is not the identity for {e:?}"
1429 );
1430 }
1431 }
1432
1433 #[test]
1434 fn percentage_parse_error_owned_round_trips_through_shared() {
1435 let variants = [
1436 PercentageParseErrorOwned::ValueParseErr(CssParseFloatError::Invalid),
1437 PercentageParseErrorOwned::NoPercentSign,
1438 PercentageParseErrorOwned::InvalidUnit("vh".to_string().into()),
1439 ];
1440 for e in variants {
1441 assert_eq!(e.to_shared().to_contained(), e);
1442 }
1443 }
1444
1445 #[test]
1446 fn percentage_parse_error_display_is_non_empty() {
1447 for e in [
1450 PercentageParseError::ValueParseErr(CssParseFloatError::Empty),
1451 PercentageParseError::NoPercentSign,
1452 PercentageParseError::InvalidUnit(String::new().into()),
1453 ] {
1454 let shown = e.to_string();
1455 assert!(!shown.is_empty(), "{e:?} renders as an empty message");
1456 assert_eq!(format!("{e:?}"), shown);
1457 }
1458 }
1459
1460 #[test]
1467 fn known_bug_percentage_multibyte_numeric_char_panics() {
1468 for input in ["\u{00BD}%", "\u{0665}%", "5\u{00BD}", "\u{FF15}%"] {
1476 assert!(
1477 parse_percentage_value(input).is_err(),
1478 "{input:?} should be rejected"
1479 );
1480 }
1481 }
1482
1483 #[test]
1484 #[cfg(target_pointer_width = "64")]
1485 fn known_bug_const_new_fractional_huge_post_comma_escapes_the_fraction() {
1486 for post in [12_345_678_901_isize, 123_456_789_012, isize::MAX] {
1491 let frac = FloatValue::const_new_fractional(0, post).get();
1492 assert!(
1493 (0.0..1.0).contains(&frac),
1494 "const_new_fractional(0, {post}) produced {frac}, which is not a fraction"
1495 );
1496 }
1497 }
1498}