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] pub const fn const_new(value: isize) -> Self {
44 Self {
45 number: FloatValue::const_new(value),
46 }
47 }
48
49 #[inline]
61 #[must_use] pub const fn const_new_fractional(pre_comma: isize, post_comma: isize) -> Self {
62 Self {
63 number: FloatValue::const_new_fractional(pre_comma, post_comma),
64 }
65 }
66
67 #[inline]
68 #[must_use] pub fn new(value: f32) -> Self {
69 Self {
70 number: value.into(),
71 }
72 }
73
74 #[inline]
77 #[must_use] pub fn normalized(&self) -> f32 {
78 self.number.get() / 100.0
79 }
80
81 #[inline]
82 #[must_use] pub fn interpolate(&self, other: &Self, t: f32) -> Self {
83 Self {
84 number: self.number.interpolate(&other.number, t),
85 }
86 }
87}
88
89#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
92#[repr(C)]
93pub struct FloatValue {
94 pub(crate) number: isize,
95}
96
97impl fmt::Display for FloatValue {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 write!(f, "{}", self.get())
100 }
101}
102
103impl ::core::fmt::Debug for FloatValue {
104 fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
105 write!(f, "{self}")
106 }
107}
108
109impl Default for FloatValue {
110 fn default() -> Self {
111 const DEFAULT_FLV: FloatValue = FloatValue::const_new(0);
112 DEFAULT_FLV
113 }
114}
115
116impl FloatValue {
117 #[inline]
120 #[must_use] pub const fn const_new(value: isize) -> Self {
121 Self {
122 number: value * FP_PRECISION_MULTIPLIER_CONST,
123 }
124 }
125
126 #[inline]
149 #[must_use] pub const fn const_new_fractional(pre_comma: isize, post_comma: isize) -> Self {
150 let abs_post = if post_comma < 0 {
152 -post_comma
153 } else {
154 post_comma
155 };
156
157 let (normalized_post, divisor) = if abs_post < 10 {
160 (abs_post, 10)
162 } else if abs_post < 100 {
163 (abs_post, 100)
165 } else if abs_post < 1000 {
166 (abs_post, 1000)
168 } else {
169 let mut reduced = abs_post;
176 while reduced >= 1000 {
177 reduced /= 10;
178 }
179 (reduced, 1000)
180 };
181
182 let fractional_part = normalized_post * (FP_PRECISION_MULTIPLIER_CONST / divisor);
184
185 let signed_fractional = if post_comma < 0 {
187 -fractional_part
188 } else {
189 fractional_part
190 };
191
192 let final_fractional = if pre_comma < 0 && post_comma >= 0 {
195 -signed_fractional
196 } else {
197 signed_fractional
198 };
199
200 Self {
201 number: pre_comma * FP_PRECISION_MULTIPLIER_CONST + final_fractional,
202 }
203 }
204
205 #[inline]
206 #[must_use] pub fn new(value: f32) -> Self {
207 Self {
208 number: crate::cast::f32_to_isize(value * FP_PRECISION_MULTIPLIER),
209 }
210 }
211
212 #[inline]
213 #[must_use] pub fn get(&self) -> f32 {
214 crate::cast::isize_to_f32(self.number) / FP_PRECISION_MULTIPLIER
215 }
216
217 #[inline]
222 #[must_use] pub const fn number(&self) -> isize {
223 self.number
224 }
225
226 #[inline]
227 #[allow(clippy::suboptimal_flops)] #[must_use] pub fn interpolate(&self, other: &Self, t: f32) -> Self {
229 let self_val_f32 = self.get();
230 let other_val_f32 = other.get();
231 let interpolated = self_val_f32 + ((other_val_f32 - self_val_f32) * t);
232 Self::new(interpolated)
233 }
234}
235
236impl From<f32> for FloatValue {
237 #[inline]
238 fn from(val: f32) -> Self {
239 Self::new(val)
240 }
241}
242
243#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
245#[repr(C)]
246#[derive(Default)]
247pub enum SizeMetric {
248 #[default]
249 Px,
250 Pt,
251 Em,
252 Rem,
253 In,
254 Cm,
255 Mm,
256 Percent,
257 Vw,
259 Vh,
261 Vmin,
263 Vmax,
265}
266
267
268impl fmt::Display for SizeMetric {
269 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 match self {
272 Px => write!(f, "px"),
273 Pt => write!(f, "pt"),
274 Em => write!(f, "em"),
275 Rem => write!(f, "rem"),
276 In => write!(f, "in"),
277 Cm => write!(f, "cm"),
278 Mm => write!(f, "mm"),
279 Percent => write!(f, "%"),
280 Vw => write!(f, "vw"),
281 Vh => write!(f, "vh"),
282 Vmin => write!(f, "vmin"),
283 Vmax => write!(f, "vmax"),
284 }
285 }
286}
287
288pub fn parse_float_value(input: &str) -> Result<FloatValue, ParseFloatError> {
292 Ok(FloatValue::new(input.trim().parse::<f32>()?))
293}
294#[allow(variant_size_differences)] #[derive(Clone, PartialEq, Eq)]
296#[repr(C, u8)]
297pub enum PercentageParseError {
298 ValueParseErr(crate::props::basic::error::ParseFloatError),
299 NoPercentSign,
300 InvalidUnit(AzString),
301}
302
303impl_debug_as_display!(PercentageParseError);
304
305impl From<ParseFloatError> for PercentageParseError {
306 fn from(e: ParseFloatError) -> Self {
307 Self::ValueParseErr(crate::props::basic::error::ParseFloatError::from(e))
308 }
309}
310
311impl_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)] #[derive(Debug, Clone, PartialEq, Eq)]
318#[repr(C, u8)]
319pub enum PercentageParseErrorOwned {
320 ValueParseErr(crate::props::basic::error::ParseFloatError),
321 NoPercentSign,
322 InvalidUnit(AzString),
323}
324
325impl PercentageParseError {
326 #[must_use] pub fn to_contained(&self) -> PercentageParseErrorOwned {
327 match self {
328 Self::ValueParseErr(e) => PercentageParseErrorOwned::ValueParseErr(*e),
329 Self::NoPercentSign => PercentageParseErrorOwned::NoPercentSign,
330 Self::InvalidUnit(u) => PercentageParseErrorOwned::InvalidUnit(u.clone()),
331 }
332 }
333}
334
335impl PercentageParseErrorOwned {
336 #[must_use] pub fn to_shared(&self) -> PercentageParseError {
337 match self {
338 Self::ValueParseErr(e) => PercentageParseError::ValueParseErr(*e),
339 Self::NoPercentSign => PercentageParseError::NoPercentSign,
340 Self::InvalidUnit(u) => PercentageParseError::InvalidUnit(u.clone()),
341 }
342 }
343}
344
345pub fn parse_percentage_value(input: &str) -> Result<PercentageValue, PercentageParseError> {
350 let input = input.trim();
351
352 if input.is_empty() {
353 return Err(PercentageParseError::ValueParseErr(
354 crate::props::basic::error::ParseFloatError::from("empty string".parse::<f32>().unwrap_err()),
355 ));
356 }
357
358 let mut split_pos = 0;
359 let mut found_numeric = false;
360 for (idx, ch) in input.char_indices() {
361 if ch.is_numeric() || ch == '.' || ch == '-' {
362 split_pos = idx + ch.len_utf8();
366 found_numeric = true;
367 }
368 }
369
370 if !found_numeric {
371 return Err(PercentageParseError::ValueParseErr(
372 crate::props::basic::error::ParseFloatError::from("no numeric value".parse::<f32>().unwrap_err()),
373 ));
374 }
375
376 let unit = input[split_pos..].trim();
377 let mut number = input[..split_pos]
378 .trim()
379 .parse::<f32>()
380 .map_err(|e| PercentageParseError::ValueParseErr(crate::props::basic::error::ParseFloatError::from(e)))?;
381
382 match unit {
383 "" => {
384 number *= 100.0;
385 } "%" => {} other => {
388 return Err(PercentageParseError::InvalidUnit(other.to_string().into()));
389 }
390 }
391
392 Ok(PercentageValue::new(number))
393}
394
395#[cfg(all(test, feature = "parser"))]
396mod tests {
397 #![allow(clippy::float_cmp)]
399 use super::*;
400
401 #[test]
402 fn test_parse_float_value() {
403 assert_eq!(parse_float_value("10").unwrap().get(), 10.0);
404 assert_eq!(parse_float_value("2.5").unwrap().get(), 2.5);
405 assert_eq!(parse_float_value("-50.2").unwrap().get(), -50.2);
406 assert_eq!(parse_float_value(" 0 ").unwrap().get(), 0.0);
407 assert!(parse_float_value("10a").is_err());
408 assert!(parse_float_value("").is_err());
409 }
410
411 #[test]
412 fn test_parse_percentage_value() {
413 assert_eq!(parse_percentage_value("50%").unwrap().normalized(), 0.5);
415 assert_eq!(parse_percentage_value("120%").unwrap().normalized(), 1.2);
416 assert_eq!(parse_percentage_value("-25%").unwrap().normalized(), -0.25);
417 assert_eq!(
418 parse_percentage_value(" 75.5% ").unwrap().normalized(),
419 0.755
420 );
421
422 assert!((parse_percentage_value("0.5").unwrap().normalized() - 0.5).abs() < 1e-6);
424 assert!((parse_percentage_value("1.2").unwrap().normalized() - 1.2).abs() < 1e-6);
425 assert!((parse_percentage_value("1").unwrap().normalized() - 1.0).abs() < 1e-6);
426
427 assert!(matches!(
429 parse_percentage_value("50px").err().unwrap(),
430 PercentageParseError::InvalidUnit(_)
431 ));
432 assert!(parse_percentage_value("fifty%").is_err());
433 assert!(parse_percentage_value("").is_err());
434 }
435
436 #[test]
437 fn test_const_new_fractional_single_digit() {
438 let val = FloatValue::const_new_fractional(1, 5);
440 assert_eq!(val.get(), 1.5);
441
442 let val = FloatValue::const_new_fractional(0, 5);
443 assert_eq!(val.get(), 0.5);
444
445 let val = FloatValue::const_new_fractional(2, 3);
446 assert_eq!(val.get(), 2.3);
447
448 let val = FloatValue::const_new_fractional(0, 0);
449 assert_eq!(val.get(), 0.0);
450
451 let val = FloatValue::const_new_fractional(10, 9);
452 assert_eq!(val.get(), 10.9);
453 }
454
455 #[test]
456 fn test_const_new_fractional_two_digits() {
457 let val = FloatValue::const_new_fractional(0, 83);
459 assert!((val.get() - 0.83).abs() < 0.001);
460
461 let val = FloatValue::const_new_fractional(1, 17);
462 assert!((val.get() - 1.17).abs() < 0.001);
463
464 let val = FloatValue::const_new_fractional(1, 52);
465 assert!((val.get() - 1.52).abs() < 0.001);
466
467 let val = FloatValue::const_new_fractional(0, 33);
468 assert!((val.get() - 0.33).abs() < 0.001);
469
470 let val = FloatValue::const_new_fractional(2, 67);
471 assert!((val.get() - 2.67).abs() < 0.001);
472
473 let val = FloatValue::const_new_fractional(0, 10);
474 assert!((val.get() - 0.10).abs() < 0.001);
475
476 let val = FloatValue::const_new_fractional(0, 99);
477 assert!((val.get() - 0.99).abs() < 0.001);
478 }
479
480 #[test]
481 fn test_const_new_fractional_three_digits() {
482 let val = FloatValue::const_new_fractional(1, 523);
484 assert!((val.get() - 1.523).abs() < 0.001);
485
486 let val = FloatValue::const_new_fractional(0, 123);
487 assert!((val.get() - 0.123).abs() < 0.001);
488
489 let val = FloatValue::const_new_fractional(2, 999);
490 assert!((val.get() - 2.999).abs() < 0.001);
491
492 let val = FloatValue::const_new_fractional(0, 100);
493 assert!((val.get() - 0.100).abs() < 0.001);
494
495 let val = FloatValue::const_new_fractional(5, 1);
496 assert!((val.get() - 5.1).abs() < 0.001);
497 }
498
499 #[test]
500 fn test_const_new_fractional_truncation() {
501 let val = FloatValue::const_new_fractional(0, 5234);
505 assert!((val.get() - 0.523).abs() < 0.001);
506
507 let val = FloatValue::const_new_fractional(1, 12345);
509 assert!((val.get() - 1.123).abs() < 0.001);
510
511 let val = FloatValue::const_new_fractional(1, 123_456);
513 assert!((val.get() - 1.123).abs() < 0.001);
514
515 let val = FloatValue::const_new_fractional(0, 9_876_543);
517 assert!((val.get() - 0.987).abs() < 0.001);
518
519 let val = FloatValue::const_new_fractional(2, 1_234_567_890);
521 assert!((val.get() - 2.123).abs() < 0.001);
522 }
523
524 #[test]
525 fn test_const_new_fractional_negative() {
526 let val = FloatValue::const_new_fractional(-1, 5);
528 assert_eq!(val.get(), -1.5);
529
530 let val = FloatValue::const_new_fractional(0, 83);
531 assert!((val.get() - 0.83).abs() < 0.001);
532
533 let val = FloatValue::const_new_fractional(-2, 123);
534 assert!((val.get() - -2.123).abs() < 0.001);
535
536 let val = FloatValue::const_new_fractional(1, -5);
538 assert_eq!(val.get(), 0.5); let val = FloatValue::const_new_fractional(0, -50);
541 assert!((val.get() - -0.5).abs() < 0.001); }
543
544 #[test]
545 fn test_const_new_fractional_edge_cases() {
546 let val = FloatValue::const_new_fractional(0, 0);
548 assert_eq!(val.get(), 0.0);
549
550 let val = FloatValue::const_new_fractional(100, 5);
552 assert_eq!(val.get(), 100.5);
553
554 let val = FloatValue::const_new_fractional(1000, 99);
555 assert!((val.get() - 1000.99).abs() < 0.001);
556
557 let val = FloatValue::const_new_fractional(0, 999);
559 assert!((val.get() - 0.999).abs() < 0.001);
560
561 let val = FloatValue::const_new_fractional(1, 1);
563 assert!((val.get() - 1.1).abs() < 0.001);
564
565 let val = FloatValue::const_new_fractional(1, 10);
566 assert!((val.get() - 1.10).abs() < 0.001);
567 }
568
569 #[test]
570 fn test_const_new_fractional_ua_css_values() {
571 let val = FloatValue::const_new_fractional(2, 0);
575 assert_eq!(val.get(), 2.0);
576
577 let val = FloatValue::const_new_fractional(1, 5);
579 assert_eq!(val.get(), 1.5);
580
581 let val = FloatValue::const_new_fractional(1, 17);
583 assert!((val.get() - 1.17).abs() < 0.001);
584
585 let val = FloatValue::const_new_fractional(1, 0);
587 assert_eq!(val.get(), 1.0);
588
589 let val = FloatValue::const_new_fractional(0, 83);
591 assert!((val.get() - 0.83).abs() < 0.001);
592
593 let val = FloatValue::const_new_fractional(0, 67);
595 assert!((val.get() - 0.67).abs() < 0.001);
596
597 let val = FloatValue::const_new_fractional(0, 67);
599 assert!((val.get() - 0.67).abs() < 0.001);
600
601 let val = FloatValue::const_new_fractional(0, 83);
603 assert!((val.get() - 0.83).abs() < 0.001);
604
605 let val = FloatValue::const_new_fractional(1, 33);
607 assert!((val.get() - 1.33).abs() < 0.001);
608
609 let val = FloatValue::const_new_fractional(1, 67);
611 assert!((val.get() - 1.67).abs() < 0.001);
612
613 let val = FloatValue::const_new_fractional(2, 33);
615 assert!((val.get() - 2.33).abs() < 0.001);
616 }
617
618 #[test]
619 fn test_const_new_fractional_consistency() {
620 let const_val = FloatValue::const_new_fractional(1, 5);
623 let runtime_val = FloatValue::new(1.5);
624 assert_eq!(const_val.get(), runtime_val.get());
625
626 let const_val = FloatValue::const_new_fractional(0, 83);
627 let runtime_val = FloatValue::new(0.83);
628 assert!((const_val.get() - runtime_val.get()).abs() < 0.001);
629
630 let const_val = FloatValue::const_new_fractional(1, 523);
631 let runtime_val = FloatValue::new(1.523);
632 assert!((const_val.get() - runtime_val.get()).abs() < 0.001);
633
634 let const_val = FloatValue::const_new_fractional(2, 99);
635 let runtime_val = FloatValue::new(2.99);
636 assert!((const_val.get() - runtime_val.get()).abs() < 0.001);
637 }
638}
639
640#[cfg(test)]
641#[allow(
642 clippy::float_cmp,
643 clippy::unreadable_literal,
644 clippy::excessive_precision
645)]
646mod 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 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 #[test]
669 fn float_value_new_never_produces_a_non_finite_get() {
670 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 assert_eq!(FloatValue::new(f32::INFINITY).number(), isize::MAX);
698 assert_eq!(FloatValue::new(f32::NEG_INFINITY).number(), isize::MIN);
699 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 let nan = FloatValue::new(f32::NAN);
709 assert_eq!(nan.number(), 0);
710 assert_eq!(nan.get(), 0.0);
711 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 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 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 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 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 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 #[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 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 #[test]
828 fn const_new_fractional_zero_and_sign_handling() {
829 assert_eq!(FloatValue::const_new_fractional(0, 0).number(), 0);
830 assert_eq!(FloatValue::const_new_fractional(-1, 5).number(), -1500);
832 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 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 assert_eq!(FloatValue::const_new_fractional(0, 5234).number(), 523);
865 assert_eq!(FloatValue::const_new_fractional(1, 123_456).number(), 1123);
866 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 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 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 #[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 assert_eq!(b.interpolate(&a, 0.5).get(), 5.0);
906 }
907
908 #[test]
909 fn interpolate_extrapolates_outside_zero_one() {
910 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 assert_eq!(a.interpolate(&b, f32::NAN).number(), 0);
925
926 assert_eq!(a.interpolate(&b, f32::INFINITY).number(), isize::MAX);
928 assert_eq!(a.interpolate(&b, f32::NEG_INFINITY).number(), isize::MIN);
929
930 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); let hi = FloatValue::new(f32::INFINITY); 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 #[test]
954 fn float_value_round_trips_through_display_and_parse() {
955 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 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 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 assert!(fv.to_string().parse::<f32>().is_ok());
988 }
989 assert_eq!(FloatValue::default().to_string(), "0");
990 }
991
992 #[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();
1000 for m in all {
1001 let s = m.to_string();
1002 assert!(!s.is_empty(), "{m:?} renders as an empty string");
1003 assert!(
1004 seen.insert(s.clone()),
1005 "two SizeMetric variants both render as {s:?} (copy-paste in Display)"
1006 );
1007 }
1008 assert_eq!(seen.len(), all.len());
1009 }
1010
1011 #[test]
1012 fn size_metric_display_matches_the_css_unit_tokens() {
1013 assert_eq!(SizeMetric::Px.to_string(), "px");
1014 assert_eq!(SizeMetric::Pt.to_string(), "pt");
1015 assert_eq!(SizeMetric::Em.to_string(), "em");
1016 assert_eq!(SizeMetric::Rem.to_string(), "rem");
1017 assert_eq!(SizeMetric::In.to_string(), "in");
1018 assert_eq!(SizeMetric::Cm.to_string(), "cm");
1019 assert_eq!(SizeMetric::Mm.to_string(), "mm");
1020 assert_eq!(SizeMetric::Percent.to_string(), "%");
1021 assert_eq!(SizeMetric::Vw.to_string(), "vw");
1022 assert_eq!(SizeMetric::Vh.to_string(), "vh");
1023 assert_eq!(SizeMetric::Vmin.to_string(), "vmin");
1024 assert_eq!(SizeMetric::Vmax.to_string(), "vmax");
1025 }
1026
1027 #[test]
1028 fn size_metric_default_is_px() {
1029 assert_eq!(SizeMetric::default(), SizeMetric::Px);
1030 assert_eq!(SizeMetric::default().to_string(), "px");
1031 }
1032
1033 #[test]
1036 fn percentage_value_normalized_divides_by_a_hundred() {
1037 assert_eq!(PercentageValue::new(50.0).normalized(), 0.5);
1038 assert_eq!(PercentageValue::new(0.0).normalized(), 0.0);
1039 assert_eq!(PercentageValue::new(-25.0).normalized(), -0.25);
1040 assert_eq!(PercentageValue::const_new(100).normalized(), 1.0);
1041 assert_eq!(PercentageValue::default().normalized(), 0.0);
1042 }
1043
1044 #[test]
1045 fn percentage_value_normalized_is_always_finite() {
1046 for v in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, f32::MAX, f32::MIN] {
1047 let n = PercentageValue::new(v).normalized();
1048 assert!(
1049 n.is_finite(),
1050 "PercentageValue::new({v}).normalized() leaked {n}"
1051 );
1052 }
1053 assert_eq!(PercentageValue::new(f32::NAN), PercentageValue::default());
1055 }
1056
1057 #[test]
1058 fn percentage_value_const_new_boundaries_do_not_panic() {
1059 assert_eq!(PercentageValue::const_new(0), PercentageValue::default());
1060 assert!(PercentageValue::const_new(MAX_SAFE_CONST_NEW)
1061 .normalized()
1062 .is_finite());
1063 assert!(PercentageValue::const_new(MIN_SAFE_CONST_NEW)
1064 .normalized()
1065 .is_finite());
1066 assert!(
1067 PercentageValue::const_new(MIN_SAFE_CONST_NEW)
1068 < PercentageValue::const_new(MAX_SAFE_CONST_NEW)
1069 );
1070 }
1071
1072 #[test]
1073 fn percentage_value_const_new_fractional_matches_the_docs() {
1074 assert_eq!(
1076 PercentageValue::const_new_fractional(100, 0).normalized(),
1077 1.0
1078 );
1079 assert!((PercentageValue::const_new_fractional(50, 5).normalized() - 0.505).abs() < 1e-5);
1080 assert_eq!(
1081 PercentageValue::const_new_fractional(100, 0),
1082 PercentageValue::const_new(100)
1083 );
1084 }
1085
1086 #[test]
1087 fn percentage_value_interpolate_endpoints_and_nan() {
1088 let a = PercentageValue::new(0.0);
1089 let b = PercentageValue::new(100.0);
1090 assert_eq!(a.interpolate(&b, 0.0), a);
1091 assert_eq!(a.interpolate(&b, 1.0), b);
1092 assert_eq!(a.interpolate(&b, 0.5).normalized(), 0.5);
1093 assert_eq!(a.interpolate(&b, f32::NAN).normalized(), 0.0);
1095 assert!(a.interpolate(&b, f32::INFINITY).normalized().is_finite());
1096 assert!(a.interpolate(&b, f32::NEG_INFINITY).normalized().is_finite());
1097 }
1098
1099 #[test]
1100 fn percentage_value_display_round_trips_through_the_parser() {
1101 for v in [0.0_f32, 50.0, 100.0, 150.0, -25.0, 75.5, 0.5] {
1102 let p = PercentageValue::new(v);
1103 let s = p.to_string();
1104 assert!(s.ends_with('%'), "Display lost the percent sign: {s:?}");
1105 let back = parse_percentage_value(&s)
1106 .unwrap_or_else(|e| panic!("Display of {p:?} ({s:?}) did not re-parse: {e}"));
1107 assert!(
1108 (back.normalized() - p.normalized()).abs() < 1e-4,
1109 "round-trip drifted: {p:?} -> {s:?} -> {back:?}"
1110 );
1111 }
1112 }
1113
1114 #[test]
1117 fn parse_float_value_positive_control() {
1118 assert_eq!(parse_float_value("0").unwrap().number(), 0);
1119 assert_eq!(parse_float_value("1.5").unwrap().number(), 1500);
1120 assert_eq!(parse_float_value("-1.5").unwrap().number(), -1500);
1121 assert_eq!(parse_float_value("+2").unwrap().number(), 2000);
1122 assert_eq!(parse_float_value(".5").unwrap().number(), 500);
1124 assert_eq!(parse_float_value("5.").unwrap().number(), 5000);
1125 }
1126
1127 #[test]
1128 fn parse_float_value_rejects_empty_and_whitespace() {
1129 assert!(parse_float_value("").is_err());
1130 assert!(parse_float_value(" ").is_err());
1131 assert!(parse_float_value("\t\n\r ").is_err());
1132 }
1133
1134 #[test]
1135 fn parse_float_value_rejects_garbage() {
1136 for input in [
1137 "abc", "1_000", "1,5", "0x10", "1.2.3", "--1", "1e", "e5", "5 5", "1/2", ";", "\0",
1138 "5;garbage", "50px", "5%",
1139 ] {
1140 assert!(
1141 parse_float_value(input).is_err(),
1142 "garbage input {input:?} was accepted"
1143 );
1144 }
1145 }
1146
1147 #[test]
1148 fn parse_float_value_trims_but_does_not_tolerate_inner_junk() {
1149 assert_eq!(parse_float_value(" 1.5 ").unwrap().number(), 1500);
1150 assert!(parse_float_value("1.5 garbage").is_err());
1151 }
1152
1153 #[test]
1154 fn parse_float_value_boundary_numbers_saturate_instead_of_panicking() {
1155 assert_eq!(parse_float_value("-0").unwrap().number(), 0);
1157 assert!(parse_float_value("-0").unwrap().get().is_sign_positive());
1158
1159 assert_eq!(parse_float_value("NaN").unwrap().number(), 0);
1161 assert_eq!(parse_float_value("inf").unwrap().number(), isize::MAX);
1162 assert_eq!(parse_float_value("infinity").unwrap().number(), isize::MAX);
1163 assert_eq!(parse_float_value("-inf").unwrap().number(), isize::MIN);
1164
1165 assert_eq!(parse_float_value("1e400").unwrap().number(), isize::MAX);
1167 assert_eq!(parse_float_value("-1e400").unwrap().number(), isize::MIN);
1168 assert_eq!(parse_float_value("1e-400").unwrap().number(), 0);
1170
1171 for input in [
1173 "9223372036854775807",
1174 "-9223372036854775808",
1175 "179769313486231570000000000000000000000000000000000",
1176 ] {
1177 let v = parse_float_value(input)
1178 .unwrap_or_else(|e| panic!("{input:?} should parse as f32, got {e}"));
1179 assert!(v.get().is_finite(), "{input:?} decoded to {}", v.get());
1180 }
1181 }
1182
1183 #[test]
1184 fn parse_float_value_unicode_does_not_panic() {
1185 for input in [
1187 "\u{1F600}", "5\u{1F600}", "\u{0665}", "5\u{0301}", "\u{00BD}", "\u{FF15}", "\u{200B}5", "\u{2212}5", ] {
1196 assert!(
1197 parse_float_value(input).is_err(),
1198 "non-ASCII input {input:?} was accepted as a float"
1199 );
1200 }
1201 }
1202
1203 #[test]
1204 fn parse_float_value_extremely_long_input_terminates() {
1205 let huge = "9".repeat(200_000);
1208 if let Ok(v) = parse_float_value(&huge) {
1210 assert!(v.get().is_finite(), "200k digits decoded to {}", v.get());
1211 }
1212
1213 let long_junk = "a".repeat(200_000);
1215 assert!(parse_float_value(&long_junk).is_err());
1216 }
1217
1218 #[test]
1219 fn parse_float_value_deeply_nested_input_does_not_stack_overflow() {
1220 let nested = "(".repeat(10_000);
1221 assert!(parse_float_value(&nested).is_err());
1222 let nested_pair = format!("{}5{}", "(".repeat(10_000), ")".repeat(10_000));
1223 assert!(parse_float_value(&nested_pair).is_err());
1224 }
1225
1226 #[test]
1229 fn parse_percentage_value_positive_control() {
1230 assert_eq!(parse_percentage_value("50%").unwrap().normalized(), 0.5);
1231 assert_eq!(parse_percentage_value("0%").unwrap().normalized(), 0.0);
1232 assert_eq!(parse_percentage_value("-25%").unwrap().normalized(), -0.25);
1233 assert_eq!(
1235 parse_percentage_value("0.5").unwrap(),
1236 parse_percentage_value("50%").unwrap()
1237 );
1238 }
1239
1240 #[test]
1241 fn parse_percentage_value_bare_number_is_multiplied_by_a_hundred() {
1242 assert_eq!(parse_percentage_value("50").unwrap().normalized(), 50.0);
1244 assert_ne!(
1245 parse_percentage_value("50").unwrap(),
1246 parse_percentage_value("50%").unwrap()
1247 );
1248 }
1249
1250 #[test]
1251 fn parse_percentage_value_rejects_empty_and_whitespace() {
1252 assert!(matches!(
1253 parse_percentage_value(""),
1254 Err(PercentageParseError::ValueParseErr(_))
1255 ));
1256 assert!(matches!(
1257 parse_percentage_value(" "),
1258 Err(PercentageParseError::ValueParseErr(_))
1259 ));
1260 assert!(matches!(
1261 parse_percentage_value("\t\n"),
1262 Err(PercentageParseError::ValueParseErr(_))
1263 ));
1264 assert!(parse_percentage_value("%").is_err());
1265 }
1266
1267 #[test]
1268 fn parse_percentage_value_rejects_garbage_without_panicking() {
1269 for input in [
1270 "abc", "fifty%", "%50", "50%%", "5 0 %", "--5%", "1.2.3%", ";", "\0", "NaN", "inf",
1271 "-inf",
1272 ] {
1273 assert!(
1274 parse_percentage_value(input).is_err(),
1275 "garbage input {input:?} was accepted"
1276 );
1277 }
1278 }
1279
1280 #[test]
1281 fn parse_percentage_value_reports_invalid_units() {
1282 for (input, unit) in [("50px", "px"), ("50em", "em"), ("1.5rem", "rem")] {
1283 match parse_percentage_value(input) {
1284 Err(PercentageParseError::InvalidUnit(u)) => assert_eq!(u.as_str(), unit),
1285 other => panic!("{input:?} should be InvalidUnit({unit:?}), got {other:?}"),
1286 }
1287 }
1288 }
1289
1290 #[test]
1291 fn parse_percentage_value_trims_leading_and_trailing_whitespace() {
1292 assert_eq!(
1293 parse_percentage_value(" 75.5% ").unwrap().normalized(),
1294 0.755
1295 );
1296 assert_eq!(parse_percentage_value("50 %").unwrap().normalized(), 0.5);
1298 }
1299
1300 #[test]
1301 fn parse_percentage_value_boundary_numbers_stay_finite() {
1302 let neg_zero = parse_percentage_value("-0%").unwrap();
1304 assert_eq!(neg_zero.normalized(), 0.0);
1305 assert!(neg_zero.normalized().is_sign_positive());
1306
1307 let huge = parse_percentage_value("1e400%").unwrap();
1309 assert!(
1310 huge.normalized().is_finite(),
1311 "1e400% leaked {}",
1312 huge.normalized()
1313 );
1314 let huge_neg = parse_percentage_value("-1e400%").unwrap();
1315 assert!(huge_neg.normalized().is_finite());
1316 assert_eq!(parse_percentage_value("1e-400%").unwrap().normalized(), 0.0);
1318
1319 let big = parse_percentage_value("9223372036854775807%").unwrap();
1321 assert!(big.normalized().is_finite());
1322 }
1323
1324 #[test]
1325 fn parse_percentage_value_ascii_unicode_neighbours_do_not_panic() {
1326 for input in [
1329 "\u{1F600}", "50\u{1F600}", "\u{20AC}50", "abc\u{00E9}%",
1333 "\u{200B}%", ] {
1335 assert!(
1336 parse_percentage_value(input).is_err(),
1337 "{input:?} was accepted"
1338 );
1339 }
1340 assert!(matches!(
1342 parse_percentage_value("50\u{1F600}"),
1343 Err(PercentageParseError::InvalidUnit(_))
1344 ));
1345 }
1346
1347 #[test]
1348 fn parse_percentage_value_extremely_long_input_terminates() {
1349 let huge = format!("{}%", "9".repeat(200_000));
1350 if let Ok(v) = parse_percentage_value(&huge) { assert!(v.normalized().is_finite()) }
1351 let long_junk = format!("{}%", "a".repeat(200_000));
1352 assert!(parse_percentage_value(&long_junk).is_err());
1353 }
1354
1355 #[test]
1356 fn parse_percentage_value_deeply_nested_input_does_not_stack_overflow() {
1357 assert!(parse_percentage_value(&"(".repeat(10_000)).is_err());
1358 let nested = format!("{}5%", "(".repeat(10_000));
1361 assert!(parse_percentage_value(&nested).is_err());
1362 }
1363
1364 #[test]
1367 fn percentage_parse_error_round_trips_through_owned() {
1368 let variants = [
1369 PercentageParseError::ValueParseErr(CssParseFloatError::Empty),
1370 PercentageParseError::ValueParseErr(CssParseFloatError::Invalid),
1371 PercentageParseError::NoPercentSign,
1372 PercentageParseError::InvalidUnit(String::new().into()),
1373 PercentageParseError::InvalidUnit("px".to_string().into()),
1374 PercentageParseError::InvalidUnit("\u{1F600}".to_string().into()),
1376 ];
1377 for e in variants {
1378 let round_tripped = e.to_contained().to_shared();
1379 assert_eq!(
1380 e, round_tripped,
1381 "to_contained/to_shared is not the identity for {e:?}"
1382 );
1383 }
1384 }
1385
1386 #[test]
1387 fn percentage_parse_error_owned_round_trips_through_shared() {
1388 let variants = [
1389 PercentageParseErrorOwned::ValueParseErr(CssParseFloatError::Invalid),
1390 PercentageParseErrorOwned::NoPercentSign,
1391 PercentageParseErrorOwned::InvalidUnit("vh".to_string().into()),
1392 ];
1393 for e in variants {
1394 assert_eq!(e.to_shared().to_contained(), e);
1395 }
1396 }
1397
1398 #[test]
1399 fn percentage_parse_error_display_is_non_empty() {
1400 for e in [
1403 PercentageParseError::ValueParseErr(CssParseFloatError::Empty),
1404 PercentageParseError::NoPercentSign,
1405 PercentageParseError::InvalidUnit(String::new().into()),
1406 ] {
1407 let shown = e.to_string();
1408 assert!(!shown.is_empty(), "{e:?} renders as an empty message");
1409 assert_eq!(format!("{e:?}"), shown);
1410 }
1411 }
1412
1413 #[test]
1420 fn known_bug_percentage_multibyte_numeric_char_panics() {
1421 for input in ["\u{00BD}%", "\u{0665}%", "5\u{00BD}", "\u{FF15}%"] {
1429 assert!(
1430 parse_percentage_value(input).is_err(),
1431 "{input:?} should be rejected"
1432 );
1433 }
1434 }
1435
1436 #[test]
1437 #[cfg(target_pointer_width = "64")]
1438 fn known_bug_const_new_fractional_huge_post_comma_escapes_the_fraction() {
1439 for post in [12_345_678_901_isize, 123_456_789_012, isize::MAX] {
1444 let frac = FloatValue::const_new_fractional(0, post).get();
1445 assert!(
1446 (0.0..1.0).contains(&frac),
1447 "const_new_fractional(0, {post}) produced {frac}, which is not a fraction"
1448 );
1449 }
1450 }
1451}