1use alloc::string::{String, ToString};
4use core::{fmt, num::ParseFloatError};
5use crate::corety::AzString;
6
7use crate::props::basic::error::ParseFloatErrorWithInput;
8
9use crate::props::{basic::length::FloatValue, formatter::PrintAsCssValue};
10
11#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
13#[repr(C)]
14#[derive(Default)]
15pub enum AngleMetric {
16 #[default]
17 Degree,
18 Radians,
19 Grad,
20 Turn,
21 Percent,
22}
23
24
25impl fmt::Display for AngleMetric {
26 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27 use self::AngleMetric::{Degree, Radians, Grad, Turn, Percent};
28 match self {
29 Degree => write!(f, "deg"),
30 Radians => write!(f, "rad"),
31 Grad => write!(f, "grad"),
32 Turn => write!(f, "turn"),
33 Percent => write!(f, "%"),
34 }
35 }
36}
37
38#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
40#[repr(C)]
41pub struct AngleValue {
42 pub metric: AngleMetric,
43 pub number: FloatValue,
44}
45
46impl_option!(
47 AngleValue,
48 OptionAngleValue,
49 [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
50);
51
52impl fmt::Debug for AngleValue {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 write!(f, "{self}")
55 }
56}
57
58impl fmt::Display for AngleValue {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 write!(f, "{}{}", self.number, self.metric)
61 }
62}
63
64impl PrintAsCssValue for AngleValue {
65 fn print_as_css_value(&self) -> String {
66 format!("{self}")
67 }
68}
69
70impl AngleValue {
71 #[inline]
73 #[must_use] pub const fn zero() -> Self {
74 const ZERO_DEG: AngleValue = AngleValue::const_deg(0);
75 ZERO_DEG
76 }
77
78 #[inline]
80 #[must_use] pub const fn const_deg(value: isize) -> Self {
81 Self::const_from_metric(AngleMetric::Degree, value)
82 }
83
84 #[inline]
86 #[must_use] pub const fn const_rad(value: isize) -> Self {
87 Self::const_from_metric(AngleMetric::Radians, value)
88 }
89
90 #[inline]
92 #[must_use] pub const fn const_grad(value: isize) -> Self {
93 Self::const_from_metric(AngleMetric::Grad, value)
94 }
95
96 #[inline]
98 #[must_use] pub const fn const_turn(value: isize) -> Self {
99 Self::const_from_metric(AngleMetric::Turn, value)
100 }
101
102 #[inline]
104 #[must_use] pub const fn const_percent(value: isize) -> Self {
105 Self::const_from_metric(AngleMetric::Percent, value)
106 }
107
108 #[inline]
110 #[must_use] pub const fn const_from_metric(metric: AngleMetric, value: isize) -> Self {
111 Self {
112 metric,
113 number: FloatValue::const_new(value),
114 }
115 }
116
117 #[inline]
124 #[must_use] pub const fn const_from_metric_fractional(metric: AngleMetric, pre_comma: isize, post_comma: isize) -> Self {
125 Self {
126 metric,
127 number: FloatValue::const_new_fractional(pre_comma, post_comma),
128 }
129 }
130
131 #[inline]
133 #[must_use] pub fn deg(value: f32) -> Self {
134 Self::from_metric(AngleMetric::Degree, value)
135 }
136
137 #[inline]
139 #[must_use] pub fn rad(value: f32) -> Self {
140 Self::from_metric(AngleMetric::Radians, value)
141 }
142
143 #[inline]
145 #[must_use] pub fn grad(value: f32) -> Self {
146 Self::from_metric(AngleMetric::Grad, value)
147 }
148
149 #[inline]
151 #[must_use] pub fn turn(value: f32) -> Self {
152 Self::from_metric(AngleMetric::Turn, value)
153 }
154
155 #[inline]
157 #[must_use] pub fn percent(value: f32) -> Self {
158 Self::from_metric(AngleMetric::Percent, value)
159 }
160
161 #[inline]
163 #[must_use] pub fn from_metric(metric: AngleMetric, value: f32) -> Self {
164 Self {
165 metric,
166 number: FloatValue::new(value),
167 }
168 }
169
170 #[inline]
174 #[must_use] pub fn to_degrees(&self) -> f32 {
175 let mut val = self.to_degrees_raw() % 360.0;
176 if val < 0.0 {
177 val += 360.0;
178 }
179 val
180 }
181
182 #[inline]
185 #[must_use] pub fn to_degrees_raw(&self) -> f32 {
186 match self.metric {
187 AngleMetric::Degree => self.number.get(),
188 AngleMetric::Grad => self.number.get() / 400.0 * 360.0,
189 AngleMetric::Radians => self.number.get().to_degrees(),
190 AngleMetric::Turn => self.number.get() * 360.0,
191 AngleMetric::Percent => self.number.get() / 100.0 * 360.0,
192 }
193 }
194}
195
196#[derive(Clone, PartialEq, Eq)]
200pub enum CssAngleValueParseError<'a> {
201 EmptyString,
202 NoValueGiven(&'a str, AngleMetric),
203 ValueParseErr(ParseFloatError, &'a str),
204 InvalidAngle(&'a str),
205}
206
207impl_debug_as_display!(CssAngleValueParseError<'a>);
208impl_display! { CssAngleValueParseError<'a>, {
209 EmptyString => format!("Missing [rad / deg / turn / %] value"),
210 NoValueGiven(input, metric) => format!("Expected floating-point angle value, got: \"{}{}\"", input, metric),
211 ValueParseErr(err, number_str) => format!("Could not parse \"{}\" as floating-point value: \"{}\"", number_str, err),
212 InvalidAngle(s) => format!("Invalid angle value: \"{}\"", s),
213}}
214
215#[derive(Debug, Clone, PartialEq, Eq)]
217#[repr(C)]
218pub struct AngleNoValueGivenError {
219 pub value: AzString,
220 pub metric: AngleMetric,
221}
222
223#[derive(Debug, Clone, PartialEq, Eq)]
225#[repr(C, u8)]
226pub enum CssAngleValueParseErrorOwned {
227 EmptyString,
228 NoValueGiven(AngleNoValueGivenError),
229 ValueParseErr(ParseFloatErrorWithInput),
230 InvalidAngle(AzString),
231}
232
233impl CssAngleValueParseError<'_> {
234 #[must_use] pub fn to_contained(&self) -> CssAngleValueParseErrorOwned {
235 match self {
236 CssAngleValueParseError::EmptyString => CssAngleValueParseErrorOwned::EmptyString,
237 CssAngleValueParseError::NoValueGiven(s, metric) => {
238 CssAngleValueParseErrorOwned::NoValueGiven(AngleNoValueGivenError { value: (*s).to_string().into(), metric: *metric })
239 }
240 CssAngleValueParseError::ValueParseErr(err, s) => {
241 CssAngleValueParseErrorOwned::ValueParseErr(ParseFloatErrorWithInput { error: err.clone().into(), input: (*s).to_string().into() })
242 }
243 CssAngleValueParseError::InvalidAngle(s) => {
244 CssAngleValueParseErrorOwned::InvalidAngle((*s).to_string().into())
245 }
246 }
247 }
248}
249
250impl CssAngleValueParseErrorOwned {
251 #[must_use] pub fn to_shared(&self) -> CssAngleValueParseError<'_> {
252 match self {
253 Self::EmptyString => CssAngleValueParseError::EmptyString,
254 Self::NoValueGiven(e) => {
255 CssAngleValueParseError::NoValueGiven(e.value.as_str(), e.metric)
256 }
257 Self::ValueParseErr(e) => {
258 CssAngleValueParseError::ValueParseErr(e.error.to_std(), e.input.as_str())
259 }
260 Self::InvalidAngle(s) => {
261 CssAngleValueParseError::InvalidAngle(s.as_str())
262 }
263 }
264 }
265}
266
267#[cfg(feature = "parser")]
270pub fn parse_angle_value(input: &str) -> Result<AngleValue, CssAngleValueParseError<'_>> {
274 let input = input.trim();
275
276 if input.is_empty() {
277 return Err(CssAngleValueParseError::EmptyString);
278 }
279
280 let match_values = &[
281 ("deg", AngleMetric::Degree),
282 ("turn", AngleMetric::Turn),
283 ("grad", AngleMetric::Grad),
284 ("rad", AngleMetric::Radians),
285 ("%", AngleMetric::Percent),
286 ];
287
288 for (match_val, metric) in match_values {
289 if let Some(value) = input.strip_suffix(match_val) {
290 let value = value.trim();
291 if value.is_empty() {
292 return Err(CssAngleValueParseError::NoValueGiven(input, *metric));
293 }
294 match value.parse::<f32>() {
295 Ok(o) => return Ok(AngleValue::from_metric(*metric, o)),
296 Err(e) => return Err(CssAngleValueParseError::ValueParseErr(e, value)),
297 }
298 }
299 }
300
301 input.parse::<f32>().map_or_else(
303 |_| Err(CssAngleValueParseError::InvalidAngle(input)),
304 |o| Ok(AngleValue::from_metric(AngleMetric::Degree, o)),
305 )
306}
307
308#[cfg(all(test, feature = "parser"))]
309mod tests {
310 #![allow(clippy::float_cmp, clippy::approx_constant)]
313 use super::*;
314
315 #[test]
316 fn test_parse_angle_value_deg() {
317 assert_eq!(parse_angle_value("90deg").unwrap(), AngleValue::deg(90.0));
318 assert_eq!(
319 parse_angle_value("-45.5deg").unwrap(),
320 AngleValue::deg(-45.5)
321 );
322 assert_eq!(parse_angle_value("180").unwrap(), AngleValue::deg(180.0));
324 }
325
326 #[test]
327 fn test_parse_angle_value_rad() {
328 assert_eq!(parse_angle_value("1.57rad").unwrap(), AngleValue::rad(1.57));
329 assert_eq!(
330 parse_angle_value(" -3.14rad ").unwrap(),
331 AngleValue::rad(-3.14)
332 );
333 }
334
335 #[test]
336 fn test_parse_angle_value_grad() {
337 assert_eq!(
338 parse_angle_value("100grad").unwrap(),
339 AngleValue::grad(100.0)
340 );
341 assert_eq!(
342 parse_angle_value("400grad").unwrap(),
343 AngleValue::grad(400.0)
344 );
345 }
346
347 #[test]
348 fn test_parse_angle_value_turn() {
349 assert_eq!(
350 parse_angle_value("0.25turn").unwrap(),
351 AngleValue::turn(0.25)
352 );
353 assert_eq!(parse_angle_value("1turn").unwrap(), AngleValue::turn(1.0));
354 }
355
356 #[test]
357 fn test_parse_angle_value_percent() {
358 assert_eq!(parse_angle_value("50%").unwrap(), AngleValue::percent(50.0));
359 }
360
361 #[test]
362 fn test_parse_angle_value_errors() {
363 assert!(parse_angle_value("").is_err());
364 assert!(parse_angle_value("deg").is_err());
365 assert!(parse_angle_value("90 degs").is_err());
366 assert!(parse_angle_value("ninety-deg").is_err());
367 assert!(parse_angle_value("1.57 rads").is_err());
368 }
369
370 #[test]
371 fn test_to_degrees_conversion() {
372 assert_eq!(AngleValue::deg(90.0).to_degrees(), 90.0);
373 assert!((AngleValue::rad(core::f32::consts::PI).to_degrees() - 180.0).abs() < 0.1);
375 assert_eq!(AngleValue::grad(100.0).to_degrees(), 90.0);
376 assert_eq!(AngleValue::turn(0.5).to_degrees(), 180.0);
377 assert_eq!(AngleValue::deg(-90.0).to_degrees(), 270.0);
378 assert_eq!(AngleValue::deg(450.0).to_degrees(), 90.0);
379 }
380}
381
382#[cfg(test)]
383#[allow(
384 clippy::float_cmp,
385 clippy::unreadable_literal,
386 clippy::cast_precision_loss,
387 clippy::too_many_lines
388)]
389mod autotest_generated {
390 use super::*;
391 use crate::props::basic::error::{
392 ParseFloatError as FfiParseFloatError, ParseFloatErrorWithInput,
393 };
394
395 const MULT: isize = 1000;
397
398 const ALL_METRICS: [AngleMetric; 5] = [
400 AngleMetric::Degree,
401 AngleMetric::Radians,
402 AngleMetric::Grad,
403 AngleMetric::Turn,
404 AngleMetric::Percent,
405 ];
406
407 #[test]
412 fn autotest_angle_metric_display_is_non_empty_and_exact() {
413 assert_eq!(AngleMetric::Degree.to_string(), "deg");
414 assert_eq!(AngleMetric::Radians.to_string(), "rad");
415 assert_eq!(AngleMetric::Grad.to_string(), "grad");
416 assert_eq!(AngleMetric::Turn.to_string(), "turn");
417 assert_eq!(AngleMetric::Percent.to_string(), "%");
418 assert_eq!(AngleMetric::default(), AngleMetric::Degree);
419 for m in ALL_METRICS {
420 assert!(!m.to_string().is_empty(), "empty unit string for {m:?}");
421 }
422 }
423
424 #[test]
425 fn autotest_angle_value_display_default_and_zero() {
426 assert_eq!(AngleValue::default().to_string(), "0deg");
427 assert_eq!(AngleValue::zero().to_string(), "0deg");
428 assert_eq!(format!("{:?}", AngleValue::zero()), "0deg");
430 assert_eq!(AngleValue::zero().print_as_css_value(), "0deg");
431 }
432
433 #[test]
434 fn autotest_angle_value_display_never_emits_inf_or_nan() {
435 for m in ALL_METRICS {
438 for v in [
439 f32::NAN,
440 f32::INFINITY,
441 f32::NEG_INFINITY,
442 f32::MAX,
443 f32::MIN,
444 f32::MIN_POSITIVE,
445 -0.0,
446 ] {
447 let s = AngleValue::from_metric(m, v).to_string();
448 assert!(!s.is_empty(), "empty serialization for {m:?} / {v}");
449 assert!(!s.contains("inf"), "serialized infinity: {s}");
450 assert!(!s.contains("NaN"), "serialized NaN: {s}");
451 assert!(s.ends_with(&m.to_string()), "lost the unit suffix: {s}");
452 }
453 }
454 }
455
456 #[test]
457 fn autotest_angle_value_nan_serializes_as_zero() {
458 assert_eq!(AngleValue::deg(f32::NAN).to_string(), "0deg");
460 assert_eq!(AngleValue::rad(f32::NAN).to_string(), "0rad");
461 }
462
463 #[test]
468 fn autotest_zero_is_the_neutral_element() {
469 let z = AngleValue::zero();
470 assert_eq!(z, AngleValue::default());
471 assert_eq!(z, AngleValue::const_deg(0));
472 assert_eq!(z, AngleValue::deg(0.0));
473 assert_eq!(z.metric, AngleMetric::Degree);
474 assert_eq!(z.number.number(), 0);
475 assert_eq!(z.number.get(), 0.0);
476 assert_eq!(z.to_degrees(), 0.0);
477 assert_eq!(z.to_degrees_raw(), 0.0);
478 }
479
480 #[test]
481 fn autotest_from_metric_fields_match_args() {
482 for m in ALL_METRICS {
483 let a = AngleValue::from_metric(m, 12.5);
484 assert_eq!(a.metric, m);
485 assert_eq!(a.number.get(), 12.5);
486 assert_eq!(a.number.number(), 12_500);
487 }
488 assert_eq!(
490 AngleValue::deg(1.5),
491 AngleValue::from_metric(AngleMetric::Degree, 1.5)
492 );
493 assert_eq!(
494 AngleValue::rad(1.5),
495 AngleValue::from_metric(AngleMetric::Radians, 1.5)
496 );
497 assert_eq!(
498 AngleValue::grad(1.5),
499 AngleValue::from_metric(AngleMetric::Grad, 1.5)
500 );
501 assert_eq!(
502 AngleValue::turn(1.5),
503 AngleValue::from_metric(AngleMetric::Turn, 1.5)
504 );
505 assert_eq!(
506 AngleValue::percent(1.5),
507 AngleValue::from_metric(AngleMetric::Percent, 1.5)
508 );
509 }
510
511 #[test]
516 fn autotest_const_ctors_zero_negative_and_metric() {
517 for (built, metric) in [
518 (AngleValue::const_deg(0), AngleMetric::Degree),
519 (AngleValue::const_rad(0), AngleMetric::Radians),
520 (AngleValue::const_grad(0), AngleMetric::Grad),
521 (AngleValue::const_turn(0), AngleMetric::Turn),
522 (AngleValue::const_percent(0), AngleMetric::Percent),
523 ] {
524 assert_eq!(built.metric, metric);
525 assert_eq!(built.number.number(), 0);
526 }
527 assert_eq!(AngleValue::const_deg(-90).number.get(), -90.0);
528 assert_eq!(AngleValue::const_rad(-3).number.number(), -3 * MULT);
529 assert_eq!(AngleValue::const_turn(-1).to_degrees_raw(), -360.0);
530 }
531
532 #[test]
533 fn autotest_const_from_metric_matches_specific_ctors() {
534 for (m, specific) in [
535 (AngleMetric::Degree, AngleValue::const_deg(7)),
536 (AngleMetric::Radians, AngleValue::const_rad(7)),
537 (AngleMetric::Grad, AngleValue::const_grad(7)),
538 (AngleMetric::Turn, AngleValue::const_turn(7)),
539 (AngleMetric::Percent, AngleValue::const_percent(7)),
540 ] {
541 assert_eq!(AngleValue::const_from_metric(m, 7), specific);
542 assert_eq!(specific.number.number(), 7 * MULT);
543 }
544 }
545
546 #[test]
547 fn autotest_const_ctors_at_safe_isize_boundary() {
548 const MAX_SAFE: isize = isize::MAX / MULT;
551 const MIN_SAFE: isize = isize::MIN / MULT;
552
553 assert_eq!(
554 AngleValue::const_deg(MAX_SAFE).number.number(),
555 MAX_SAFE * MULT
556 );
557 assert_eq!(
558 AngleValue::const_deg(MIN_SAFE).number.number(),
559 MIN_SAFE * MULT
560 );
561 assert!(AngleValue::const_deg(MAX_SAFE).number.get().is_finite());
563 assert!(AngleValue::const_deg(MIN_SAFE).number.get().is_finite());
564 assert!(AngleValue::const_turn(MAX_SAFE).to_degrees().is_finite());
565 assert!(AngleValue::const_turn(MIN_SAFE).to_degrees().is_finite());
566 }
567
568 #[test]
569 fn autotest_const_deg_isize_max_overflows_unchecked() {
570 let huge = core::hint::black_box(isize::MAX);
575 let prev = std::panic::take_hook();
576 std::panic::set_hook(Box::new(|_| {}));
577 let res = std::panic::catch_unwind(move || AngleValue::const_deg(huge).number.number());
578 std::panic::set_hook(prev);
579
580 match res {
581 Err(_) => {} Ok(n) => assert_eq!(
583 n,
584 isize::MAX.wrapping_mul(MULT),
585 "release build must wrap, not produce a sanitized value"
586 ),
587 }
588 }
589
590 #[test]
591 fn autotest_const_from_metric_fractional_digit_truncation() {
592 let f = |pre, post| {
593 AngleValue::const_from_metric_fractional(AngleMetric::Degree, pre, post)
594 .number
595 .number()
596 };
597 assert_eq!(f(0, 0), 0);
598 assert_eq!(f(45, 5), 45_500); assert_eq!(f(0, 83), 830); assert_eq!(f(1, 523), 1_523); assert_eq!(f(2, 123456), 2_123); assert_eq!(f(0, 999_999_999), 999); assert_eq!(
605 AngleValue::const_from_metric_fractional(AngleMetric::Turn, 0, 25).metric,
606 AngleMetric::Turn
607 );
608 }
609
610 #[test]
611 fn autotest_const_fractional_sign_handling_and_negative_zero_trap() {
612 let deg = |pre, post| {
613 AngleValue::const_from_metric_fractional(AngleMetric::Degree, pre, post)
614 .number
615 .get()
616 };
617 assert_eq!(deg(-1, 5), -1.5); assert_eq!(deg(0, -5), -0.5); assert_eq!(deg(-1, -5), -1.5); assert_eq!(deg(-0, 5), 0.5);
623 assert_ne!(deg(-0, 5), -0.5);
624 }
625
626 #[test]
631 fn autotest_f32_ctor_nan_collapses_to_zero() {
632 for m in ALL_METRICS {
633 let a = AngleValue::from_metric(m, f32::NAN);
634 assert_eq!(a.number.number(), 0, "NaN did not clamp to 0 for {m:?}");
635 assert_eq!(a.number.get(), 0.0);
636 assert!(!a.number.get().is_nan());
637 assert_eq!(a.to_degrees(), 0.0);
638 assert_eq!(a.to_degrees_raw(), 0.0);
639 assert_eq!(a, AngleValue::from_metric(m, 0.0));
641 }
642 }
643
644 #[test]
645 fn autotest_f32_ctor_infinities_saturate_to_isize_bounds() {
646 assert_eq!(AngleValue::deg(f32::INFINITY).number.number(), isize::MAX);
647 assert_eq!(
648 AngleValue::deg(f32::NEG_INFINITY).number.number(),
649 isize::MIN
650 );
651 assert_eq!(AngleValue::deg(f32::MAX).number.number(), isize::MAX);
653 assert_eq!(AngleValue::deg(f32::MIN).number.number(), isize::MIN);
654 for m in ALL_METRICS {
655 for v in [f32::INFINITY, f32::NEG_INFINITY, f32::MAX, f32::MIN] {
656 let a = AngleValue::from_metric(m, v);
657 assert!(a.number.get().is_finite(), "{m:?} / {v} leaked a non-finite");
658 }
659 }
660 }
661
662 #[test]
663 fn autotest_f32_ctor_truncates_toward_zero_below_precision() {
664 assert_eq!(AngleValue::deg(1.9999).number.number(), 1_999);
667 assert_eq!(AngleValue::deg(-1.9999).number.number(), -1_999);
668 assert_eq!(AngleValue::deg(0.0004).number.number(), 0);
669 assert_eq!(AngleValue::deg(-0.0004).number.number(), 0);
670 assert_eq!(AngleValue::deg(f32::EPSILON).number.number(), 0);
671 assert_eq!(AngleValue::deg(f32::MIN_POSITIVE).number.number(), 0);
672 assert_eq!(AngleValue::deg(-0.0).number.number(), 0);
674 assert_eq!(AngleValue::deg(-0.0), AngleValue::deg(0.0));
675 }
676
677 #[test]
682 fn autotest_to_degrees_known_conversions() {
683 assert_eq!(AngleValue::deg(90.0).to_degrees(), 90.0);
684 assert_eq!(AngleValue::grad(100.0).to_degrees(), 90.0);
685 assert_eq!(AngleValue::turn(0.25).to_degrees(), 90.0);
686 assert_eq!(AngleValue::percent(50.0).to_degrees(), 180.0);
687 assert_eq!(AngleValue::percent(25.0).to_degrees_raw(), 90.0);
688 assert!((AngleValue::rad(core::f32::consts::FRAC_PI_2).to_degrees() - 90.0).abs() < 0.1);
690 }
691
692 #[test]
693 fn autotest_to_degrees_normalizes_but_raw_does_not() {
694 assert_eq!(AngleValue::deg(360.0).to_degrees(), 0.0);
696 assert_eq!(AngleValue::turn(1.0).to_degrees(), 0.0);
697 assert_eq!(AngleValue::grad(400.0).to_degrees(), 0.0);
698 assert_eq!(AngleValue::percent(100.0).to_degrees(), 0.0);
699 assert_eq!(AngleValue::deg(360.0).to_degrees_raw(), 360.0);
701 assert_eq!(AngleValue::turn(1.0).to_degrees_raw(), 360.0);
702 assert_eq!(AngleValue::grad(400.0).to_degrees_raw(), 360.0);
703 assert_eq!(AngleValue::percent(100.0).to_degrees_raw(), 360.0);
704
705 assert_eq!(AngleValue::deg(-90.0).to_degrees(), 270.0);
707 assert_eq!(AngleValue::deg(-450.0).to_degrees(), 270.0);
708 assert_eq!(AngleValue::deg(-0.5).to_degrees(), 359.5);
709 assert_eq!(AngleValue::deg(720.0).to_degrees(), 0.0);
710 assert_eq!(AngleValue::deg(450.0).to_degrees_raw(), 450.0);
711 assert_eq!(AngleValue::turn(-2.0).to_degrees(), 0.0);
712 }
713
714 #[test]
715 fn autotest_to_degrees_on_saturated_values_stays_finite_and_in_range() {
716 for m in ALL_METRICS {
720 for v in [f32::INFINITY, f32::NEG_INFINITY, f32::MAX, f32::MIN, f32::NAN] {
721 let a = AngleValue::from_metric(m, v);
722 let raw = a.to_degrees_raw();
723 let norm = a.to_degrees();
724 assert!(raw.is_finite(), "to_degrees_raw not finite: {m:?} / {v}");
725 assert!(norm.is_finite(), "to_degrees not finite: {m:?} / {v}");
726 assert!(
727 (0.0..360.0).contains(&norm),
728 "to_degrees out of [0,360): {m:?} / {v} -> {norm}"
729 );
730 }
731 }
732 }
733
734 #[test]
735 fn autotest_ord_is_metric_first_not_semantic_angle() {
736 assert!(AngleValue::deg(1000.0) < AngleValue::rad(0.0));
739 assert!(AngleValue::turn(0.0) < AngleValue::percent(0.0));
740 assert!(AngleValue::deg(-1.0) < AngleValue::deg(1.0));
742 use core::hash::{Hash, Hasher};
744 let h = |a: AngleValue| {
745 let mut s = std::collections::hash_map::DefaultHasher::new();
746 a.hash(&mut s);
747 s.finish()
748 };
749 assert_eq!(AngleValue::deg(1.0), AngleValue::deg(1.0));
750 assert_eq!(h(AngleValue::deg(1.0)), h(AngleValue::deg(1.0)));
751 assert_eq!(h(AngleValue::deg(f32::NAN)), h(AngleValue::deg(0.0)));
752 assert_ne!(AngleValue::deg(1.0), AngleValue::rad(1.0));
753 }
754
755 #[cfg(feature = "parser")]
760 #[test]
761 fn autotest_parse_empty_and_whitespace_only() {
762 for input in ["", " ", "\t\n\r", "\u{a0}", " \u{2003} "] {
763 assert!(
764 matches!(
765 parse_angle_value(input),
766 Err(CssAngleValueParseError::EmptyString)
767 ),
768 "expected EmptyString for {input:?}"
769 );
770 }
771 }
772
773 #[cfg(feature = "parser")]
774 #[test]
775 fn autotest_parse_unit_without_number() {
776 for (input, metric) in [
777 ("deg", AngleMetric::Degree),
778 ("rad", AngleMetric::Radians),
779 ("grad", AngleMetric::Grad),
780 ("turn", AngleMetric::Turn),
781 ("%", AngleMetric::Percent),
782 (" deg ", AngleMetric::Degree),
783 ] {
784 match parse_angle_value(input) {
785 Err(CssAngleValueParseError::NoValueGiven(_, m)) => assert_eq!(m, metric),
786 other => panic!("expected NoValueGiven for {input:?}, got {other:?}"),
787 }
788 }
789 }
790
791 #[cfg(feature = "parser")]
792 #[test]
793 fn autotest_parse_garbage_is_rejected_without_panicking() {
794 for input in [
795 "ninety",
796 "!!!",
797 "90 degs",
798 "1.57 rads",
799 "90degdeg",
800 "--90deg",
801 "1_0deg",
802 "90;garbage",
803 "deg90",
804 "%50",
805 "0x1Fdeg",
806 "+-1turn",
807 "9 0deg",
808 "\0deg",
809 ] {
810 let res = parse_angle_value(input);
811 assert!(res.is_err(), "garbage accepted: {input:?} -> {res:?}");
812 assert!(!format!("{}", res.unwrap_err()).is_empty());
814 }
815 }
816
817 #[cfg(feature = "parser")]
818 #[test]
819 fn autotest_parse_uppercase_units_are_rejected() {
820 for input in ["90DEG", "1RAD", "0.5TURN", "100GRAD", "90Deg"] {
823 assert!(
824 parse_angle_value(input).is_err(),
825 "case-insensitive unit unexpectedly accepted: {input:?}"
826 );
827 }
828 }
829
830 #[cfg(feature = "parser")]
831 #[test]
832 fn autotest_parse_accepts_whitespace_between_number_and_unit() {
833 assert_eq!(parse_angle_value("90 deg").unwrap(), AngleValue::deg(90.0));
836 assert_eq!(parse_angle_value("90\tdeg").unwrap(), AngleValue::deg(90.0));
837 assert_eq!(
838 parse_angle_value("50 %").unwrap(),
839 AngleValue::percent(50.0)
840 );
841 assert_eq!(parse_angle_value(" 90deg ").unwrap(), AngleValue::deg(90.0));
842 }
843
844 #[cfg(feature = "parser")]
845 #[test]
846 fn autotest_parse_accepts_float_keywords_and_neutralizes_them() {
847 let nan = parse_angle_value("NaN").expect("f32::from_str accepts NaN");
850 assert_eq!(nan.number.number(), 0);
851 assert!(!nan.to_degrees().is_nan());
852
853 let nan_rad = parse_angle_value("nanrad").expect("f32::from_str accepts nan");
854 assert_eq!(nan_rad.metric, AngleMetric::Radians);
855 assert_eq!(nan_rad.number.number(), 0);
856
857 let inf = parse_angle_value("inf").expect("f32::from_str accepts inf");
858 assert_eq!(inf.number.number(), isize::MAX);
859 assert!(inf.to_degrees().is_finite());
860
861 let neg_inf = parse_angle_value("-infdeg").expect("f32::from_str accepts -inf");
862 assert_eq!(neg_inf.number.number(), isize::MIN);
863 assert!(neg_inf.to_degrees_raw().is_finite());
864 }
865
866 #[cfg(feature = "parser")]
867 #[test]
868 fn autotest_parse_boundary_numbers_saturate() {
869 assert_eq!(parse_angle_value("0").unwrap(), AngleValue::deg(0.0));
870 assert_eq!(parse_angle_value("-0").unwrap().number.number(), 0);
871 assert_eq!(parse_angle_value("+90deg").unwrap(), AngleValue::deg(90.0));
872 assert_eq!(parse_angle_value(".5turn").unwrap(), AngleValue::turn(0.5));
873 assert_eq!(
876 parse_angle_value("9223372036854775807").unwrap().number.number(),
877 isize::MAX
878 );
879 assert_eq!(
880 parse_angle_value("-9223372036854775808").unwrap().number.number(),
881 isize::MIN
882 );
883 assert_eq!(parse_angle_value("1e40deg").unwrap().number.number(), isize::MAX);
885 assert_eq!(parse_angle_value("1e-40deg").unwrap().number.number(), 0);
886 assert_eq!(parse_angle_value("0.0001deg").unwrap().number.number(), 0);
887 }
888
889 #[cfg(feature = "parser")]
890 #[test]
891 fn autotest_parse_extremely_long_input_terminates() {
892 let long_digits = "9".repeat(100_000) + "deg";
894 assert_eq!(
895 parse_angle_value(&long_digits).unwrap().number.number(),
896 isize::MAX
897 );
898
899 let padded = "0".repeat(100_000) + "1deg";
901 assert_eq!(parse_angle_value(&padded).unwrap(), AngleValue::deg(1.0));
902
903 let long_junk = "a".repeat(100_000);
905 assert!(parse_angle_value(&long_junk).is_err());
906 }
907
908 #[cfg(feature = "parser")]
909 #[test]
910 fn autotest_parse_deeply_nested_brackets_does_not_stack_overflow() {
911 let nested = "(".repeat(10_000);
913 assert!(parse_angle_value(&nested).is_err());
914 let nested_unit = "[".repeat(10_000) + "deg";
915 assert!(parse_angle_value(&nested_unit).is_err());
916 }
917
918 #[cfg(feature = "parser")]
919 #[test]
920 fn autotest_parse_unicode_input_never_panics() {
921 for input in [
923 "°",
924 "90°",
925 "\u{1F600}",
926 "\u{1F600}deg",
927 "90deg", "9\u{0301}0deg", "٩٠%", "\u{200b}90deg", "90de\u{0261}", ] {
933 let res = parse_angle_value(input);
934 assert!(res.is_err(), "unicode garbage accepted: {input:?} -> {res:?}");
935 assert!(!format!("{}", res.unwrap_err()).is_empty());
936 }
937 }
938
939 #[cfg(feature = "parser")]
940 #[test]
941 fn autotest_parse_valid_minimal_positive_control() {
942 assert_eq!(parse_angle_value("1deg").unwrap(), AngleValue::deg(1.0));
943 assert_eq!(parse_angle_value("0").unwrap(), AngleValue::zero());
944 }
945
946 #[cfg(feature = "parser")]
951 #[test]
952 fn autotest_round_trip_display_then_parse_all_metrics() {
953 for m in ALL_METRICS {
956 for v in [
957 0.0_f32, 1.0, -1.0, 0.5, -0.25, 45.5, 90.0, 180.0, 359.0, 1000.0,
958 ] {
959 let angle = AngleValue::from_metric(m, v);
960 let printed = angle.to_string();
961 let reparsed = parse_angle_value(&printed)
962 .unwrap_or_else(|e| panic!("cannot re-parse own output {printed:?}: {e}"));
963 assert_eq!(reparsed, angle, "round-trip changed value: {printed:?}");
964 assert_eq!(reparsed.metric, m, "round-trip changed unit: {printed:?}");
965 assert_eq!(angle.print_as_css_value(), printed);
967 }
968 }
969 }
970
971 #[cfg(feature = "parser")]
972 #[test]
973 fn autotest_round_trip_metric_suffix_is_unambiguous() {
974 for m in ALL_METRICS {
976 let parsed = parse_angle_value(&format!("1{m}")).unwrap();
977 assert_eq!(parsed.metric, m, "unit {m} did not round-trip");
978 assert_eq!(parsed.number.get(), 1.0);
979 }
980 assert_eq!(parse_angle_value("1grad").unwrap().metric, AngleMetric::Grad);
981 assert_eq!(
982 parse_angle_value("1rad").unwrap().metric,
983 AngleMetric::Radians
984 );
985 }
986
987 #[cfg(feature = "parser")]
988 #[test]
989 fn autotest_round_trip_quantization_is_idempotent() {
990 for v in [0.0_f32, 1.0, -1.0, 0.5, -0.25, 45.5, 359.0] {
993 let once = AngleValue::deg(v);
994 let twice = AngleValue::deg(once.number.get());
995 assert_eq!(once, twice, "quantization not idempotent for {v}");
996 }
997 }
998
999 #[cfg(feature = "parser")]
1004 #[test]
1005 fn autotest_error_owned_round_trip_from_real_parse_failures() {
1006 for input in ["", "deg", "%", "xdeg", "zzz", "\u{1F600}rad"] {
1007 let err = parse_angle_value(input).unwrap_err();
1008 let owned = err.to_contained();
1009 assert_eq!(
1010 owned.to_shared(),
1011 err,
1012 "to_contained/to_shared lost information for {input:?}"
1013 );
1014 assert!(!format!("{err}").is_empty());
1016 assert!(!format!("{owned:?}").is_empty());
1017 }
1018 }
1019
1020 #[cfg(feature = "parser")]
1021 #[test]
1022 fn autotest_error_variants_are_the_expected_ones() {
1023 assert!(matches!(
1024 parse_angle_value("").unwrap_err(),
1025 CssAngleValueParseError::EmptyString
1026 ));
1027 assert!(matches!(
1028 parse_angle_value("turn").unwrap_err(),
1029 CssAngleValueParseError::NoValueGiven(_, AngleMetric::Turn)
1030 ));
1031 assert!(matches!(
1032 parse_angle_value("xdeg").unwrap_err(),
1033 CssAngleValueParseError::ValueParseErr(_, "x")
1034 ));
1035 assert!(matches!(
1036 parse_angle_value("zzz").unwrap_err(),
1037 CssAngleValueParseError::InvalidAngle("zzz")
1038 ));
1039 }
1040
1041 #[test]
1042 fn autotest_error_to_shared_handles_empty_and_extreme_payloads() {
1043 let cases = [
1046 CssAngleValueParseErrorOwned::EmptyString,
1047 CssAngleValueParseErrorOwned::NoValueGiven(AngleNoValueGivenError {
1048 value: String::new().into(),
1049 metric: AngleMetric::Percent,
1050 }),
1051 CssAngleValueParseErrorOwned::ValueParseErr(ParseFloatErrorWithInput {
1052 error: FfiParseFloatError::Empty,
1053 input: String::new().into(),
1054 }),
1055 CssAngleValueParseErrorOwned::ValueParseErr(ParseFloatErrorWithInput {
1056 error: FfiParseFloatError::Invalid,
1057 input: "\u{1F600}".to_string().into(),
1058 }),
1059 CssAngleValueParseErrorOwned::InvalidAngle(String::new().into()),
1060 CssAngleValueParseErrorOwned::InvalidAngle("\u{1F600}\u{0301}".to_string().into()),
1061 ];
1062 for owned in cases {
1063 let shared = owned.to_shared();
1064 assert!(!format!("{shared}").is_empty());
1065 assert_eq!(shared.to_contained(), owned);
1067 }
1068 }
1069}