Skip to main content

bacnet_ops/parse/
typed.rs

1//! Values typed for a known property: its enumeration by name, its bit
2//! string by named bits, a date-time or a timestamp, each in the form the
3//! formatter prints, so a value read can be pasted back as a write.
4
5use bacnet_encoding::constructed::{ConstructedElement, ElementShape, property_element};
6use bacnet_encoding::primitives::{encode_priority_date_time, encode_timestamp_choice};
7use bacnet_types::bitstring::{EventTransitionBits, LimitEnable, StatusFlags};
8use bacnet_types::enums::{
9    BinaryPV, LifeSafetyState, ObjectType, PropertyIdentifier, ResolvedBits, ResolvedEnum,
10    ServiceSupported,
11};
12use bacnet_types::primitives::{Date, PropertyValue, Time};
13use bytes::BytesMut;
14
15use super::datetime::{parse_date_time, parse_time, parse_timestamp};
16use super::numeric::{Number, coerce, finite, numeric};
17use super::{json, parse_value, split_priority};
18use crate::format::{holds_state, is_binary, is_date_time_value, is_multistate};
19use crate::label::kebab;
20
21/// Names listed in full when a typed name is wrong; a longer list gives
22/// the nearest few.
23const MAX_LISTED: usize = 12;
24
25/// Nearest names offered for a long list.
26const NEAREST: usize = 5;
27
28/// An enumeration: its type name and named values.
29struct Enumeration {
30    type_name: &'static str,
31    names: Vec<(&'static str, u32)>,
32}
33
34/// A named bit string: each name's bit, and how many bits it has.
35struct NamedBits {
36    type_name: &'static str,
37    names: Vec<(&'static str, usize)>,
38    length: usize,
39}
40
41/// What a property holds, as far as typed input needs to know.
42enum Kind {
43    /// An enumeration; `states` when an object's state text may follow
44    /// the name, `active (On)`.
45    Enum {
46        enumeration: Enumeration,
47        states: bool,
48    },
49    /// A multi-state object's state number, maybe with its text after it.
50    State,
51    Bits(NamedBits),
52    /// A BACnetDateTime: a date then a time.
53    DateTime,
54    /// A DateTime Value's Priority_Array slot: the BACnetPriorityValue
55    /// choice of a date-time, under context tag 1.
56    PriorityDateTime,
57    /// A number sent as the property's numeric datatype.
58    Number(Number),
59    /// One BACnetTimeStamp.
60    TimeStamp,
61    /// A constructed element typed as JSON, or a list of them when
62    /// `whole`.
63    Json {
64        element: ConstructedElement,
65        whole: bool,
66    },
67    /// Nothing known: the untyped grammar.
68    Untyped,
69}
70
71/// The enumeration a resolved value belongs to.
72fn enumeration(value: ResolvedEnum) -> Option<Enumeration> {
73    value
74        .enumeration()
75        .map(|(type_name, names)| Enumeration { type_name, names })
76}
77
78/// The bit names of a bitflags type, in declaration order, which is their
79/// order on the wire (bit 0 first).
80fn flag_names<I: Iterator<Item = (&'static str, F)>, F>(names: I) -> Vec<(&'static str, usize)> {
81    names
82        .enumerate()
83        .map(|(bit, (name, _))| (name, bit))
84        .collect()
85}
86
87/// The named bit string a property holds, if any.
88fn named_bits(property: PropertyIdentifier) -> Option<NamedBits> {
89    let (type_name, names) = match ResolvedBits::from_property(property, 0, &[]) {
90        ResolvedBits::StatusFlags(_) => {
91            ("StatusFlags", flag_names(StatusFlags::all().iter_names()))
92        }
93        ResolvedBits::EventTransitionBits(_) => (
94            "EventTransitionBits",
95            flag_names(EventTransitionBits::all().iter_names()),
96        ),
97        ResolvedBits::LimitEnable(_) => {
98            ("LimitEnable", flag_names(LimitEnable::all().iter_names()))
99        }
100        ResolvedBits::ServicesSupported(_) => (
101            "ServicesSupported",
102            ServiceSupported::ALL_NAMED
103                .iter()
104                .map(|&(name, value)| (name, usize::from(value.to_raw())))
105                .collect(),
106        ),
107        ResolvedBits::ObjectTypesSupported(_) => (
108            "ObjectTypesSupported",
109            ObjectType::ALL_NAMED
110                .iter()
111                .map(|&(name, value)| (name, value.to_raw() as usize))
112                .collect(),
113        ),
114        ResolvedBits::Unknown { .. } => return None,
115    };
116    let length = names.iter().map(|&(_, bit)| bit + 1).max().unwrap_or(0);
117    Some(NamedBits {
118        type_name,
119        names,
120        length,
121    })
122}
123
124/// The properties that hold a BACnetDateTime, by object type (the
125/// property tables of Clause 12).
126fn holds_date_time(object_type: ObjectType, property: PropertyIdentifier) -> bool {
127    type O = ObjectType;
128    type P = PropertyIdentifier;
129    match property {
130        P::CHANGE_OF_STATE_TIME | P::TIME_OF_STATE_COUNT_RESET | P::TIME_OF_ACTIVE_TIME_RESET => {
131            is_binary(object_type)
132        }
133        P::START_TIME => [
134            O::TREND_LOG,
135            O::TREND_LOG_MULTIPLE,
136            O::EVENT_LOG,
137            O::LOAD_CONTROL,
138        ]
139        .contains(&object_type),
140        P::STOP_TIME => [O::TREND_LOG, O::TREND_LOG_MULTIPLE, O::EVENT_LOG].contains(&object_type),
141        P::MODIFICATION_DATE => object_type == O::FILE,
142        P::UPDATE_TIME => [O::PULSE_CONVERTER, O::TIMER].contains(&object_type),
143        P::COUNT_CHANGE_TIME => object_type == O::PULSE_CONVERTER,
144        P::EXPIRATION_TIME => [O::TIMER, O::ACCESS_CREDENTIAL].contains(&object_type),
145        P::ACTIVATION_TIME => object_type == O::ACCESS_CREDENTIAL,
146        P::VALUE_CHANGE_TIME => object_type == O::ACCUMULATOR,
147        P::PRESENT_VALUE | P::RELINQUISH_DEFAULT => is_date_time_value(object_type),
148        _ => false,
149    }
150}
151
152/// Whether an object type is a life safety point or zone.
153fn is_life_safety(object_type: ObjectType) -> bool {
154    [ObjectType::LIFE_SAFETY_POINT, ObjectType::LIFE_SAFETY_ZONE].contains(&object_type)
155}
156
157/// The life safety properties whose values are a BACnetLifeSafetyState.
158fn holds_life_safety_state(object_type: ObjectType, property: PropertyIdentifier) -> bool {
159    is_life_safety(object_type)
160        && [
161            PropertyIdentifier::PRESENT_VALUE,
162            PropertyIdentifier::ALARM_VALUES,
163            PropertyIdentifier::FAULT_VALUES,
164            PropertyIdentifier::LIFE_SAFETY_ALARM_VALUES,
165        ]
166        .contains(&property)
167}
168
169fn kind(object_type: ObjectType, property: PropertyIdentifier, index: Option<u32>) -> Kind {
170    // Index 0 is an array's size, a count.
171    if index == Some(0) {
172        return Kind::Untyped;
173    }
174    if let Some(number) = numeric(object_type, property, index) {
175        return Kind::Number(number);
176    }
177    if property == PropertyIdentifier::PRIORITY_ARRAY
178        && index.is_some()
179        && is_date_time_value(object_type)
180    {
181        return Kind::PriorityDateTime;
182    }
183    if holds_state(property) && is_binary(object_type) {
184        let enumeration = enumeration(ResolvedEnum::BinaryPV(BinaryPV::INACTIVE));
185        return enumeration.map_or(Kind::Untyped, |enumeration| Kind::Enum {
186            enumeration,
187            states: true,
188        });
189    }
190    if holds_state(property) && is_multistate(object_type) {
191        return Kind::State;
192    }
193    if holds_life_safety_state(object_type, property) {
194        let state = ResolvedEnum::LifeSafetyState(LifeSafetyState::QUIET);
195        if let Some(enumeration) = enumeration(state) {
196            return Kind::Enum {
197                enumeration,
198                states: false,
199            };
200        }
201    }
202    // Tracking_Value is a BACnetLifeSafetyState only on the life safety
203    // objects; a Lighting Output's is a REAL.
204    let tracking = property == PropertyIdentifier::TRACKING_VALUE;
205    if (!tracking || is_life_safety(object_type))
206        && let Some(enumeration) = enumeration(ResolvedEnum::from_property(property, 0))
207    {
208        return Kind::Enum {
209            enumeration,
210            states: false,
211        };
212    }
213    if let Some(bits) = named_bits(property) {
214        return Kind::Bits(bits);
215    }
216    if holds_date_time(object_type, property) {
217        return Kind::DateTime;
218    }
219    match property_element(object_type, property) {
220        Some((ConstructedElement::TimeStamp, shape))
221            if shape == ElementShape::Single || index.is_some() =>
222        {
223            Kind::TimeStamp
224        }
225        Some((element, shape)) if json::takes_json(element) => Kind::Json {
226            element,
227            whole: shape == ElementShape::Collection && index.is_none(),
228        },
229        _ => Kind::Untyped,
230    }
231}
232
233/// Whether `text` is one of the untyped grammar's explicit forms, which
234/// say their own datatype whatever the property.
235fn explicit(text: &str) -> bool {
236    ["null", "true", "false"]
237        .iter()
238        .any(|word| text.eq_ignore_ascii_case(word))
239        || text.starts_with('"')
240        || ["enumerated:", "date:", "time:", "object:", "bits("]
241            .iter()
242            .any(|prefix| text.starts_with(prefix))
243}
244
245/// A name with its separators dropped and its case folded, so
246/// `IN_ALARM`, `in-alarm` and `InAlarm` compare equal.
247fn folded(name: &str) -> String {
248    name.chars()
249        .filter(|c| *c != '_' && *c != '-')
250        .map(|c| c.to_ascii_lowercase())
251        .collect()
252}
253
254/// Edit distance between two names.
255fn distance(a: &str, b: &str) -> usize {
256    let b: Vec<char> = b.chars().collect();
257    let mut row: Vec<usize> = (0..=b.len()).collect();
258    for (i, ca) in a.chars().enumerate() {
259        let mut previous = row[0];
260        row[0] = i + 1;
261        for (j, cb) in b.iter().enumerate() {
262            let substitute = previous + usize::from(ca != *cb);
263            previous = row[j + 1];
264            row[j + 1] = substitute.min(row[j] + 1).min(row[j + 1] + 1);
265        }
266    }
267    row[b.len()]
268}
269
270/// The error for a name that isn't one of `names`: every name when there
271/// are few, the nearest few otherwise, all in kebab case.
272fn unknown_name<'a>(
273    what: &str,
274    typed: &str,
275    names: impl Iterator<Item = &'a str>,
276    number: &str,
277) -> String {
278    let names: Vec<&str> = names.collect();
279    let (label, shown): (&str, Vec<&str>) = if names.len() <= MAX_LISTED {
280        ("expected one of", names)
281    } else {
282        let typed = folded(typed);
283        let mut ranked: Vec<(bool, usize, &str)> = names
284            .into_iter()
285            .map(|name| {
286                let name_folded = folded(name);
287                (
288                    !name_folded.contains(&typed),
289                    distance(&typed, &name_folded),
290                    name,
291                )
292            })
293            .collect();
294        ranked.sort();
295        (
296            "nearest",
297            ranked
298                .into_iter()
299                .take(NEAREST)
300                .map(|(_, _, n)| n)
301                .collect(),
302        )
303    };
304    let shown: Vec<String> = shown.into_iter().map(kebab).collect();
305    format!(
306        "unknown {what} '{typed}'; {label}: {}{number}",
307        shown.join(", ")
308    )
309}
310
311/// An enumeration value by name in any common spelling, or its number.
312fn enum_value(enumeration: &Enumeration, text: &str) -> Result<u32, String> {
313    if !text.is_empty() && text.bytes().all(|b| b.is_ascii_digit()) {
314        return text
315            .parse::<u32>()
316            .map_err(|_| format!("{text} is out of an enumeration's range (0-4294967295)"));
317    }
318    let typed = folded(text);
319    enumeration
320        .names
321        .iter()
322        .find(|(name, _)| folded(name) == typed)
323        .map(|&(_, value)| value)
324        .ok_or_else(|| {
325            unknown_name(
326                enumeration.type_name,
327                text,
328                enumeration.names.iter().map(|(name, _)| *name),
329                " (or a number)",
330            )
331        })
332}
333
334/// A state name with the object's text after it, `active (On)`, as the
335/// name alone. The first ` (` starts the text, since no state name or
336/// number holds one; the text itself may (`2 (Low (eco))`).
337fn without_state_text(text: &str) -> &str {
338    match text.find(" (") {
339        Some(at) if text.ends_with(')') => text[..at].trim_end(),
340        _ => text,
341    }
342}
343
344/// A list's items split at the commas outside parentheses, so a state's
345/// text keeps its own commas: `active (On, Auto), inactive`.
346fn list_items(inner: &str) -> Vec<&str> {
347    let mut items = Vec::new();
348    let (mut depth, mut start) = (0usize, 0);
349    for (at, c) in inner.char_indices() {
350        match c {
351            '(' => depth += 1,
352            ')' => depth = depth.saturating_sub(1),
353            ',' if depth == 0 => {
354                items.push(&inner[start..at]);
355                start = at + 1;
356            }
357            _ => {}
358        }
359    }
360    items.push(&inner[start..]);
361    items
362}
363
364/// An enumeration value, or a bracketed list of them as the formatter
365/// prints a list, `[high-limit, low-limit]`.
366fn parse_enum(
367    enumeration: &Enumeration,
368    states: bool,
369    text: &str,
370) -> Result<PropertyValue, String> {
371    let one = |text: &str| {
372        let text = text.trim();
373        let text = if states {
374            without_state_text(text)
375        } else {
376            text
377        };
378        enum_value(enumeration, text.trim()).map(PropertyValue::Enumerated)
379    };
380    match text.strip_prefix('[').and_then(|t| t.strip_suffix(']')) {
381        Some(inner) if inner.trim().is_empty() => Ok(PropertyValue::List(Vec::new())),
382        Some(inner) => list_items(inner)
383            .into_iter()
384            .map(one)
385            .collect::<Result<_, _>>()
386            .map(PropertyValue::List),
387        None => one(text),
388    }
389}
390
391/// A bit string from its set bits' names (`in-alarm,fault` or
392/// `in-alarm | fault`), its bits as 0/1 digits first bit first, or
393/// `none` for no bit set.
394fn parse_bits(bits: &NamedBits, text: &str) -> Result<PropertyValue, String> {
395    let mut set = vec![false; bits.length];
396    // An empty value is refused rather than read as no bit set: a blank
397    // field would otherwise clear every bit.
398    if text.is_empty() {
399        return Err(format!(
400            "a {} value is needed: its bits' names, the bits as 0/1 digits, or none",
401            bits.type_name
402        ));
403    }
404    let digits = text.bytes().all(|b| b == b'0' || b == b'1');
405    if digits {
406        set = text.bytes().map(|b| b == b'1').collect();
407    } else if !text.eq_ignore_ascii_case("none") {
408        for name in text.split([',', '|']).map(str::trim) {
409            if name.is_empty() {
410                continue;
411            }
412            let typed = folded(name);
413            let bit = bits
414                .names
415                .iter()
416                .find(|(known, _)| folded(known) == typed)
417                .map(|&(_, bit)| bit)
418                .ok_or_else(|| {
419                    unknown_name(
420                        &format!("{} bit", bits.type_name),
421                        name,
422                        bits.names.iter().map(|(known, _)| *known),
423                        " (or the bits as 0/1 digits, or none)",
424                    )
425                })?;
426            set[bit] = true;
427        }
428    }
429    Ok(bit_string(&set))
430}
431
432/// Bits, first bit first, as a BitString.
433pub(super) fn bit_string(set: &[bool]) -> PropertyValue {
434    let mut data = vec![0u8; set.len().div_ceil(8)];
435    for (bit, _) in set.iter().enumerate().filter(|(_, on)| **on) {
436        data[bit / 8] |= 0x80 >> (bit % 8);
437    }
438    PropertyValue::BitString {
439        unused_bits: (data.len() * 8 - set.len()) as u8,
440        data,
441    }
442}
443
444/// A date and a time, the time unspecified when none is typed.
445fn parse_date_time_value(text: &str) -> Result<PropertyValue, String> {
446    let (date, time) = parse_date_and_time(text)?;
447    Ok(PropertyValue::List(vec![
448        PropertyValue::Date(date),
449        PropertyValue::Time(time),
450    ]))
451}
452
453/// The typed date with its time, which is unspecified when only the date is given.
454fn parse_date_and_time(text: &str) -> Result<(Date, Time), String> {
455    let (date, time) = parse_date_time(text)?;
456    let time: Time = match time {
457        Some(time) => time,
458        None => parse_time("any time")?,
459    };
460    Ok((date, time))
461}
462
463/// Parse a value typed for `property` of an object of `object_type`, at
464/// `index` when the write names one, knowing the property's datatype:
465///
466/// - an enumeration by name, in kebab case, UPPER_SNAKE or any case
467///   (`offnormal`, `HIGH_LIMIT`), or by number; a binary object's state as
468///   `active`/`inactive`, with or without the object's text after it
469///   (`active (On)`);
470/// - a named bit string by its set bits (`in-alarm,fault`,
471///   `in-alarm | fault`), as 0/1 digits first bit first, or `none` for
472///   no bit set;
473/// - a plain number for the value of an analog or integer object (its
474///   Present_Value, Relinquish_Default, a Priority_Array slot, COV_Increment,
475///   High_Limit, Low_Limit and Deadband) as that property's datatype: REAL
476///   for the analog objects, Double for a Large Analog Value, INTEGER or
477///   Unsigned for the integer objects;
478/// - a BACnetDateTime as `2026-10-19 07:00` (a date alone leaves the time
479///   unspecified), and a timestamp as `seq 42`, a time, or a date and time;
480/// - a Weekly_Schedule day, an Exception_Schedule special event, a
481///   Date_List calendar entry or a Recipient_List destination as JSON, or
482///   the whole list of them for a write of the whole property, in the
483///   shapes of the Python bindings' typed constructors (`docs/CLI.md`,
484///   "Constructed values as JSON");
485/// - anything else, and the explicit forms (`null`, `true`, a quoted
486///   string, `enumerated:N`, `date:`, `time:`, `object:`, `bits(0101)`),
487///   as [`parse_value`] reads them.
488///
489/// A mistyped name is refused with the valid names, or the nearest few
490/// when there are many.
491pub fn parse_value_for(
492    object_type: ObjectType,
493    property: PropertyIdentifier,
494    index: Option<u32>,
495    text: &str,
496) -> Result<PropertyValue, String> {
497    let text = text.trim();
498    if explicit(text) {
499        return parse_value(text);
500    }
501    match kind(object_type, property, index) {
502        Kind::Enum {
503            enumeration,
504            states,
505        } => parse_enum(&enumeration, states, text),
506        Kind::State => {
507            let state = without_state_text(text);
508            state.parse::<u64>().map(PropertyValue::Unsigned).map_err(|_| {
509                format!("'{state}' is not a state number; a multi-state state is its number, from 1, its text allowed after it: `2 (Heat)`")
510            })
511        }
512        Kind::Number(Number::Double) => match text.parse::<f64>() {
513            // Read straight as a Double, so no digit is lost through a REAL.
514            Ok(double) if text.contains(['.', 'e', 'E']) => finite(PropertyValue::Double(double)),
515            _ => coerce(Number::Double, parse_value(text)?),
516        },
517        Kind::Number(number) => coerce(number, parse_value(text)?),
518        Kind::PriorityDateTime => {
519            let (date, time) = parse_date_and_time(text)?;
520            let mut buf = BytesMut::new();
521            encode_priority_date_time(&mut buf, &date, &time);
522            Ok(PropertyValue::ApplicationData(buf.to_vec()))
523        }
524        Kind::Bits(bits) => parse_bits(&bits, text),
525        Kind::DateTime => parse_date_time_value(text),
526        Kind::TimeStamp => {
527            let stamp = parse_timestamp(text)?;
528            let mut buf = BytesMut::new();
529            encode_timestamp_choice(&mut buf, &stamp)
530                .map_err(|e| format!("cannot encode the timestamp: {e}"))?;
531            Ok(PropertyValue::ApplicationData(buf.to_vec()))
532        }
533        Kind::Json { element, whole } if text.starts_with(['[', '{']) => {
534            json::parse(element, whole, text)
535        }
536        Kind::Json { .. } | Kind::Untyped => parse_value(text),
537    }
538}
539
540/// [`parse_value_for`] with an optional `@priority` suffix, as
541/// [`parse_value_with_priority`](super::parse_value_with_priority) splits
542/// it.
543pub fn parse_value_for_with_priority(
544    object_type: ObjectType,
545    property: PropertyIdentifier,
546    index: Option<u32>,
547    text: &str,
548) -> Result<(PropertyValue, Option<u8>), String> {
549    let (value, priority) = split_priority(text)?;
550    Ok((
551        parse_value_for(object_type, property, index, value)?,
552        priority,
553    ))
554}