Skip to main content

hayro_postscript/
number.rs

1use crate::error::{Error, Result};
2use crate::reader::{Reader, is_delimiter, is_whitespace};
3
4/// A PostScript number object (integer or real).
5#[derive(Debug, Clone, Copy, PartialEq)]
6pub enum Number {
7    Integer(i32),
8    Real(f32),
9}
10
11impl Number {
12    /// Return the value as an `i32`. Reals are truncated.
13    pub fn as_i32(self) -> i32 {
14        match self {
15            Self::Integer(v) => v,
16            Self::Real(v) => v as i32,
17        }
18    }
19
20    /// Return the value as an `f32`.
21    pub fn as_f32(self) -> f32 {
22        match self {
23            Self::Integer(v) => v as f32,
24            Self::Real(v) => v,
25        }
26    }
27
28    /// Return the value as an `f64`.
29    pub fn as_f64(self) -> f64 {
30        match self {
31            Self::Integer(v) => v as f64,
32            Self::Real(v) => v as f64,
33        }
34    }
35}
36
37fn is_terminated(r: &Reader<'_>) -> bool {
38    match r.peek_byte() {
39        None => true,
40        Some(b) => is_whitespace(b) || is_delimiter(b),
41    }
42}
43
44pub(crate) fn read(r: &mut Reader<'_>) -> Result<Number> {
45    let saved = r.offset();
46
47    // Optional sign.
48    let first = r.peek_byte().ok_or(Error::SyntaxError)?;
49    let has_sign = first == b'+' || first == b'-';
50
51    if has_sign {
52        r.forward();
53    }
54
55    // Consume leading digits.
56    let digit_start = r.offset();
57    r.forward_while(|b| b.is_ascii_digit());
58    let has_digits = r.offset() > digit_start;
59
60    // Check if number is a radix number.
61    if !has_sign && has_digits && r.peek_byte() == Some(b'#') {
62        let base_bytes = r.range(digit_start..r.offset()).ok_or(Error::SyntaxError)?;
63        let base_str = core::str::from_utf8(base_bytes).map_err(|_| Error::SyntaxError)?;
64        let base = base_str.parse::<u32>().map_err(|_| Error::SyntaxError)?;
65
66        if !(2..=36).contains(&base) {
67            return Err(Error::SyntaxError);
68        }
69
70        // Skip `#`.
71        r.forward();
72
73        let num_start = r.offset();
74        r.forward_while(|b| b.is_ascii_alphanumeric());
75
76        if r.offset() == num_start || !is_terminated(r) {
77            return Err(Error::SyntaxError);
78        }
79
80        let num_bytes = r.range(num_start..r.offset()).ok_or(Error::SyntaxError)?;
81        let num_str = core::str::from_utf8(num_bytes).map_err(|_| Error::SyntaxError)?;
82        let value = i32::from_str_radix(num_str, base).map_err(|_| Error::SyntaxError)?;
83
84        return Ok(Number::Integer(value));
85    }
86
87    // Check for real number indicators: `.` or `e`/`E`.
88    let has_dot = r.peek_byte() == Some(b'.');
89
90    if has_dot {
91        r.forward(); // skip '.'
92        r.forward_while(|b| b.is_ascii_digit());
93    }
94
95    // At this point we need at least some digits (before or after the dot).
96    if !has_digits && !has_dot {
97        return Err(Error::SyntaxError);
98    }
99
100    let has_exponent = matches!(r.peek_byte(), Some(b'e' | b'E'));
101    if has_exponent {
102        r.forward();
103
104        // Optional exponent sign.
105        if matches!(r.peek_byte(), Some(b'+' | b'-')) {
106            r.forward();
107        }
108
109        r.forward_while(|b| b.is_ascii_digit());
110    }
111
112    if !is_terminated(r) {
113        return Err(Error::SyntaxError);
114    }
115
116    let token = r.range(saved..r.offset()).ok_or(Error::SyntaxError)?;
117    let str = core::str::from_utf8(token).map_err(|_| Error::SyntaxError)?;
118
119    if has_dot || has_exponent {
120        let value = str.parse::<f32>().map_err(|_| Error::SyntaxError)?;
121
122        Ok(Number::Real(value))
123    } else {
124        if !has_digits {
125            return Err(Error::SyntaxError);
126        }
127
128        let value = str.parse::<i32>().map_err(|_| Error::SyntaxError)?;
129
130        Ok(Number::Integer(value))
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    fn read_num(input: &[u8]) -> Result<Number> {
139        let mut r = Reader::new(input);
140        read(&mut r)
141    }
142
143    #[test]
144    fn signed_integers() {
145        assert_eq!(read_num(b"123 ").unwrap(), Number::Integer(123));
146        assert_eq!(read_num(b"-98 ").unwrap(), Number::Integer(-98));
147        assert_eq!(read_num(b"43445 ").unwrap(), Number::Integer(43445));
148        assert_eq!(read_num(b"0 ").unwrap(), Number::Integer(0));
149        assert_eq!(read_num(b"+17 ").unwrap(), Number::Integer(17));
150    }
151
152    #[test]
153    fn real_numbers() {
154        assert_eq!(read_num(b"-.002 ").unwrap(), Number::Real(-0.002));
155        assert_eq!(read_num(b"34.5 ").unwrap(), Number::Real(34.5));
156        assert_eq!(read_num(b"-3.62 ").unwrap(), Number::Real(-3.62));
157        assert_eq!(read_num(b"123.6e10 ").unwrap(), Number::Real(123.6e10));
158        assert_eq!(read_num(b"1.0E-5 ").unwrap(), Number::Real(1.0E-5));
159        assert_eq!(read_num(b"1E6 ").unwrap(), Number::Real(1E6));
160        assert_eq!(read_num(b"-1. ").unwrap(), Number::Real(-1.0));
161        assert_eq!(read_num(b"0.0 ").unwrap(), Number::Real(0.0));
162    }
163
164    #[test]
165    fn radix_numbers() {
166        assert_eq!(read_num(b"8#1777 ").unwrap(), Number::Integer(0o1777));
167        assert_eq!(read_num(b"16#FFFE ").unwrap(), Number::Integer(0xFFFE));
168        assert_eq!(read_num(b"2#1000 ").unwrap(), Number::Integer(0b1000));
169    }
170
171    #[test]
172    fn invalid() {
173        assert!(read_num(b"abc").is_err());
174        assert!(read_num(b"+abc").is_err());
175        assert!(read_num(b"1a").is_err());
176    }
177}