Skip to main content

ggplot_rs/data/
mod.rs

1mod dataframe;
2mod source;
3
4pub use dataframe::DataFrame;
5pub use source::GGData;
6
7/// Dynamic value type for DataFrame columns.
8#[derive(Clone, Debug)]
9pub enum Value {
10    Float(f64),
11    Integer(i64),
12    Str(String),
13    Bool(bool),
14    /// Seconds since Unix epoch (1970-01-01 00:00:00 UTC).
15    DateTime(i64),
16    Na,
17}
18
19impl Value {
20    /// Try to extract as f64, coercing integers and datetimes.
21    pub fn as_f64(&self) -> Option<f64> {
22        match self {
23            Value::Float(f) => Some(*f),
24            Value::Integer(i) => Some(*i as f64),
25            Value::DateTime(secs) => Some(*secs as f64),
26            _ => None,
27        }
28    }
29
30    /// Try to extract as string representation.
31    pub fn as_str(&self) -> Option<&str> {
32        match self {
33            Value::Str(s) => Some(s),
34            _ => None,
35        }
36    }
37
38    /// Check if this is NA/missing.
39    pub fn is_na(&self) -> bool {
40        matches!(self, Value::Na)
41    }
42
43    /// Check if this is a DateTime value.
44    pub fn is_datetime(&self) -> bool {
45        matches!(self, Value::DateTime(_))
46    }
47
48    /// Create a DateTime from seconds since Unix epoch.
49    pub fn from_timestamp(secs: i64) -> Self {
50        Value::DateTime(secs)
51    }
52
53    /// Convert to a string for display/grouping purposes.
54    pub fn to_group_key(&self) -> String {
55        match self {
56            Value::Float(f) => format!("{f}"),
57            Value::Integer(i) => format!("{i}"),
58            Value::Str(s) => s.clone(),
59            Value::Bool(b) => format!("{b}"),
60            Value::DateTime(secs) => format_epoch_secs(*secs),
61            Value::Na => "NA".to_string(),
62        }
63    }
64}
65
66/// Format epoch seconds as a human-readable date/time string.
67pub fn format_epoch_secs(secs: i64) -> String {
68    // Simple UTC date/time formatting without external dependencies
69    const SECS_PER_DAY: i64 = 86400;
70    const SECS_PER_HOUR: i64 = 3600;
71    const SECS_PER_MINUTE: i64 = 60;
72
73    let (mut days, rem) = if secs >= 0 {
74        (secs / SECS_PER_DAY, secs % SECS_PER_DAY)
75    } else {
76        let d = (secs - SECS_PER_DAY + 1) / SECS_PER_DAY;
77        (d, secs - d * SECS_PER_DAY)
78    };
79
80    let hour = rem / SECS_PER_HOUR;
81    let minute = (rem % SECS_PER_HOUR) / SECS_PER_MINUTE;
82    let second = rem % SECS_PER_MINUTE;
83
84    // Days since 1970-01-01 to Y-M-D (civil calendar)
85    days += 719_468; // shift epoch from 1970-01-01 to 0000-03-01
86    let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
87    let doe = (days - era * 146_097) as u32;
88    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
89    let y = yoe as i64 + era * 400;
90    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
91    let mp = (5 * doy + 2) / 153;
92    let d = doy - (153 * mp + 2) / 5 + 1;
93    let m = if mp < 10 { mp + 3 } else { mp - 9 };
94    let y = if m <= 2 { y + 1 } else { y };
95
96    if hour == 0 && minute == 0 && second == 0 {
97        format!("{y:04}-{m:02}-{d:02}")
98    } else {
99        format!("{y:04}-{m:02}-{d:02} {hour:02}:{minute:02}:{second:02}")
100    }
101}
102
103impl PartialEq for Value {
104    fn eq(&self, other: &Self) -> bool {
105        match (self, other) {
106            (Value::Float(a), Value::Float(b)) => a.to_bits() == b.to_bits(),
107            (Value::Integer(a), Value::Integer(b)) => a == b,
108            (Value::Str(a), Value::Str(b)) => a == b,
109            (Value::Bool(a), Value::Bool(b)) => a == b,
110            (Value::DateTime(a), Value::DateTime(b)) => a == b,
111            (Value::Na, Value::Na) => true,
112            _ => false,
113        }
114    }
115}