binary_data_schema/
number.rs

1//! Implementation of the number schema
2//!
3//! When binary data is exchanged a common objective is to reduce the data size.
4//! For this the [IEEE 754] formats are not suited.
5//! To reduce the size of a floating-point number linear interpolation can be used to fit a number into a smaller integer.
6//!
7//! # Parameters
8//!
9//! | Key           | Type     | Default  | Comment |
10//! | ------------- | --------:| --------:| ------- |
11//! | `"byteorder"` | `string` | "bigendian" | The order in which the bytes are encoded. |
12//! | `"length"`    |   `uint` |        4 | Number of bytes of the encoded number |
13//! | `"signed"`    |   `bool` |     true | Whether the number is signed or not |
14//! | `"bits"`      |   `uint` | optional | Number of bits the [bitfield] covers |
15//! | `"bitoffset"` |   `uint` | optional | Number of bits the [bitfield] is shifted |
16//! | `"scale"`     | `double` | optional | Factor to scale the encoded value |
17//! | `"offset"`    | `double` | optional | Offset for the encoded value |
18//!
19//! ## Validation
20//!
21//! If none of the optional parameters are provided only a length of 4 or 8 byte are valid (single and double precision floating-point numbers, IEEE 754).
22//! However, there are methods to [encode floating-point numbers as integers](#linear-interpolation).
23//!
24//! # Features
25//!
26//! ## Linear Interpolation
27//!
28//! Linear interpolation is performed if `"bits"`, `"bitoffset"`, `"scale"` or `"offset"` are set.
29//! If `"scale"` is not set the default value 1.0 is assumed.
30//! If `"offset"` is not set the default value 0.0 is assumed.
31//!
32//! For linear interpolation the parameters `"scale"` and `"offset"` are used.
33//! The formulae are as follows:
34//!
35//! - Encoding: `encoded_value = (json_value - offset) / scale`
36//! - Decoding: `json_value = scale * encoded_value + offset`
37//!
38//! An interpolated value is encoded with the integer schema defined by the number schema.
39//! Accordingly, it is also possible to encode an interpolated value in a [bitfield].
40//!
41//! ### Example
42//!
43//! The schema describes that a floating-point JSON value is encoded in the lower 11 bits (max 2047) with a scale of 0.001 and an offset of 1.6.
44//! The calculation to encode 3.0:
45//!
46//! ```text
47//! (3.0 - 1.6) / 0.001 = 1400 = 0b0000_0101_0111_1000 = encoded_value
48//! ```
49//!
50//! ```
51//! # use binary_data_schema::*;
52//! # use valico::json_schema;
53//! # use serde_json::{json, from_value};
54//! let schema = json!({
55//!     "type": "number",
56//!     "offset": 1.6,
57//!     "scale": 0.001,
58//!     "length": 2,
59//!     "bits": 11
60//! });
61//!
62//! let mut scope = json_schema::Scope::new();
63//! let j_schema = scope.compile_and_return(schema.clone(), false)?;
64//! let schema = from_value::<DataSchema>(schema)?;
65//!
66//! let value = json!(3.0);
67//! assert!(j_schema.validate(&value).is_valid());
68//! let mut encoded = Vec::new();
69//! schema.encode(&mut encoded, &value)?;
70//! let expected = [ 0b0000_0101, 0b0111_1000 ];
71//! assert_eq!(&expected, encoded.as_slice());
72//!
73//! let mut encoded = std::io::Cursor::new(encoded);
74//! let back = schema.decode(&mut encoded)?;
75//! assert!(j_schema.validate(&back).is_valid());
76//! // This would fail due to rounding errors
77//! // assert_eq!(back, value);
78//! # Ok::<(), anyhow::Error>(())
79//! ```
80//!
81//! [IEEE 754]: https://ieeexplore.ieee.org/document/8766229
82//! [bitfield]: ../object/index.html#bitfields
83
84use std::{convert::TryFrom, io};
85
86use byteorder::{ReadBytesExt, WriteBytesExt, BE, LE};
87use serde::{
88    de::{Deserializer, Error as DeError},
89    Deserialize,
90};
91use serde_json::Value;
92
93use crate::{integer::RawIntegerSchema, ByteOrder, Decoder, Encoder, IntegerSchema};
94
95/// Errors validating a [NumberSchema].
96#[derive(Debug, thiserror::Error)]
97pub enum ValidationError {
98    #[error("Invalid bitfield: {0}")]
99    InvalidIntergerSchema(#[from] crate::integer::ValidationError),
100    #[error("The requsted length of {requested} is invalid for floating-point serialization. Either use 4 or 8 or use an integer-based encoding")]
101    InvalidFloatingLength { requested: usize },
102}
103
104/// Errors encoding a string with a [NumberSchema].
105#[derive(Debug, thiserror::Error)]
106pub enum EncodingError {
107    #[error("The value '{value}' can not be encoded with a number schema")]
108    InvalidValue { value: String },
109    #[error("Writing to buffer failed: {0}")]
110    WriteFail(#[from] io::Error),
111    #[error("Failed to encode as integer: {0}")]
112    Integer(#[from] crate::integer::EncodingError),
113}
114
115/// Errors decoding a string with a [NumberSchema].
116#[derive(Debug, thiserror::Error)]
117pub enum DecodingError {
118    #[error("Reading encoded data failed: {0}")]
119    ReadFail(#[from] io::Error),
120    #[error("Failed to decode underlying integer: {0}")]
121    Integer(#[from] crate::integer::DecodingError),
122}
123
124impl DecodingError {
125    pub fn due_to_eof(&self) -> bool {
126        matches!(self, Self::ReadFail(e) if e.kind() == std::io::ErrorKind::UnexpectedEof)
127    }
128}
129
130/// Raw version of a number schema. May hold invalid invariants.
131#[derive(Debug, Clone, Deserialize)]
132#[serde(rename_all = "lowercase")]
133struct RawNumber {
134    #[serde(flatten, default)]
135    int: RawIntegerSchema,
136    scale: Option<f64>,
137    offset: Option<f64>,
138}
139
140/// The number schema describes a numeric value (further information on [the module's documentation](index.html)).
141#[derive(Debug, Clone)]
142pub enum NumberSchema {
143    Integer {
144        integer: IntegerSchema,
145        scale: f64,
146        offset: f64,
147    },
148    Float {
149        byteorder: ByteOrder,
150    },
151    Double {
152        byteorder: ByteOrder,
153    },
154}
155
156const DEFAULT_LENGTH: usize = 4;
157const DEFAULT_SCALE: f64 = 1_f64;
158const DEFAULT_OFFSET: f64 = 0_f64;
159
160impl NumberSchema {
161    /// Default length is 8 bytes like a double-precision floating-point number.
162    pub fn default_length() -> usize {
163        DEFAULT_LENGTH
164    }
165    /// Default scale is neutral, i.e. `1.0`.
166    pub fn default_scale() -> f64 {
167        DEFAULT_SCALE
168    }
169    /// Default offset is neutral, i.e. `0.0`.
170    pub fn default_offset() -> f64 {
171        DEFAULT_OFFSET
172    }
173    /// Apply scale and offset to the value.
174    pub fn to_binary_value(&self, value: f64) -> i64 {
175        match self {
176            NumberSchema::Integer { scale, offset, .. } => {
177                ((value - *offset) / *scale).round() as _
178            }
179            _ => value as _,
180        }
181    }
182    /// Apply scale and offset to the value.
183    pub fn from_binary_value(&self, value: f64) -> f64 {
184        match self {
185            NumberSchema::Integer { scale, offset, .. } => value * *scale + *offset,
186            _ => value,
187        }
188    }
189    pub fn length(&self) -> usize {
190        match self {
191            NumberSchema::Integer { integer, .. } => integer.length(),
192            NumberSchema::Float { .. } => 4,
193            NumberSchema::Double { .. } => 8,
194        }
195    }
196}
197
198impl TryFrom<RawNumber> for NumberSchema {
199    type Error = ValidationError;
200
201    fn try_from(raw: RawNumber) -> Result<Self, Self::Error> {
202        if raw.scale.is_some()
203            || raw.offset.is_some()
204            || raw.int.bit_offset.is_some()
205            || raw.int.bits.is_some()
206        {
207            let integer = IntegerSchema::try_from(raw.int)?;
208            Ok(NumberSchema::Integer {
209                integer,
210                scale: raw.scale.unwrap_or(DEFAULT_SCALE),
211                offset: raw.offset.unwrap_or(DEFAULT_OFFSET),
212            })
213        } else {
214            match raw.int.length {
215                4 => Ok(NumberSchema::Float {
216                    byteorder: raw.int.byteorder,
217                }),
218                8 => Ok(NumberSchema::Double {
219                    byteorder: raw.int.byteorder,
220                }),
221                _ => Err(ValidationError::InvalidFloatingLength {
222                    requested: raw.int.length,
223                }),
224            }
225        }
226    }
227}
228
229impl<'de> Deserialize<'de> for NumberSchema {
230    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
231    where
232        D: Deserializer<'de>,
233    {
234        let raw = RawNumber::deserialize(deserializer)?;
235        NumberSchema::try_from(raw).map_err(D::Error::custom)
236    }
237}
238
239impl Encoder for NumberSchema {
240    type Error = EncodingError;
241
242    fn encode<W>(&self, target: &mut W, value: &Value) -> Result<usize, Self::Error>
243    where
244        W: io::Write + WriteBytesExt,
245    {
246        let value = value.as_f64().ok_or_else(|| EncodingError::InvalidValue {
247            value: value.to_string(),
248        })?;
249        let length = match self {
250            NumberSchema::Integer { integer, .. } => {
251                let value = self.to_binary_value(value).into();
252                integer.encode(target, &value)?
253            }
254            NumberSchema::Float { byteorder } => {
255                let value = value as f32;
256                match byteorder {
257                    ByteOrder::LittleEndian => target.write_f32::<LE>(value)?,
258                    ByteOrder::BigEndian => target.write_f32::<BE>(value)?,
259                }
260                4
261            }
262            NumberSchema::Double { byteorder } => {
263                let value = value;
264                match byteorder {
265                    ByteOrder::LittleEndian => target.write_f64::<LE>(value)?,
266                    ByteOrder::BigEndian => target.write_f64::<BE>(value)?,
267                }
268                8
269            }
270        };
271
272        Ok(length)
273    }
274}
275
276impl Decoder for NumberSchema {
277    type Error = DecodingError;
278
279    fn decode<R>(&self, target: &mut R) -> Result<Value, Self::Error>
280    where
281        R: io::Read + ReadBytesExt,
282    {
283        let value = match self {
284            NumberSchema::Integer { integer, .. } => {
285                let int = integer
286                    .decode(target)?
287                    .as_f64()
288                    .expect("always works on integer schemata");
289                self.from_binary_value(int).into()
290            }
291            NumberSchema::Float { byteorder } => match byteorder {
292                ByteOrder::LittleEndian => target.read_f32::<LE>()?.into(),
293                ByteOrder::BigEndian => target.read_f32::<BE>()?.into(),
294            },
295            NumberSchema::Double { byteorder } => match byteorder {
296                ByteOrder::LittleEndian => target.read_f64::<LE>()?.into(),
297                ByteOrder::BigEndian => target.read_f64::<BE>()?.into(),
298            },
299        };
300
301        Ok(value)
302    }
303}
304
305#[cfg(test)]
306mod test {
307    use super::*;
308    use anyhow::Result;
309    use serde_json::{from_value, json};
310
311    /// Enough for this tests.
312    fn eq_fload(f1: f64, f2: f64) -> bool {
313        (f1 - f2).abs() < 0.000_001
314    }
315
316    #[test]
317    fn encode() -> Result<()> {
318        let schema = json!({
319            "scale": 0.01,
320            "offset": 10,
321            "byteorder": "littleendian",
322            "length": 2
323        });
324        let number2int: NumberSchema = from_value(schema)?;
325        let schema = json!({});
326        let number2float: NumberSchema = from_value(schema)?;
327        let schema = json!({
328            "length": 8,
329            "byteorder": "littleendian"
330        });
331        let number2double: NumberSchema = from_value(schema)?;
332        let schema = json!({"length": 3});
333        assert!(from_value::<NumberSchema>(schema).is_err());
334
335        let value = 22.5;
336        let json: Value = value.into();
337        let value_as_bin = 1250_f64;
338        assert!(eq_fload(
339            value_as_bin,
340            number2int.to_binary_value(value) as f64
341        ));
342        let value_int_le = (value_as_bin as i16).to_le_bytes();
343        let value_float_be = (value as f32).to_be_bytes();
344        let value_double_le = value.to_le_bytes();
345        let expected: Vec<u8> = value_int_le
346            .iter()
347            .chain(value_float_be.iter())
348            .chain(value_double_le.iter())
349            .copied()
350            .collect();
351
352        let mut buffer: Vec<u8> = vec![];
353        assert_eq!(2, number2int.encode(&mut buffer, &json)?);
354        assert_eq!(2, buffer.len());
355        assert_eq!(4, number2float.encode(&mut buffer, &json)?);
356        assert_eq!(2 + 4, buffer.len());
357        assert_eq!(8, number2double.encode(&mut buffer, &json)?);
358        assert_eq!(2 + 4 + 8, buffer.len());
359
360        assert_eq!(buffer, expected);
361
362        Ok(())
363    }
364
365    /// This example is the battery voltage from Ruuvi's RAWv2 protocol.
366    #[test]
367    fn bitfield() -> Result<()> {
368        let schema = json!({
369            "offset": 1.6,
370            "scale": 0.001,
371            "length": 2,
372            "bits": 11,
373            "bitoffset": 5
374        });
375        let voltage = from_value::<NumberSchema>(schema)?;
376        let value1 = 1.6;
377        let json1: Value = value1.into();
378        let value2 = 3.0;
379        let json2: Value = value2.into();
380        let mut buffer = [0; 2];
381
382        assert_eq!(2, voltage.encode(&mut buffer.as_mut(), &json1)?);
383        let res = u16::from_be_bytes(buffer);
384        assert_eq!(0, res);
385
386        assert_eq!(2, voltage.encode(&mut buffer.as_mut(), &json2)?);
387        let res = u16::from_be_bytes(buffer);
388        let diff = 1400 - ((res >> 5) as i16);
389        assert!(diff < 3 && diff > -3);
390
391        Ok(())
392    }
393
394    #[test]
395    fn bitfield2() -> Result<()> {
396        let schema = json!({
397            "type": "number",
398            "offset": -40,
399            "scale": 2,
400            "length": 2,
401            "bits": 5,
402            "bitoffset": 0,
403            "position": 50,
404            "unit": "dBm",
405            "description": "Transmission power in 1m distance."
406        });
407        let schema = from_value::<NumberSchema>(schema)?;
408
409        println!("schema:\n{:#?}", schema);
410
411        Ok(())
412    }
413
414    #[test]
415    fn bitfield3() -> Result<()> {
416        let schema = json!({
417            "type": "number",
418            "offset": 1.6,
419            "scale": 0.001,
420            "length": 2,
421            "bits": 11,
422            "bitoffset": 5,
423            "position": 50,
424            "unit": "volts",
425            "description": "Voltage of the battery powering the RuuviTag."
426        });
427        let schema = from_value::<NumberSchema>(schema)?;
428
429        println!("schema:\n{:#?}", schema);
430
431        Ok(())
432    }
433}