gie-client 0.1.1

Strictly-typed, unofficial Rust client for downloading GIE AGSI and ALSI data.
Documentation
use std::cmp::Ordering;
#[cfg(test)]
use std::collections::BTreeMap;

use super::types::GieDate;

pub(crate) fn group_time_series<T, K, FK, FD>(
    rows: Vec<T>,
    make_key: FK,
    gas_day_start: FD,
) -> Vec<(K, Vec<T>)>
where
    K: Ord,
    FK: Fn(&T) -> K,
    FD: Fn(&T) -> Option<GieDate>,
{
    group_time_series_presorted(rows, make_key, gas_day_start)
}

fn group_time_series_presorted<T, K, FK, FD>(
    rows: Vec<T>,
    make_key: FK,
    gas_day_start: FD,
) -> Vec<(K, Vec<T>)>
where
    K: Ord,
    FK: Fn(&T) -> K,
    FD: Fn(&T) -> Option<GieDate>,
{
    let mut keyed_rows: Vec<(K, Option<GieDate>, T)> = rows
        .into_iter()
        .map(|row| (make_key(&row), gas_day_start(&row), row))
        .collect();

    keyed_rows.sort_by(|(left_key, left_day, _), (right_key, right_day, _)| {
        left_key
            .cmp(right_key)
            .then_with(|| compare_optional_dates(*left_day, *right_day))
    });

    let mut grouped: Vec<(K, Vec<T>)> = Vec::new();
    for (key, _, row) in keyed_rows {
        if let Some((last_key, points)) = grouped.last_mut()
            && last_key == &key
        {
            points.push(row);
            continue;
        }
        grouped.push((key, vec![row]));
    }

    grouped
}

#[cfg(test)]
fn group_time_series_btree<T, K, FK, FD>(
    rows: Vec<T>,
    make_key: FK,
    gas_day_start: FD,
) -> Vec<(K, Vec<T>)>
where
    K: Ord,
    FK: Fn(&T) -> K,
    FD: Fn(&T) -> Option<GieDate>,
{
    let mut grouped: BTreeMap<K, Vec<T>> = BTreeMap::new();

    for row in rows {
        grouped.entry(make_key(&row)).or_default().push(row);
    }

    for points in grouped.values_mut() {
        points.sort_by(|left, right| {
            compare_optional_dates(gas_day_start(left), gas_day_start(right))
        });
    }

    grouped.into_iter().collect()
}

fn compare_optional_dates(left: Option<GieDate>, right: Option<GieDate>) -> Ordering {
    match (left, right) {
        (Some(left), Some(right)) => left.cmp(&right),
        (Some(_), None) => Ordering::Less,
        (None, Some(_)) => Ordering::Greater,
        (None, None) => Ordering::Equal,
    }
}

#[cfg(test)]
mod tests {
    use std::time::Instant;

    use super::*;
    use crate::common::types::parse_date;

    fn test_date(value: &str) -> GieDate {
        parse_date(value).unwrap()
    }

    #[test]
    fn time_series_are_grouped_and_sorted_by_date() {
        #[derive(Debug, Clone, PartialEq, Eq)]
        struct Probe {
            key: &'static str,
            gas_day_start: Option<GieDate>,
            value: u32,
        }

        let rows = vec![
            Probe {
                key: "A",
                gas_day_start: Some(test_date("2026-03-03")),
                value: 3,
            },
            Probe {
                key: "B",
                gas_day_start: Some(test_date("2026-03-02")),
                value: 2,
            },
            Probe {
                key: "A",
                gas_day_start: Some(test_date("2026-03-01")),
                value: 1,
            },
            Probe {
                key: "A",
                gas_day_start: None,
                value: 4,
            },
        ];

        let grouped = group_time_series(rows, |row| row.key, |row| row.gas_day_start);

        assert_eq!(grouped.len(), 2);
        assert_eq!(
            grouped[0].1.iter().map(|row| row.value).collect::<Vec<_>>(),
            vec![1, 3, 4]
        );
        assert_eq!(
            grouped[1].1.iter().map(|row| row.value).collect::<Vec<_>>(),
            vec![2]
        );
    }

    #[test]
    fn presorted_strategy_matches_btree_strategy() {
        #[derive(Debug, Clone, PartialEq, Eq)]
        struct Probe {
            key: &'static str,
            gas_day_start: Option<GieDate>,
            value: u32,
        }

        let rows = vec![
            Probe {
                key: "A",
                gas_day_start: Some(test_date("2026-03-03")),
                value: 3,
            },
            Probe {
                key: "B",
                gas_day_start: Some(test_date("2026-03-02")),
                value: 2,
            },
            Probe {
                key: "A",
                gas_day_start: Some(test_date("2026-03-01")),
                value: 1,
            },
            Probe {
                key: "A",
                gas_day_start: None,
                value: 4,
            },
            Probe {
                key: "B",
                gas_day_start: None,
                value: 5,
            },
        ];

        let presorted = group_time_series(rows.clone(), |row| row.key, |row| row.gas_day_start);
        let btree = group_time_series_btree(rows, |row| row.key, |row| row.gas_day_start);

        assert_eq!(presorted, btree);
    }

    #[test]
    #[ignore]
    fn benchmark_grouping_strategies() {
        #[derive(Debug, Clone, PartialEq, Eq)]
        struct Probe {
            key: u16,
            gas_day_start: Option<GieDate>,
            value: u32,
        }

        let mut rows = Vec::with_capacity(100_000);
        for index in 0_u32..100_000_u32 {
            let day = 1 + (index % 28);
            let date = parse_date(&format!("2026-03-{day:02}")).ok();

            rows.push(Probe {
                key: u16::try_from(index % 300).expect("key is in range"),
                gas_day_start: date,
                value: index,
            });
        }

        let started = Instant::now();
        let _ = group_time_series(rows.clone(), |row| row.key, |row| row.gas_day_start);
        let presorted_elapsed = started.elapsed();

        let started = Instant::now();
        let _ = group_time_series_btree(rows, |row| row.key, |row| row.gas_day_start);
        let btree_elapsed = started.elapsed();

        eprintln!(
            "group_time_series benchmark: presorted={presorted_elapsed:?}, btree={btree_elapsed:?}"
        );
    }
}