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
18pub 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
71pub 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 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}