svgtypes/
angle.rs

1// Copyright 2018 the SVG Types Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4use crate::{Error, Stream};
5
6/// List of all SVG angle units.
7#[derive(Clone, Copy, PartialEq, Eq, Debug)]
8#[allow(missing_docs)]
9pub enum AngleUnit {
10    Degrees,
11    Gradians,
12    Radians,
13    Turns,
14}
15
16/// Representation of the [`<angle>`] type.
17///
18/// [`<angle>`]: https://www.w3.org/TR/css-values-3/#angles
19#[derive(Clone, Copy, PartialEq, Debug)]
20#[allow(missing_docs)]
21pub struct Angle {
22    pub number: f64,
23    pub unit: AngleUnit,
24}
25
26impl Angle {
27    /// Constructs a new angle.
28    #[inline]
29    pub fn new(number: f64, unit: AngleUnit) -> Angle {
30        Angle { number, unit }
31    }
32
33    /// Converts angle to degrees.
34    #[inline]
35    pub fn to_degrees(&self) -> f64 {
36        match self.unit {
37            AngleUnit::Degrees => self.number,
38            AngleUnit::Gradians => self.number * 180.0 / 200.0,
39            AngleUnit::Radians => self.number.to_degrees(),
40            AngleUnit::Turns => self.number * 360.0,
41        }
42    }
43}
44
45impl std::str::FromStr for Angle {
46    type Err = Error;
47
48    #[inline]
49    fn from_str(text: &str) -> Result<Self, Error> {
50        let mut s = Stream::from(text);
51        let l = s.parse_angle()?;
52
53        if !s.at_end() {
54            return Err(Error::UnexpectedData(s.calc_char_pos()));
55        }
56
57        Ok(Angle::new(l.number, l.unit))
58    }
59}
60
61impl Stream<'_> {
62    /// Parses angle from the stream.
63    ///
64    /// <https://www.w3.org/TR/SVG2/types.html#InterfaceSVGAngle>
65    ///
66    /// # Notes
67    ///
68    /// - Suffix must be lowercase, otherwise it will be an error.
69    pub fn parse_angle(&mut self) -> Result<Angle, Error> {
70        self.skip_spaces();
71
72        let n = self.parse_number()?;
73
74        if self.at_end() {
75            return Ok(Angle::new(n, AngleUnit::Degrees));
76        }
77
78        let u = if self.starts_with(b"deg") {
79            self.advance(3);
80            AngleUnit::Degrees
81        } else if self.starts_with(b"grad") {
82            self.advance(4);
83            AngleUnit::Gradians
84        } else if self.starts_with(b"rad") {
85            self.advance(3);
86            AngleUnit::Radians
87        } else if self.starts_with(b"turn") {
88            self.advance(4);
89            AngleUnit::Turns
90        } else {
91            AngleUnit::Degrees
92        };
93
94        Ok(Angle::new(n, u))
95    }
96}
97
98#[rustfmt::skip]
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use std::str::FromStr;
103
104    macro_rules! test_p {
105        ($name:ident, $text:expr, $result:expr) => (
106            #[test]
107            fn $name() {
108                assert_eq!(Angle::from_str($text).unwrap(), $result);
109            }
110        )
111    }
112
113    test_p!(parse_1,  "1",   Angle::new(1.0, AngleUnit::Degrees));
114    test_p!(parse_2,  "1deg", Angle::new(1.0, AngleUnit::Degrees));
115    test_p!(parse_3,  "1grad", Angle::new(1.0, AngleUnit::Gradians));
116    test_p!(parse_4,  "1rad", Angle::new(1.0, AngleUnit::Radians));
117    test_p!(parse_5,  "1turn", Angle::new(1.0, AngleUnit::Turns));
118
119    #[test]
120    fn err_1() {
121        let mut s = Stream::from("1q");
122        assert_eq!(s.parse_angle().unwrap(), Angle::new(1.0, AngleUnit::Degrees));
123        assert_eq!(s.parse_angle().unwrap_err().to_string(),
124                   "invalid number at position 2");
125    }
126
127    #[test]
128    fn err_2() {
129        assert_eq!(Angle::from_str("1degq").unwrap_err().to_string(),
130                   "unexpected data at position 5");
131    }
132}