Skip to main content

powerio_dist/glm/
quantities.rs

1use num_complex::Complex64;
2use powerio_core::{Diagnostic, Error};
3
4use super::{GlmDialect, GlmProject, GlmProperty, GlmPropertyValue, TokenKind, units};
5
6/// Native property storage kind. Quoted constants use a different native
7/// conversion path from unquoted GLM expressions.
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum GlmQuantityKind {
10    Real,
11    Complex,
12}
13
14/// A constant in the property's declared native unit. Diagnostics must be
15/// forwarded when projecting into a network, particularly for ignored suffixes.
16#[derive(Clone, Debug)]
17pub struct GlmQuantity {
18    pub value: Complex64,
19    pub diagnostics: Vec<Diagnostic>,
20}
21
22impl GlmProject {
23    /// Read a finite constant using the native property kind and unit. The
24    /// caller supplies the unit from the class schema, not from the input text.
25    /// `None` denotes a property without units, not an unknown unit.
26    ///
27    /// Supports decimal/exponent numbers, rectangular `i`/`j`, polar `d`/`r`,
28    /// and the bounded static electrical/geometry unit profile. Expressions,
29    /// property references and custom unit definitions require further
30    /// interpretation and fail here. Arras additionally accepts quoted,
31    /// parenthesized complex literals. Source bytes and diagnostic spans remain.
32    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    // Native sscanf stops after the first number if the suffix does not start
120    // another number. It consequently never reaches complex unit conversion.
121    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
186// A bounded decimal scanner avoids confusing exponent signs with the second
187// component, while refusing sscanf's partial malformed-number acceptance.
188fn 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}