1use crate::corety::AzString;
4use alloc::string::{String, ToString};
5use core::{fmt, num::ParseFloatError};
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
24impl fmt::Display for AngleMetric {
25 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26 use self::AngleMetric::{Degree, Grad, Percent, Radians, Turn};
27 match self {
28 Degree => write!(f, "deg"),
29 Radians => write!(f, "rad"),
30 Grad => write!(f, "grad"),
31 Turn => write!(f, "turn"),
32 Percent => write!(f, "%"),
33 }
34 }
35}
36
37#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
39#[repr(C)]
40pub struct AngleValue {
41 pub metric: AngleMetric,
42 pub number: FloatValue,
43}
44
45impl_option!(
46 AngleValue,
47 OptionAngleValue,
48 [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
49);
50
51impl fmt::Debug for AngleValue {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 write!(f, "{self}")
54 }
55}
56
57impl fmt::Display for AngleValue {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 write!(f, "{}{}", self.number, self.metric)
60 }
61}
62
63impl PrintAsCssValue for AngleValue {
64 fn print_as_css_value(&self) -> String {
65 format!("{self}")
66 }
67}
68
69impl AngleValue {
70 #[inline]
72 #[must_use]
73 pub const fn zero() -> Self {
74 const ZERO_DEG: AngleValue = AngleValue::const_deg(0);
75 ZERO_DEG
76 }
77
78 #[inline]
80 #[must_use]
81 pub const fn const_deg(value: isize) -> Self {
82 Self::const_from_metric(AngleMetric::Degree, value)
83 }
84
85 #[inline]
87 #[must_use]
88 pub const fn const_rad(value: isize) -> Self {
89 Self::const_from_metric(AngleMetric::Radians, value)
90 }
91
92 #[inline]
94 #[must_use]
95 pub const fn const_grad(value: isize) -> Self {
96 Self::const_from_metric(AngleMetric::Grad, value)
97 }
98
99 #[inline]
101 #[must_use]
102 pub const fn const_turn(value: isize) -> Self {
103 Self::const_from_metric(AngleMetric::Turn, value)
104 }
105
106 #[inline]
108 #[must_use]
109 pub const fn const_percent(value: isize) -> Self {
110 Self::const_from_metric(AngleMetric::Percent, value)
111 }
112
113 #[inline]
115 #[must_use]
116 pub const fn const_from_metric(metric: AngleMetric, value: isize) -> Self {
117 Self {
118 metric,
119 number: FloatValue::const_new(value),
120 }
121 }
122
123 #[inline]
130 #[must_use]
131 pub const fn const_from_metric_fractional(
132 metric: AngleMetric,
133 pre_comma: isize,
134 post_comma: isize,
135 ) -> Self {
136 Self {
137 metric,
138 number: FloatValue::const_new_fractional(pre_comma, post_comma),
139 }
140 }
141
142 #[inline]
144 #[must_use]
145 pub fn deg(value: f32) -> Self {
146 Self::from_metric(AngleMetric::Degree, value)
147 }
148
149 #[inline]
151 #[must_use]
152 pub fn rad(value: f32) -> Self {
153 Self::from_metric(AngleMetric::Radians, value)
154 }
155
156 #[inline]
158 #[must_use]
159 pub fn grad(value: f32) -> Self {
160 Self::from_metric(AngleMetric::Grad, value)
161 }
162
163 #[inline]
165 #[must_use]
166 pub fn turn(value: f32) -> Self {
167 Self::from_metric(AngleMetric::Turn, value)
168 }
169
170 #[inline]
172 #[must_use]
173 pub fn percent(value: f32) -> Self {
174 Self::from_metric(AngleMetric::Percent, value)
175 }
176
177 #[inline]
179 #[must_use]
180 pub fn from_metric(metric: AngleMetric, value: f32) -> Self {
181 Self {
182 metric,
183 number: FloatValue::new(value),
184 }
185 }
186
187 #[inline]
191 #[must_use]
192 pub fn to_degrees(&self) -> f32 {
193 let mut val = self.to_degrees_raw() % 360.0;
194 if val < 0.0 {
195 val += 360.0;
196 }
197 val
198 }
199
200 #[inline]
203 #[must_use]
204 pub fn to_degrees_raw(&self) -> f32 {
205 match self.metric {
206 AngleMetric::Degree => self.number.get(),
207 AngleMetric::Grad => self.number.get() / 400.0 * 360.0,
208 AngleMetric::Radians => self.number.get().to_degrees(),
209 AngleMetric::Turn => self.number.get() * 360.0,
210 AngleMetric::Percent => self.number.get() / 100.0 * 360.0,
211 }
212 }
213}
214
215#[derive(Clone, PartialEq, Eq)]
219pub enum CssAngleValueParseError<'a> {
220 EmptyString,
221 NoValueGiven(&'a str, AngleMetric),
222 ValueParseErr(ParseFloatError, &'a str),
223 InvalidAngle(&'a str),
224}
225
226impl_debug_as_display!(CssAngleValueParseError<'a>);
227impl_display! { CssAngleValueParseError<'a>, {
228 EmptyString => format!("Missing [rad / deg / turn / %] value"),
229 NoValueGiven(input, metric) => format!("Expected floating-point angle value, got: \"{}{}\"", input, metric),
230 ValueParseErr(err, number_str) => format!("Could not parse \"{}\" as floating-point value: \"{}\"", number_str, err),
231 InvalidAngle(s) => format!("Invalid angle value: \"{}\"", s),
232}}
233
234#[derive(Debug, Clone, PartialEq, Eq)]
236#[repr(C)]
237pub struct AngleNoValueGivenError {
238 pub value: AzString,
239 pub metric: AngleMetric,
240}
241
242#[derive(Debug, Clone, PartialEq, Eq)]
244#[repr(C, u8)]
245pub enum CssAngleValueParseErrorOwned {
246 EmptyString,
247 NoValueGiven(AngleNoValueGivenError),
248 ValueParseErr(ParseFloatErrorWithInput),
249 InvalidAngle(AzString),
250}
251
252impl CssAngleValueParseError<'_> {
253 #[must_use]
254 pub fn to_contained(&self) -> CssAngleValueParseErrorOwned {
255 match self {
256 CssAngleValueParseError::EmptyString => CssAngleValueParseErrorOwned::EmptyString,
257 CssAngleValueParseError::NoValueGiven(s, metric) => {
258 CssAngleValueParseErrorOwned::NoValueGiven(AngleNoValueGivenError {
259 value: (*s).to_string().into(),
260 metric: *metric,
261 })
262 }
263 CssAngleValueParseError::ValueParseErr(err, s) => {
264 CssAngleValueParseErrorOwned::ValueParseErr(ParseFloatErrorWithInput {
265 error: err.clone().into(),
266 input: (*s).to_string().into(),
267 })
268 }
269 CssAngleValueParseError::InvalidAngle(s) => {
270 CssAngleValueParseErrorOwned::InvalidAngle((*s).to_string().into())
271 }
272 }
273 }
274}
275
276impl CssAngleValueParseErrorOwned {
277 #[must_use]
278 pub fn to_shared(&self) -> CssAngleValueParseError<'_> {
279 match self {
280 Self::EmptyString => CssAngleValueParseError::EmptyString,
281 Self::NoValueGiven(e) => {
282 CssAngleValueParseError::NoValueGiven(e.value.as_str(), e.metric)
283 }
284 Self::ValueParseErr(e) => {
285 CssAngleValueParseError::ValueParseErr(e.error.to_std(), e.input.as_str())
286 }
287 Self::InvalidAngle(s) => CssAngleValueParseError::InvalidAngle(s.as_str()),
288 }
289 }
290}
291
292#[cfg(feature = "parser")]
295pub fn parse_angle_value(input: &str) -> Result<AngleValue, CssAngleValueParseError<'_>> {
299 let input = input.trim();
300
301 if input.is_empty() {
302 return Err(CssAngleValueParseError::EmptyString);
303 }
304
305 let match_values = &[
306 ("deg", AngleMetric::Degree),
307 ("turn", AngleMetric::Turn),
308 ("grad", AngleMetric::Grad),
309 ("rad", AngleMetric::Radians),
310 ("%", AngleMetric::Percent),
311 ];
312
313 for (match_val, metric) in match_values {
314 if let Some(value) = input.strip_suffix(match_val) {
315 let value = value.trim();
316 if value.is_empty() {
317 return Err(CssAngleValueParseError::NoValueGiven(input, *metric));
318 }
319 match value.parse::<f32>() {
320 Ok(o) => return Ok(AngleValue::from_metric(*metric, o)),
321 Err(e) => return Err(CssAngleValueParseError::ValueParseErr(e, value)),
322 }
323 }
324 }
325
326 input.parse::<f32>().map_or_else(
328 |_| Err(CssAngleValueParseError::InvalidAngle(input)),
329 |o| Ok(AngleValue::from_metric(AngleMetric::Degree, o)),
330 )
331}
332
333#[cfg(all(test, feature = "parser"))]
334mod tests {
335 #![allow(clippy::float_cmp, clippy::approx_constant)]
338 use super::*;
339
340 #[test]
341 fn test_parse_angle_value_deg() {
342 assert_eq!(parse_angle_value("90deg").unwrap(), AngleValue::deg(90.0));
343 assert_eq!(
344 parse_angle_value("-45.5deg").unwrap(),
345 AngleValue::deg(-45.5)
346 );
347 assert_eq!(parse_angle_value("180").unwrap(), AngleValue::deg(180.0));
349 }
350
351 #[test]
352 fn test_parse_angle_value_rad() {
353 assert_eq!(parse_angle_value("1.57rad").unwrap(), AngleValue::rad(1.57));
354 assert_eq!(
355 parse_angle_value(" -3.14rad ").unwrap(),
356 AngleValue::rad(-3.14)
357 );
358 }
359
360 #[test]
361 fn test_parse_angle_value_grad() {
362 assert_eq!(
363 parse_angle_value("100grad").unwrap(),
364 AngleValue::grad(100.0)
365 );
366 assert_eq!(
367 parse_angle_value("400grad").unwrap(),
368 AngleValue::grad(400.0)
369 );
370 }
371
372 #[test]
373 fn test_parse_angle_value_turn() {
374 assert_eq!(
375 parse_angle_value("0.25turn").unwrap(),
376 AngleValue::turn(0.25)
377 );
378 assert_eq!(parse_angle_value("1turn").unwrap(), AngleValue::turn(1.0));
379 }
380
381 #[test]
382 fn test_parse_angle_value_percent() {
383 assert_eq!(parse_angle_value("50%").unwrap(), AngleValue::percent(50.0));
384 }
385
386 #[test]
387 fn test_parse_angle_value_errors() {
388 assert!(parse_angle_value("").is_err());
389 assert!(parse_angle_value("deg").is_err());
390 assert!(parse_angle_value("90 degs").is_err());
391 assert!(parse_angle_value("ninety-deg").is_err());
392 assert!(parse_angle_value("1.57 rads").is_err());
393 }
394
395 #[test]
396 fn test_to_degrees_conversion() {
397 assert_eq!(AngleValue::deg(90.0).to_degrees(), 90.0);
398 assert!((AngleValue::rad(core::f32::consts::PI).to_degrees() - 180.0).abs() < 0.1);
400 assert_eq!(AngleValue::grad(100.0).to_degrees(), 90.0);
401 assert_eq!(AngleValue::turn(0.5).to_degrees(), 180.0);
402 assert_eq!(AngleValue::deg(-90.0).to_degrees(), 270.0);
403 assert_eq!(AngleValue::deg(450.0).to_degrees(), 90.0);
404 }
405}
406
407#[cfg(test)]
408#[allow(
409 clippy::float_cmp,
410 clippy::unreadable_literal,
411 clippy::cast_precision_loss,
412 clippy::too_many_lines
413)]
414mod autotest_generated {
415 use super::*;
416 use crate::props::basic::error::{
417 ParseFloatError as FfiParseFloatError, ParseFloatErrorWithInput,
418 };
419
420 const MULT: isize = 1000;
422
423 const ALL_METRICS: [AngleMetric; 5] = [
425 AngleMetric::Degree,
426 AngleMetric::Radians,
427 AngleMetric::Grad,
428 AngleMetric::Turn,
429 AngleMetric::Percent,
430 ];
431
432 #[test]
437 fn autotest_angle_metric_display_is_non_empty_and_exact() {
438 assert_eq!(AngleMetric::Degree.to_string(), "deg");
439 assert_eq!(AngleMetric::Radians.to_string(), "rad");
440 assert_eq!(AngleMetric::Grad.to_string(), "grad");
441 assert_eq!(AngleMetric::Turn.to_string(), "turn");
442 assert_eq!(AngleMetric::Percent.to_string(), "%");
443 assert_eq!(AngleMetric::default(), AngleMetric::Degree);
444 for m in ALL_METRICS {
445 assert!(!m.to_string().is_empty(), "empty unit string for {m:?}");
446 }
447 }
448
449 #[test]
450 fn autotest_angle_value_display_default_and_zero() {
451 assert_eq!(AngleValue::default().to_string(), "0deg");
452 assert_eq!(AngleValue::zero().to_string(), "0deg");
453 assert_eq!(format!("{:?}", AngleValue::zero()), "0deg");
455 assert_eq!(AngleValue::zero().print_as_css_value(), "0deg");
456 }
457
458 #[test]
459 fn autotest_angle_value_display_never_emits_inf_or_nan() {
460 for m in ALL_METRICS {
463 for v in [
464 f32::NAN,
465 f32::INFINITY,
466 f32::NEG_INFINITY,
467 f32::MAX,
468 f32::MIN,
469 f32::MIN_POSITIVE,
470 -0.0,
471 ] {
472 let s = AngleValue::from_metric(m, v).to_string();
473 assert!(!s.is_empty(), "empty serialization for {m:?} / {v}");
474 assert!(!s.contains("inf"), "serialized infinity: {s}");
475 assert!(!s.contains("NaN"), "serialized NaN: {s}");
476 assert!(s.ends_with(&m.to_string()), "lost the unit suffix: {s}");
477 }
478 }
479 }
480
481 #[test]
482 fn autotest_angle_value_nan_serializes_as_zero() {
483 assert_eq!(AngleValue::deg(f32::NAN).to_string(), "0deg");
485 assert_eq!(AngleValue::rad(f32::NAN).to_string(), "0rad");
486 }
487
488 #[test]
493 fn autotest_zero_is_the_neutral_element() {
494 let z = AngleValue::zero();
495 assert_eq!(z, AngleValue::default());
496 assert_eq!(z, AngleValue::const_deg(0));
497 assert_eq!(z, AngleValue::deg(0.0));
498 assert_eq!(z.metric, AngleMetric::Degree);
499 assert_eq!(z.number.number(), 0);
500 assert_eq!(z.number.get(), 0.0);
501 assert_eq!(z.to_degrees(), 0.0);
502 assert_eq!(z.to_degrees_raw(), 0.0);
503 }
504
505 #[test]
506 fn autotest_from_metric_fields_match_args() {
507 for m in ALL_METRICS {
508 let a = AngleValue::from_metric(m, 12.5);
509 assert_eq!(a.metric, m);
510 assert_eq!(a.number.get(), 12.5);
511 assert_eq!(a.number.number(), 12_500);
512 }
513 assert_eq!(
515 AngleValue::deg(1.5),
516 AngleValue::from_metric(AngleMetric::Degree, 1.5)
517 );
518 assert_eq!(
519 AngleValue::rad(1.5),
520 AngleValue::from_metric(AngleMetric::Radians, 1.5)
521 );
522 assert_eq!(
523 AngleValue::grad(1.5),
524 AngleValue::from_metric(AngleMetric::Grad, 1.5)
525 );
526 assert_eq!(
527 AngleValue::turn(1.5),
528 AngleValue::from_metric(AngleMetric::Turn, 1.5)
529 );
530 assert_eq!(
531 AngleValue::percent(1.5),
532 AngleValue::from_metric(AngleMetric::Percent, 1.5)
533 );
534 }
535
536 #[test]
541 fn autotest_const_ctors_zero_negative_and_metric() {
542 for (built, metric) in [
543 (AngleValue::const_deg(0), AngleMetric::Degree),
544 (AngleValue::const_rad(0), AngleMetric::Radians),
545 (AngleValue::const_grad(0), AngleMetric::Grad),
546 (AngleValue::const_turn(0), AngleMetric::Turn),
547 (AngleValue::const_percent(0), AngleMetric::Percent),
548 ] {
549 assert_eq!(built.metric, metric);
550 assert_eq!(built.number.number(), 0);
551 }
552 assert_eq!(AngleValue::const_deg(-90).number.get(), -90.0);
553 assert_eq!(AngleValue::const_rad(-3).number.number(), -3 * MULT);
554 assert_eq!(AngleValue::const_turn(-1).to_degrees_raw(), -360.0);
555 }
556
557 #[test]
558 fn autotest_const_from_metric_matches_specific_ctors() {
559 for (m, specific) in [
560 (AngleMetric::Degree, AngleValue::const_deg(7)),
561 (AngleMetric::Radians, AngleValue::const_rad(7)),
562 (AngleMetric::Grad, AngleValue::const_grad(7)),
563 (AngleMetric::Turn, AngleValue::const_turn(7)),
564 (AngleMetric::Percent, AngleValue::const_percent(7)),
565 ] {
566 assert_eq!(AngleValue::const_from_metric(m, 7), specific);
567 assert_eq!(specific.number.number(), 7 * MULT);
568 }
569 }
570
571 #[test]
572 fn autotest_const_ctors_at_safe_isize_boundary() {
573 const MAX_SAFE: isize = isize::MAX / MULT;
576 const MIN_SAFE: isize = isize::MIN / MULT;
577
578 assert_eq!(
579 AngleValue::const_deg(MAX_SAFE).number.number(),
580 MAX_SAFE * MULT
581 );
582 assert_eq!(
583 AngleValue::const_deg(MIN_SAFE).number.number(),
584 MIN_SAFE * MULT
585 );
586 assert!(AngleValue::const_deg(MAX_SAFE).number.get().is_finite());
588 assert!(AngleValue::const_deg(MIN_SAFE).number.get().is_finite());
589 assert!(AngleValue::const_turn(MAX_SAFE).to_degrees().is_finite());
590 assert!(AngleValue::const_turn(MIN_SAFE).to_degrees().is_finite());
591 }
592
593 #[test]
594 fn autotest_const_deg_isize_max_overflows_unchecked() {
595 let huge = core::hint::black_box(isize::MAX);
600 let prev = std::panic::take_hook();
601 std::panic::set_hook(Box::new(|_| {}));
602 let res = std::panic::catch_unwind(move || AngleValue::const_deg(huge).number.number());
603 std::panic::set_hook(prev);
604
605 match res {
606 Err(_) => {} Ok(n) => assert_eq!(
608 n,
609 isize::MAX.wrapping_mul(MULT),
610 "release build must wrap, not produce a sanitized value"
611 ),
612 }
613 }
614
615 #[test]
616 fn autotest_const_from_metric_fractional_digit_truncation() {
617 let f = |pre, post| {
618 AngleValue::const_from_metric_fractional(AngleMetric::Degree, pre, post)
619 .number
620 .number()
621 };
622 assert_eq!(f(0, 0), 0);
623 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!(
630 AngleValue::const_from_metric_fractional(AngleMetric::Turn, 0, 25).metric,
631 AngleMetric::Turn
632 );
633 }
634
635 #[test]
636 fn autotest_const_fractional_sign_handling_and_negative_zero_trap() {
637 let deg = |pre, post| {
638 AngleValue::const_from_metric_fractional(AngleMetric::Degree, pre, post)
639 .number
640 .get()
641 };
642 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);
648 assert_ne!(deg(-0, 5), -0.5);
649 }
650
651 #[test]
656 fn autotest_f32_ctor_nan_collapses_to_zero() {
657 for m in ALL_METRICS {
658 let a = AngleValue::from_metric(m, f32::NAN);
659 assert_eq!(a.number.number(), 0, "NaN did not clamp to 0 for {m:?}");
660 assert_eq!(a.number.get(), 0.0);
661 assert!(!a.number.get().is_nan());
662 assert_eq!(a.to_degrees(), 0.0);
663 assert_eq!(a.to_degrees_raw(), 0.0);
664 assert_eq!(a, AngleValue::from_metric(m, 0.0));
666 }
667 }
668
669 #[test]
670 fn autotest_f32_ctor_infinities_saturate_to_isize_bounds() {
671 assert_eq!(AngleValue::deg(f32::INFINITY).number.number(), isize::MAX);
672 assert_eq!(
673 AngleValue::deg(f32::NEG_INFINITY).number.number(),
674 isize::MIN
675 );
676 assert_eq!(AngleValue::deg(f32::MAX).number.number(), isize::MAX);
678 assert_eq!(AngleValue::deg(f32::MIN).number.number(), isize::MIN);
679 for m in ALL_METRICS {
680 for v in [f32::INFINITY, f32::NEG_INFINITY, f32::MAX, f32::MIN] {
681 let a = AngleValue::from_metric(m, v);
682 assert!(
683 a.number.get().is_finite(),
684 "{m:?} / {v} leaked a non-finite"
685 );
686 }
687 }
688 }
689
690 #[test]
691 fn autotest_f32_ctor_truncates_toward_zero_below_precision() {
692 assert_eq!(AngleValue::deg(1.9999).number.number(), 1_999);
695 assert_eq!(AngleValue::deg(-1.9999).number.number(), -1_999);
696 assert_eq!(AngleValue::deg(0.0004).number.number(), 0);
697 assert_eq!(AngleValue::deg(-0.0004).number.number(), 0);
698 assert_eq!(AngleValue::deg(f32::EPSILON).number.number(), 0);
699 assert_eq!(AngleValue::deg(f32::MIN_POSITIVE).number.number(), 0);
700 assert_eq!(AngleValue::deg(-0.0).number.number(), 0);
702 assert_eq!(AngleValue::deg(-0.0), AngleValue::deg(0.0));
703 }
704
705 #[test]
710 fn autotest_to_degrees_known_conversions() {
711 assert_eq!(AngleValue::deg(90.0).to_degrees(), 90.0);
712 assert_eq!(AngleValue::grad(100.0).to_degrees(), 90.0);
713 assert_eq!(AngleValue::turn(0.25).to_degrees(), 90.0);
714 assert_eq!(AngleValue::percent(50.0).to_degrees(), 180.0);
715 assert_eq!(AngleValue::percent(25.0).to_degrees_raw(), 90.0);
716 assert!((AngleValue::rad(core::f32::consts::FRAC_PI_2).to_degrees() - 90.0).abs() < 0.1);
718 }
719
720 #[test]
721 fn autotest_to_degrees_normalizes_but_raw_does_not() {
722 assert_eq!(AngleValue::deg(360.0).to_degrees(), 0.0);
724 assert_eq!(AngleValue::turn(1.0).to_degrees(), 0.0);
725 assert_eq!(AngleValue::grad(400.0).to_degrees(), 0.0);
726 assert_eq!(AngleValue::percent(100.0).to_degrees(), 0.0);
727 assert_eq!(AngleValue::deg(360.0).to_degrees_raw(), 360.0);
729 assert_eq!(AngleValue::turn(1.0).to_degrees_raw(), 360.0);
730 assert_eq!(AngleValue::grad(400.0).to_degrees_raw(), 360.0);
731 assert_eq!(AngleValue::percent(100.0).to_degrees_raw(), 360.0);
732
733 assert_eq!(AngleValue::deg(-90.0).to_degrees(), 270.0);
735 assert_eq!(AngleValue::deg(-450.0).to_degrees(), 270.0);
736 assert_eq!(AngleValue::deg(-0.5).to_degrees(), 359.5);
737 assert_eq!(AngleValue::deg(720.0).to_degrees(), 0.0);
738 assert_eq!(AngleValue::deg(450.0).to_degrees_raw(), 450.0);
739 assert_eq!(AngleValue::turn(-2.0).to_degrees(), 0.0);
740 }
741
742 #[test]
743 fn autotest_to_degrees_on_saturated_values_stays_finite_and_in_range() {
744 for m in ALL_METRICS {
748 for v in [
749 f32::INFINITY,
750 f32::NEG_INFINITY,
751 f32::MAX,
752 f32::MIN,
753 f32::NAN,
754 ] {
755 let a = AngleValue::from_metric(m, v);
756 let raw = a.to_degrees_raw();
757 let norm = a.to_degrees();
758 assert!(raw.is_finite(), "to_degrees_raw not finite: {m:?} / {v}");
759 assert!(norm.is_finite(), "to_degrees not finite: {m:?} / {v}");
760 assert!(
761 (0.0..360.0).contains(&norm),
762 "to_degrees out of [0,360): {m:?} / {v} -> {norm}"
763 );
764 }
765 }
766 }
767
768 #[test]
769 fn autotest_ord_is_metric_first_not_semantic_angle() {
770 assert!(AngleValue::deg(1000.0) < AngleValue::rad(0.0));
773 assert!(AngleValue::turn(0.0) < AngleValue::percent(0.0));
774 assert!(AngleValue::deg(-1.0) < AngleValue::deg(1.0));
776 use core::hash::{Hash, Hasher};
778 let h = |a: AngleValue| {
779 let mut s = std::collections::hash_map::DefaultHasher::new();
780 a.hash(&mut s);
781 s.finish()
782 };
783 assert_eq!(AngleValue::deg(1.0), AngleValue::deg(1.0));
784 assert_eq!(h(AngleValue::deg(1.0)), h(AngleValue::deg(1.0)));
785 assert_eq!(h(AngleValue::deg(f32::NAN)), h(AngleValue::deg(0.0)));
786 assert_ne!(AngleValue::deg(1.0), AngleValue::rad(1.0));
787 }
788
789 #[cfg(feature = "parser")]
794 #[test]
795 fn autotest_parse_empty_and_whitespace_only() {
796 for input in ["", " ", "\t\n\r", "\u{a0}", " \u{2003} "] {
797 assert!(
798 matches!(
799 parse_angle_value(input),
800 Err(CssAngleValueParseError::EmptyString)
801 ),
802 "expected EmptyString for {input:?}"
803 );
804 }
805 }
806
807 #[cfg(feature = "parser")]
808 #[test]
809 fn autotest_parse_unit_without_number() {
810 for (input, metric) in [
811 ("deg", AngleMetric::Degree),
812 ("rad", AngleMetric::Radians),
813 ("grad", AngleMetric::Grad),
814 ("turn", AngleMetric::Turn),
815 ("%", AngleMetric::Percent),
816 (" deg ", AngleMetric::Degree),
817 ] {
818 match parse_angle_value(input) {
819 Err(CssAngleValueParseError::NoValueGiven(_, m)) => assert_eq!(m, metric),
820 other => panic!("expected NoValueGiven for {input:?}, got {other:?}"),
821 }
822 }
823 }
824
825 #[cfg(feature = "parser")]
826 #[test]
827 fn autotest_parse_garbage_is_rejected_without_panicking() {
828 for input in [
829 "ninety",
830 "!!!",
831 "90 degs",
832 "1.57 rads",
833 "90degdeg",
834 "--90deg",
835 "1_0deg",
836 "90;garbage",
837 "deg90",
838 "%50",
839 "0x1Fdeg",
840 "+-1turn",
841 "9 0deg",
842 "\0deg",
843 ] {
844 let res = parse_angle_value(input);
845 assert!(res.is_err(), "garbage accepted: {input:?} -> {res:?}");
846 assert!(!format!("{}", res.unwrap_err()).is_empty());
848 }
849 }
850
851 #[cfg(feature = "parser")]
852 #[test]
853 fn autotest_parse_uppercase_units_are_rejected() {
854 for input in ["90DEG", "1RAD", "0.5TURN", "100GRAD", "90Deg"] {
857 assert!(
858 parse_angle_value(input).is_err(),
859 "case-insensitive unit unexpectedly accepted: {input:?}"
860 );
861 }
862 }
863
864 #[cfg(feature = "parser")]
865 #[test]
866 fn autotest_parse_accepts_whitespace_between_number_and_unit() {
867 assert_eq!(parse_angle_value("90 deg").unwrap(), AngleValue::deg(90.0));
870 assert_eq!(parse_angle_value("90\tdeg").unwrap(), AngleValue::deg(90.0));
871 assert_eq!(
872 parse_angle_value("50 %").unwrap(),
873 AngleValue::percent(50.0)
874 );
875 assert_eq!(parse_angle_value(" 90deg ").unwrap(), AngleValue::deg(90.0));
876 }
877
878 #[cfg(feature = "parser")]
879 #[test]
880 fn autotest_parse_accepts_float_keywords_and_neutralizes_them() {
881 let nan = parse_angle_value("NaN").expect("f32::from_str accepts NaN");
884 assert_eq!(nan.number.number(), 0);
885 assert!(!nan.to_degrees().is_nan());
886
887 let nan_rad = parse_angle_value("nanrad").expect("f32::from_str accepts nan");
888 assert_eq!(nan_rad.metric, AngleMetric::Radians);
889 assert_eq!(nan_rad.number.number(), 0);
890
891 let inf = parse_angle_value("inf").expect("f32::from_str accepts inf");
892 assert_eq!(inf.number.number(), isize::MAX);
893 assert!(inf.to_degrees().is_finite());
894
895 let neg_inf = parse_angle_value("-infdeg").expect("f32::from_str accepts -inf");
896 assert_eq!(neg_inf.number.number(), isize::MIN);
897 assert!(neg_inf.to_degrees_raw().is_finite());
898 }
899
900 #[cfg(feature = "parser")]
901 #[test]
902 fn autotest_parse_boundary_numbers_saturate() {
903 assert_eq!(parse_angle_value("0").unwrap(), AngleValue::deg(0.0));
904 assert_eq!(parse_angle_value("-0").unwrap().number.number(), 0);
905 assert_eq!(parse_angle_value("+90deg").unwrap(), AngleValue::deg(90.0));
906 assert_eq!(parse_angle_value(".5turn").unwrap(), AngleValue::turn(0.5));
907 assert_eq!(
910 parse_angle_value("9223372036854775807")
911 .unwrap()
912 .number
913 .number(),
914 isize::MAX
915 );
916 assert_eq!(
917 parse_angle_value("-9223372036854775808")
918 .unwrap()
919 .number
920 .number(),
921 isize::MIN
922 );
923 assert_eq!(
925 parse_angle_value("1e40deg").unwrap().number.number(),
926 isize::MAX
927 );
928 assert_eq!(parse_angle_value("1e-40deg").unwrap().number.number(), 0);
929 assert_eq!(parse_angle_value("0.0001deg").unwrap().number.number(), 0);
930 }
931
932 #[cfg(feature = "parser")]
933 #[test]
934 fn autotest_parse_extremely_long_input_terminates() {
935 let long_digits = "9".repeat(100_000) + "deg";
937 assert_eq!(
938 parse_angle_value(&long_digits).unwrap().number.number(),
939 isize::MAX
940 );
941
942 let padded = "0".repeat(100_000) + "1deg";
944 assert_eq!(parse_angle_value(&padded).unwrap(), AngleValue::deg(1.0));
945
946 let long_junk = "a".repeat(100_000);
948 assert!(parse_angle_value(&long_junk).is_err());
949 }
950
951 #[cfg(feature = "parser")]
952 #[test]
953 fn autotest_parse_deeply_nested_brackets_does_not_stack_overflow() {
954 let nested = "(".repeat(10_000);
956 assert!(parse_angle_value(&nested).is_err());
957 let nested_unit = "[".repeat(10_000) + "deg";
958 assert!(parse_angle_value(&nested_unit).is_err());
959 }
960
961 #[cfg(feature = "parser")]
962 #[test]
963 fn autotest_parse_unicode_input_never_panics() {
964 for input in [
966 "°",
967 "90°",
968 "\u{1F600}",
969 "\u{1F600}deg",
970 "90deg", "9\u{0301}0deg", "٩٠%", "\u{200b}90deg", "90de\u{0261}", ] {
976 let res = parse_angle_value(input);
977 assert!(
978 res.is_err(),
979 "unicode garbage accepted: {input:?} -> {res:?}"
980 );
981 assert!(!format!("{}", res.unwrap_err()).is_empty());
982 }
983 }
984
985 #[cfg(feature = "parser")]
986 #[test]
987 fn autotest_parse_valid_minimal_positive_control() {
988 assert_eq!(parse_angle_value("1deg").unwrap(), AngleValue::deg(1.0));
989 assert_eq!(parse_angle_value("0").unwrap(), AngleValue::zero());
990 }
991
992 #[cfg(feature = "parser")]
997 #[test]
998 fn autotest_round_trip_display_then_parse_all_metrics() {
999 for m in ALL_METRICS {
1002 for v in [
1003 0.0_f32, 1.0, -1.0, 0.5, -0.25, 45.5, 90.0, 180.0, 359.0, 1000.0,
1004 ] {
1005 let angle = AngleValue::from_metric(m, v);
1006 let printed = angle.to_string();
1007 let reparsed = parse_angle_value(&printed)
1008 .unwrap_or_else(|e| panic!("cannot re-parse own output {printed:?}: {e}"));
1009 assert_eq!(reparsed, angle, "round-trip changed value: {printed:?}");
1010 assert_eq!(reparsed.metric, m, "round-trip changed unit: {printed:?}");
1011 assert_eq!(angle.print_as_css_value(), printed);
1013 }
1014 }
1015 }
1016
1017 #[cfg(feature = "parser")]
1018 #[test]
1019 fn autotest_round_trip_metric_suffix_is_unambiguous() {
1020 for m in ALL_METRICS {
1022 let parsed = parse_angle_value(&format!("1{m}")).unwrap();
1023 assert_eq!(parsed.metric, m, "unit {m} did not round-trip");
1024 assert_eq!(parsed.number.get(), 1.0);
1025 }
1026 assert_eq!(
1027 parse_angle_value("1grad").unwrap().metric,
1028 AngleMetric::Grad
1029 );
1030 assert_eq!(
1031 parse_angle_value("1rad").unwrap().metric,
1032 AngleMetric::Radians
1033 );
1034 }
1035
1036 #[cfg(feature = "parser")]
1037 #[test]
1038 fn autotest_round_trip_quantization_is_idempotent() {
1039 for v in [0.0_f32, 1.0, -1.0, 0.5, -0.25, 45.5, 359.0] {
1042 let once = AngleValue::deg(v);
1043 let twice = AngleValue::deg(once.number.get());
1044 assert_eq!(once, twice, "quantization not idempotent for {v}");
1045 }
1046 }
1047
1048 #[cfg(feature = "parser")]
1053 #[test]
1054 fn autotest_error_owned_round_trip_from_real_parse_failures() {
1055 for input in ["", "deg", "%", "xdeg", "zzz", "\u{1F600}rad"] {
1056 let err = parse_angle_value(input).unwrap_err();
1057 let owned = err.to_contained();
1058 assert_eq!(
1059 owned.to_shared(),
1060 err,
1061 "to_contained/to_shared lost information for {input:?}"
1062 );
1063 assert!(!format!("{err}").is_empty());
1065 assert!(!format!("{owned:?}").is_empty());
1066 }
1067 }
1068
1069 #[cfg(feature = "parser")]
1070 #[test]
1071 fn autotest_error_variants_are_the_expected_ones() {
1072 assert!(matches!(
1073 parse_angle_value("").unwrap_err(),
1074 CssAngleValueParseError::EmptyString
1075 ));
1076 assert!(matches!(
1077 parse_angle_value("turn").unwrap_err(),
1078 CssAngleValueParseError::NoValueGiven(_, AngleMetric::Turn)
1079 ));
1080 assert!(matches!(
1081 parse_angle_value("xdeg").unwrap_err(),
1082 CssAngleValueParseError::ValueParseErr(_, "x")
1083 ));
1084 assert!(matches!(
1085 parse_angle_value("zzz").unwrap_err(),
1086 CssAngleValueParseError::InvalidAngle("zzz")
1087 ));
1088 }
1089
1090 #[test]
1091 fn autotest_error_to_shared_handles_empty_and_extreme_payloads() {
1092 let cases = [
1095 CssAngleValueParseErrorOwned::EmptyString,
1096 CssAngleValueParseErrorOwned::NoValueGiven(AngleNoValueGivenError {
1097 value: String::new().into(),
1098 metric: AngleMetric::Percent,
1099 }),
1100 CssAngleValueParseErrorOwned::ValueParseErr(ParseFloatErrorWithInput {
1101 error: FfiParseFloatError::Empty,
1102 input: String::new().into(),
1103 }),
1104 CssAngleValueParseErrorOwned::ValueParseErr(ParseFloatErrorWithInput {
1105 error: FfiParseFloatError::Invalid,
1106 input: "\u{1F600}".to_string().into(),
1107 }),
1108 CssAngleValueParseErrorOwned::InvalidAngle(String::new().into()),
1109 CssAngleValueParseErrorOwned::InvalidAngle("\u{1F600}\u{0301}".to_string().into()),
1110 ];
1111 for owned in cases {
1112 let shared = owned.to_shared();
1113 assert!(!format!("{shared}").is_empty());
1114 assert_eq!(shared.to_contained(), owned);
1116 }
1117 }
1118}