1use num_complex::Complex64;
2use powerio_core::{Diagnostic, Error};
3
4use super::{GlmDialect, GlmProject, GlmProperty, GlmPropertyValue, TokenKind, units};
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum GlmQuantityKind {
10 Real,
11 Complex,
12}
13
14#[derive(Clone, Debug)]
17pub struct GlmQuantity {
18 pub value: Complex64,
19 pub diagnostics: Vec<Diagnostic>,
20}
21
22impl GlmProject {
23 pub fn quantity(
33 &self,
34 property: &GlmProperty<'_>,
35 kind: GlmQuantityKind,
36 native_unit: Option<&str>,
37 ) -> Result<GlmQuantity, Error> {
38 let fail = |message| self.quantity_error(property.span, message);
39 let GlmPropertyValue::Tokens(tokens) = &property.value else {
40 return Err(fail("quantity cannot be an inline object"));
41 };
42 let quoted = tokens.len() == 1 && tokens[0].kind() == TokenKind::Quoted;
43 let text = if let [token] = *tokens
44 && token.kind() == TokenKind::Quoted
45 {
46 token
47 .text()
48 .strip_prefix('"')
49 .and_then(|s| s.strip_suffix('"'))
50 .ok_or_else(|| fail("quantity strings require double quotes"))?
51 .to_owned()
52 } else {
53 let mut text = String::new();
54 for token in *tokens {
55 if token.kind() != TokenKind::Word {
56 return Err(fail("quantity contains nonliteral syntax"));
57 }
58 if text.len() + token.text().len() + 1 > 1 << 20 {
59 return Err(fail("quantity exceeds 1 MiB"));
60 }
61 if !text.is_empty() {
62 text.push(' ');
63 }
64 text.push_str(token.text());
65 }
66 text
67 };
68 let (value, ignored) =
69 parse(&text, kind, native_unit, self.dialect(), quoted).map_err(fail)?;
70 let diagnostics = if let Some(suffix) = ignored {
71 vec![self.ignored_quantity_unit(property.span, suffix)?]
72 } else {
73 Vec::new()
74 };
75 Ok(GlmQuantity { value, diagnostics })
76 }
77}
78
79fn parse<'a>(
80 text: &'a str,
81 kind: GlmQuantityKind,
82 unit: Option<&str>,
83 dialect: GlmDialect,
84 quoted: bool,
85) -> Result<(Complex64, Option<&'a str>), &'static str> {
86 if quoted && text.is_empty() && kind == GlmQuantityKind::Complex {
87 return Ok((Complex64::new(0.0, 0.0), None));
88 }
89 let text = text.trim();
90 let (text, parenthesized) = if text.starts_with('(') {
91 if !quoted || kind != GlmQuantityKind::Complex || dialect != GlmDialect::Arras4_3 {
92 return Err("parenthesized quantity requires a quoted Arras complex literal");
93 }
94 (
95 text.strip_prefix('(')
96 .and_then(|s| s.strip_suffix(')'))
97 .ok_or("parenthesized complex literal must have no trailing suffix")?,
98 true,
99 )
100 } else {
101 (text, false)
102 };
103 let (first, rest) = number(text)?;
104 if kind == GlmQuantityKind::Real {
105 let suffix = rest.trim();
106 let value = if suffix.is_empty() {
107 first
108 } else if let Some(unit) = unit {
109 units::convert(first, suffix, unit)?
110 } else if !quoted {
111 units::convert(1.0, suffix, suffix)?;
112 return Ok((Complex64::new(first, 0.0), Some(suffix)));
113 } else {
114 return Err("quoted real property does not accept units");
115 };
116 return Ok((Complex64::new(value, 0.0), None));
117 }
118 let rest = rest.trim_start();
119 let starts_number = rest
122 .as_bytes()
123 .first()
124 .is_some_and(|c| c.is_ascii_digit() || matches!(c, b'+' | b'-' | b'.'));
125 if !starts_number {
126 if parenthesized && !rest.is_empty() {
127 return Err("invalid parenthesized complex literal");
128 }
129 let suffix = rest.trim();
130 let value = Complex64::new(first, 0.0);
131 return if quoted {
132 Ok((value, (!suffix.is_empty()).then_some(suffix)))
133 } else {
134 convert_complex(value, suffix, unit, parenthesized)
135 };
136 }
137 let (second, rest) = number(rest)?;
138 let notation = rest
139 .as_bytes()
140 .first()
141 .copied()
142 .ok_or("complex value lacks i, j, d or r notation")?;
143 let value = match notation {
144 b'i' | b'j' => Complex64::new(first, second),
145 b'd' => Complex64::from_polar(first, second.to_radians()),
146 b'r' => Complex64::from_polar(first, second),
147 _ => return Err("complex value lacks i, j, d or r notation"),
148 };
149 let suffix = rest[1..].trim();
150 if parenthesized && !suffix.is_empty() {
151 return Err("parenthesized complex literal cannot contain a unit suffix");
152 }
153 convert_complex(value, suffix, unit, quoted || parenthesized)
154}
155
156fn convert_complex<'a>(
157 value: Complex64,
158 suffix: &'a str,
159 unit: Option<&str>,
160 string_conversion: bool,
161) -> Result<(Complex64, Option<&'a str>), &'static str> {
162 let (value, ignored) = if suffix.is_empty() {
163 (value, None)
164 } else if let Some(unit) = unit {
165 let converted = if string_conversion {
166 value * units::convert(1.0, suffix, unit)?
167 } else {
168 Complex64::new(
169 units::convert(value.re, suffix, unit)?,
170 units::convert(value.im, suffix, unit)?,
171 )
172 };
173 (converted, None)
174 } else {
175 if !string_conversion {
176 units::convert(1.0, suffix, suffix)?;
177 }
178 (value, Some(suffix))
179 };
180 if !value.re.is_finite() || !value.im.is_finite() {
181 return Err("complex conversion produced a nonfinite value");
182 }
183 Ok((value, ignored))
184}
185
186fn number(text: &str) -> Result<(f64, &str), &'static str> {
189 let bytes = text.as_bytes();
190 let mut end = usize::from(bytes.first().is_some_and(|b| matches!(b, b'+' | b'-')));
191 let mut digits = 0;
192 while bytes.get(end).is_some_and(u8::is_ascii_digit) {
193 end += 1;
194 digits += 1;
195 }
196 if bytes.get(end) == Some(&b'.') {
197 end += 1;
198 while bytes.get(end).is_some_and(u8::is_ascii_digit) {
199 end += 1;
200 digits += 1;
201 }
202 }
203 if digits == 0 {
204 return Err("expected a finite decimal quantity");
205 }
206 if bytes.get(end).is_some_and(|b| matches!(b, b'e' | b'E')) {
207 end += 1;
208 if bytes.get(end).is_some_and(|b| matches!(b, b'+' | b'-')) {
209 end += 1;
210 }
211 let exponent_start = end;
212 while bytes.get(end).is_some_and(u8::is_ascii_digit) {
213 end += 1;
214 }
215 if end == exponent_start {
216 return Err("quantity exponent has no digits");
217 }
218 }
219 let value = text[..end]
220 .parse::<f64>()
221 .map_err(|_| "invalid quantity number")?;
222 if !value.is_finite() {
223 return Err("quantity must be finite");
224 }
225 Ok((value, &text[end..]))
226}