geode/parse/
unit.rs

1use itertools::{structs::TupleWindows, Itertools};
2use std::{iter, usize};
3
4use thiserror::Error;
5
6#[derive(Error, Debug, PartialEq)]
7pub enum FormatUnitError {
8    #[error("no unit supplied")]
9    EmptyUnit,
10    #[error("missing unit")]
11    MissingUnit,
12    #[error("an exponentiation indicator '^' can only directly follow an unit")]
13    BadExpIndicatorPosition,
14    #[error("bad sign position")]
15    BadSignPosition,
16}
17
18/// Formats an unit to use proper notation.
19pub fn format_unit(unit: &str) -> Result<String, FormatUnitError> {
20    const OPS: &str = "^+⁺-⁻";
21    let unit = unit.trim();
22    if unit.len() == 0 {
23        return Err(FormatUnitError::EmptyUnit);
24    }
25    let mut chars = unit.chars().peekable();
26    let first_char = *chars.peek().expect("unit length has been checked");
27    if "^+⁺-⁻".contains(first_char) {
28        return Err(FormatUnitError::MissingUnit);
29    }
30    Ok(iter::once(Ok(first_char))
31        .chain(
32            chars
33                .chain(iter::once(' '))
34                .tuple_windows()
35                .filter(|(b, c, a)| *c != ' ' || *a != ' ')
36                .filter_map(|(c1, c2, c3)| match (c1, c2, c3) {
37                    (c, '^', _) if !c.is_ascii_digit() && !"+⁺-⁻".contains(c) => None,
38                    (_, '^', _) => Some(Err(FormatUnitError::BadExpIndicatorPosition)),
39                    (c, '+', _) if !c.is_ascii_digit() => None,
40                    (_, '+', _) => Some(Err(FormatUnitError::BadSignPosition)),
41                    (c, '⁺', _) if !c.is_ascii_digit() => None,
42                    (_, '⁺', _) => Some(Err(FormatUnitError::BadSignPosition)),
43                    (c, '-', _) if !c.is_ascii_digit() => Some(Ok('⁻')),
44                    (_, '-', _) => Some(Err(FormatUnitError::BadSignPosition)),
45                    (c, '⁻', _) if !c.is_ascii_digit() => Some(Ok('⁻')),
46                    (_, '⁻', _) => Some(Err(FormatUnitError::BadSignPosition)),
47                    (_, ' ', a) if a.is_numeric() || OPS.contains(a) => None,
48                    (c, '-', _) => Some(Ok('⁻')),
49                    (_, '*', _) => Some(Ok('·')),
50                    (_, '.', _) => Some(Ok('⋅')),
51                    _ => Some(Ok(c2.to_superscript_digit().unwrap_or(c2))),
52                }),
53        )
54        .collect::<Result<_, _>>()?)
55}
56
57#[derive(Error, Debug, PartialEq)]
58#[error("not a valid digit to superscript")]
59pub struct NotADigit;
60
61pub trait CharUtils {
62    fn to_superscript_digit(self) -> Result<char, NotADigit>;
63}
64
65impl CharUtils for char {
66    fn to_superscript_digit(self) -> Result<char, NotADigit> {
67        digit_char_to_superscript(self)
68    }
69}
70
71/// Converts a digit character to its superscript form.
72pub const fn digit_char_to_superscript(d: char) -> Result<char, NotADigit> {
73    const SUPER_DIGITS: [char; 10] = ['⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹'];
74    if let Some(d) = d.to_digit(10) {
75        Ok(SUPER_DIGITS[d as usize])
76    } else {
77        Err(NotADigit)
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    mod digit_char_to_superscript {
86        use super::*;
87
88        #[test]
89        fn valid_digit() {
90            assert_eq!(digit_char_to_superscript('0'), Ok('⁰'));
91            assert_eq!(digit_char_to_superscript('1'), Ok('¹'));
92            assert_eq!(digit_char_to_superscript('2'), Ok('²'));
93            assert_eq!(digit_char_to_superscript('3'), Ok('³'));
94            assert_eq!(digit_char_to_superscript('4'), Ok('⁴'));
95            assert_eq!(digit_char_to_superscript('5'), Ok('⁵'));
96            assert_eq!(digit_char_to_superscript('6'), Ok('⁶'));
97            assert_eq!(digit_char_to_superscript('7'), Ok('⁷'));
98            assert_eq!(digit_char_to_superscript('8'), Ok('⁸'));
99            assert_eq!(digit_char_to_superscript('9'), Ok('⁹'));
100        }
101
102        #[test]
103        fn invalid_digit() {
104            assert!(digit_char_to_superscript('⁹').is_err());
105            assert!(digit_char_to_superscript('a').is_err());
106        }
107    }
108
109    mod format_unit {
110        use super::*;
111        fn ok(s: &str) -> Result<String, FormatUnitError> {
112            Ok(String::from(s))
113        }
114
115        #[test]
116        fn empty_string() {
117            // Placeholder test
118            assert_eq!(format_unit(""), Err(FormatUnitError::EmptyUnit));
119            assert_eq!(
120                format_unit("     \n \r\t   "),
121                Err(FormatUnitError::EmptyUnit)
122            );
123        }
124
125        #[test]
126        fn valid_unit() {
127            assert_eq!(format_unit("m"), ok("m"));
128            assert_eq!(format_unit("m²"), ok("m²"));
129        }
130
131        #[test]
132        fn bad_format_unit() {
133            assert_eq!(format_unit("m^2"), ok("m²"));
134        }
135        #[test]
136        fn neg_exponent() {
137            assert_eq!(format_unit("m^-1"), ok("m⁻¹"));
138            assert_eq!(format_unit("m-1"), ok("m⁻¹"));
139        }
140        #[test]
141        fn pos_exponent() {
142            assert_eq!(format_unit("m^+1"), ok("m¹"));
143            assert_eq!(format_unit("m+1"), ok("m¹"));
144        }
145
146        #[test]
147        fn missing_unit() {
148            assert_eq!(format_unit("^2"), Err(FormatUnitError::MissingUnit));
149        }
150        #[test]
151        fn bad_exp_position() {
152            assert_eq!(
153                format_unit("m^2^4"),
154                Err(FormatUnitError::BadExpIndicatorPosition)
155            );
156            assert_eq!(
157                format_unit("m-^-4"),
158                Err(FormatUnitError::BadExpIndicatorPosition)
159            );
160            assert_eq!(
161                format_unit("m^+^4"),
162                Err(FormatUnitError::BadExpIndicatorPosition)
163            );
164        }
165        #[test]
166        fn bad_sign_position() {
167            assert_eq!(format_unit("m+2+4"), Err(FormatUnitError::BadSignPosition));
168            assert_eq!(format_unit("m-2-4"), Err(FormatUnitError::BadSignPosition));
169        }
170        #[test]
171        fn good_sign_position() {
172            assert_eq!(format_unit("m^-1"), ok("m⁻¹"))
173        }
174        #[test]
175        fn valid_complex_units() {
176            assert_eq!(format_unit("m.kg^2/s-2"), ok("m⋅kg²/s⁻²"));
177        }
178        #[test]
179        fn remove_redondant_spaces() {
180            assert_eq!(
181                format_unit("   m   .   kg     ^     -      2     /     s    ^   4   "),
182                ok("m ⋅ kg⁻² / s⁴")
183            );
184        }
185    }
186}