const FIELD_SEP: u8 = 0xB;
const ROW_SEP: u8 = 0xC;
const END: u8 = 0xD;
const NEGATIVE: u8 = 0xE;
const SPACING: usize = 5;
const MAX_DIGITS: usize = 10;
const DATE_MARKER: u8 = 0xCE;
pub fn decode_fit_buffer_bulk(buf: &[u8], fields_per_row: usize) -> Vec<Vec<i32>> {
let mut reader = FitReader::new(buf);
let mut rows = Vec::new();
let mut prev = vec![0i32; fields_per_row];
let mut alloc = vec![0i32; fields_per_row];
let mut first = true;
while !reader.is_exhausted() {
alloc.iter_mut().for_each(|v| *v = 0);
let n = reader.read_changes(&mut alloc);
if n == 0 {
continue;
}
if first {
prev.copy_from_slice(&alloc);
first = false;
} else {
apply_deltas(&mut alloc, &prev, n);
prev.copy_from_slice(&alloc);
}
rows.push(alloc.clone());
}
rows
}
pub struct FitReader<'a> {
buf: &'a [u8],
pos: usize,
pub is_date: bool,
}
impl<'a> FitReader<'a> {
#[inline]
pub fn new(buf: &'a [u8]) -> Self {
Self {
buf,
pos: 0,
is_date: false,
}
}
#[inline]
pub fn with_offset(buf: &'a [u8], offset: usize) -> Self {
Self {
buf,
pos: offset,
is_date: false,
}
}
#[inline]
pub fn position(&self) -> usize {
self.pos
}
#[inline]
pub fn is_exhausted(&self) -> bool {
self.pos >= self.buf.len()
}
#[inline]
pub fn read_changes(&mut self, alloc: &mut [i32]) -> usize {
self.is_date = false;
if self.pos < self.buf.len() && self.buf[self.pos] == DATE_MARKER {
self.is_date = true;
self.pos += 1;
self.skip_to_end();
return 0;
}
let mut idx: usize = 0;
let mut digits = [0u8; MAX_DIGITS];
let mut count: usize = 0;
let mut negative = false;
while self.pos < self.buf.len() {
let byte = self.buf[self.pos];
self.pos += 1;
let high = byte >> 4;
let low = byte & 0x0F;
if self.process_nibble(
high,
alloc,
&mut idx,
&mut digits,
&mut count,
&mut negative,
) {
return idx;
}
if self.process_nibble(low, alloc, &mut idx, &mut digits, &mut count, &mut negative) {
return idx;
}
}
if count > 0 || negative {
let val = flush_digits(&digits, count, negative);
if idx < alloc.len() {
alloc[idx] = val;
}
idx += 1;
}
idx
}
#[inline]
fn process_nibble(
&self,
nibble: u8,
alloc: &mut [i32],
idx: &mut usize,
digits: &mut [u8; MAX_DIGITS],
count: &mut usize,
negative: &mut bool,
) -> bool {
match nibble {
0..=9 => {
if *count < MAX_DIGITS {
digits[*count] = nibble;
*count += 1;
}
false
}
FIELD_SEP => {
let val = flush_digits(digits, *count, *negative);
if *idx < alloc.len() {
alloc[*idx] = val;
}
*idx += 1;
*count = 0;
*negative = false;
false
}
ROW_SEP => {
let val = flush_digits(digits, *count, *negative);
if *idx < alloc.len() {
alloc[*idx] = val;
}
*idx += 1;
*count = 0;
*negative = false;
while *idx < SPACING {
if *idx < alloc.len() {
alloc[*idx] = 0;
}
*idx += 1;
}
*idx = SPACING;
false
}
END => {
let val = flush_digits(digits, *count, *negative);
if *idx < alloc.len() {
alloc[*idx] = val;
}
*idx += 1;
true
}
NEGATIVE => {
*negative = true;
false
}
_ => {
false
}
}
}
fn skip_to_end(&mut self) {
while self.pos < self.buf.len() {
let byte = self.buf[self.pos];
self.pos += 1;
if (byte >> 4) == END || (byte & 0x0F) == END {
return;
}
}
}
}
#[inline]
fn flush_digits(digits: &[u8; MAX_DIGITS], count: usize, negative: bool) -> i32 {
let mut val: i64 = 0;
for &digit in digits.iter().take(count) {
val = val * 10 + digit as i64;
}
if negative {
val = -val;
}
i32::try_from(val).unwrap_or(if val > 0 { i32::MAX } else { i32::MIN })
}
#[inline]
pub fn apply_deltas(tick: &mut [i32], prev: &[i32], n_fields: usize) {
let len = tick.len().min(prev.len());
let n = n_fields.min(len);
for i in 0..n {
tick[i] = tick[i].wrapping_add(prev[i]);
}
if n < len {
tick[n..len].copy_from_slice(&prev[n..len]);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn pack(high: u8, low: u8) -> u8 {
(high << 4) | (low & 0x0F)
}
#[test]
fn flush_digits_basic() {
let mut d = [0u8; MAX_DIGITS];
assert_eq!(flush_digits(&d, 0, false), 0);
d[0] = 7;
assert_eq!(flush_digits(&d, 1, false), 7);
assert_eq!(flush_digits(&d, 1, true), -7);
d[0] = 1;
d[1] = 2;
d[2] = 3;
assert_eq!(flush_digits(&d, 3, false), 123);
assert_eq!(flush_digits(&d, 3, true), -123);
}
#[test]
fn single_field_end() {
let data = [pack(4, 2), pack(END, 0)];
let mut alloc = [0i32; 8];
let mut reader = FitReader::new(&data);
let n = reader.read_changes(&mut alloc);
assert_eq!(n, 1);
assert_eq!(alloc[0], 42);
}
#[test]
fn two_fields_comma_then_end() {
let data = [pack(1, 2), pack(FIELD_SEP, 3), pack(4, END)];
let mut alloc = [0i32; 8];
let mut reader = FitReader::new(&data);
let n = reader.read_changes(&mut alloc);
assert_eq!(n, 2);
assert_eq!(alloc[0], 12);
assert_eq!(alloc[1], 34);
}
#[test]
fn negative_value() {
let data = [pack(NEGATIVE, 5), pack(END, 0)];
let mut alloc = [0i32; 8];
let mut reader = FitReader::new(&data);
let n = reader.read_changes(&mut alloc);
assert_eq!(n, 1);
assert_eq!(alloc[0], -5);
}
#[test]
fn negative_multi_digit() {
let data = [
pack(NEGATIVE, 1),
pack(2, 3),
pack(FIELD_SEP, 4),
pack(5, END),
];
let mut alloc = [0i32; 8];
let mut reader = FitReader::new(&data);
let n = reader.read_changes(&mut alloc);
assert_eq!(n, 2);
assert_eq!(alloc[0], -123);
assert_eq!(alloc[1], 45);
}
#[test]
fn row_separator_zero_fills() {
let data = [pack(7, ROW_SEP), pack(9, 9), pack(END, 0)];
let mut alloc = [0i32; 16];
let mut reader = FitReader::new(&data);
let n = reader.read_changes(&mut alloc);
assert_eq!(n, 6);
assert_eq!(alloc[0], 7);
assert_eq!(alloc[1], 0);
assert_eq!(alloc[2], 0);
assert_eq!(alloc[3], 0);
assert_eq!(alloc[4], 0);
assert_eq!(alloc[5], 99);
}
#[test]
fn empty_fields_flush_as_zero() {
let data = [pack(FIELD_SEP, FIELD_SEP), pack(END, 0)];
let mut alloc = [99i32; 8]; let mut reader = FitReader::new(&data);
let n = reader.read_changes(&mut alloc);
assert_eq!(n, 3);
assert_eq!(alloc[0], 0);
assert_eq!(alloc[1], 0);
assert_eq!(alloc[2], 0);
}
#[test]
fn date_marker_returns_zero() {
let data = [DATE_MARKER, pack(1, 2), pack(END, 0)];
let mut alloc = [0i32; 8];
let mut reader = FitReader::new(&data);
let n = reader.read_changes(&mut alloc);
assert_eq!(n, 0);
assert!(reader.is_date);
}
#[test]
fn multi_row_sequential() {
let data = [
pack(5, FIELD_SEP),
pack(3, END),
pack(1, FIELD_SEP),
pack(2, END),
];
let mut alloc = [0i32; 8];
let mut reader = FitReader::new(&data);
let n1 = reader.read_changes(&mut alloc);
assert_eq!(n1, 2);
assert_eq!(alloc[0], 5);
assert_eq!(alloc[1], 3);
let mut alloc2 = [0i32; 8];
let n2 = reader.read_changes(&mut alloc2);
assert_eq!(n2, 2);
assert_eq!(alloc2[0], 1);
assert_eq!(alloc2[1], 2);
}
#[test]
fn delta_decompression() {
let prev = [100i32, 200, 50];
let mut tick = [5i32, -3, 10];
apply_deltas(&mut tick, &prev, 3);
assert_eq!(tick, [105, 197, 60]);
}
#[test]
fn delta_trailing_fields_carried_forward() {
let prev = [10i32, 20, 30, 40, 50];
let mut tick = [1i32, -2, 0, 0, 0];
apply_deltas(&mut tick, &prev, 2);
assert_eq!(tick, [11, 18, 30, 40, 50]);
}
#[test]
fn large_value() {
let data = [
pack(1, 9),
pack(9, 9),
pack(9, 9),
pack(9, 9),
pack(9, 9),
pack(END, 0),
];
let mut alloc = [0i32; 4];
let mut reader = FitReader::new(&data);
let n = reader.read_changes(&mut alloc);
assert_eq!(n, 1);
assert_eq!(alloc[0], 1_999_999_999);
}
#[test]
fn realistic_trade_tick_row() {
let mut data = Vec::new();
data.push(pack(3, 4));
data.push(pack(2, 0));
data.push(pack(0, 0));
data.push(pack(0, 0));
data.push(pack(FIELD_SEP, 1));
data.push(pack(ROW_SEP, 1));
data.push(pack(0, 0));
data.push(pack(FIELD_SEP, 4));
data.push(pack(FIELD_SEP, 1));
data.push(pack(5, 0));
data.push(pack(2, 5));
data.push(pack(FIELD_SEP, FIELD_SEP));
data.push(pack(FIELD_SEP, FIELD_SEP));
data.push(pack(FIELD_SEP, 1));
data.push(pack(FIELD_SEP, 2));
data.push(pack(0, 2));
data.push(pack(4, 0));
data.push(pack(3, 1));
data.push(pack(5, END));
let mut alloc = [0i32; 32];
let mut reader = FitReader::new(&data);
let n = reader.read_changes(&mut alloc);
assert_eq!(n, 14);
assert_eq!(alloc[0], 34200000); assert_eq!(alloc[1], 1); assert_eq!(alloc[2], 0); assert_eq!(alloc[3], 0); assert_eq!(alloc[4], 0); assert_eq!(alloc[5], 100); assert_eq!(alloc[6], 4); assert_eq!(alloc[7], 15025); assert_eq!(alloc[8], 0); assert_eq!(alloc[9], 0); assert_eq!(alloc[10], 0); assert_eq!(alloc[11], 0); assert_eq!(alloc[12], 1); assert_eq!(alloc[13], 20240315); }
#[test]
fn with_offset_starts_at_given_position() {
let data = [0xFF, 0xFF, pack(5, END)];
let mut alloc = [0i32; 4];
let mut reader = FitReader::with_offset(&data, 2);
let n = reader.read_changes(&mut alloc);
assert_eq!(n, 1);
assert_eq!(alloc[0], 5);
assert_eq!(reader.position(), 3);
}
#[test]
fn exhausted_after_single_row() {
let data = [pack(1, END)];
let mut alloc = [0i32; 4];
let mut reader = FitReader::new(&data);
assert!(!reader.is_exhausted());
reader.read_changes(&mut alloc);
assert!(reader.is_exhausted());
}
#[test]
fn zero_value_end() {
let data = [pack(END, 0)];
let mut alloc = [99i32; 4];
let mut reader = FitReader::new(&data);
let n = reader.read_changes(&mut alloc);
assert_eq!(n, 1);
assert_eq!(alloc[0], 0);
}
#[test]
fn negative_zero_flushes_as_zero() {
let data = [pack(NEGATIVE, END)];
let mut alloc = [99i32; 4];
let mut reader = FitReader::new(&data);
let n = reader.read_changes(&mut alloc);
assert_eq!(n, 1);
assert_eq!(alloc[0], 0);
}
#[test]
fn row_sep_with_empty_pre_slash_field() {
let data = [pack(ROW_SEP, 8), pack(END, 0)];
let mut alloc = [99i32; 16];
let mut reader = FitReader::new(&data);
let n = reader.read_changes(&mut alloc);
assert_eq!(n, 6);
assert_eq!(alloc[0], 0); assert_eq!(alloc[1], 0); assert_eq!(alloc[2], 0);
assert_eq!(alloc[3], 0);
assert_eq!(alloc[4], 0);
assert_eq!(alloc[5], 8);
}
#[test]
fn delta_full_round_trip() {
let row1 = [
pack(1, 0),
pack(0, 0),
pack(FIELD_SEP, 5),
pack(0, FIELD_SEP),
pack(2, 0),
pack(0, END),
];
let row2 = [
pack(5, FIELD_SEP),
pack(NEGATIVE, 3),
pack(FIELD_SEP, 1),
pack(0, END),
];
let mut data = Vec::new();
data.extend_from_slice(&row1);
data.extend_from_slice(&row2);
let mut reader = FitReader::new(&data);
let mut prev = [0i32; 8];
let n1 = reader.read_changes(&mut prev);
assert_eq!(n1, 3);
assert_eq!(prev[0], 1000);
assert_eq!(prev[1], 50);
assert_eq!(prev[2], 200);
let mut tick = [0i32; 8];
let n2 = reader.read_changes(&mut tick);
assert_eq!(n2, 3);
assert_eq!(tick[0], 5);
assert_eq!(tick[1], -3);
assert_eq!(tick[2], 10);
apply_deltas(&mut tick, &prev, n2);
assert_eq!(tick[0], 1005);
assert_eq!(tick[1], 47);
assert_eq!(tick[2], 210);
}
#[test]
fn alloc_too_small_does_not_panic() {
let data = [
pack(1, FIELD_SEP),
pack(2, FIELD_SEP),
pack(3, FIELD_SEP),
pack(4, FIELD_SEP),
pack(5, END),
];
let mut alloc = [0i32; 2]; let mut reader = FitReader::new(&data);
let n = reader.read_changes(&mut alloc);
assert_eq!(n, 5);
assert_eq!(alloc[0], 1);
assert_eq!(alloc[1], 2);
}
#[test]
fn empty_buffer() {
let data: [u8; 0] = [];
let mut alloc = [0i32; 4];
let mut reader = FitReader::new(&data);
assert!(reader.is_exhausted());
let n = reader.read_changes(&mut alloc);
assert_eq!(n, 0);
}
}