aimx/aim/
version.rs

1//! AIM version header support.
2//!
3//! Provides compact epoch/partial version handling for AIM headers and journals.
4//! Parsed versions are written via [`Version::print`].
5
6use crate::{aim::{WriterLike, Writer}, literals::{parse_date, parse_unsigned}};
7use nom::{
8    IResult, Parser,
9    character::complete::{char, multispace0},
10    combinator::opt,
11    sequence::delimited,
12};
13use jiff::{Timestamp, civil::DateTime};
14use std::fmt;
15
16pub fn now() -> DateTime {
17    match Timestamp::now().in_tz("UTC") {
18        Ok(zdt) => zdt.datetime(),
19        _ => unimplemented!(),
20    }
21}
22
23
24/// AIM version identifier used in headers and journals.
25///
26/// Encodes a major `epoch` and minor `partial` component.
27#[derive(Debug, PartialEq, Clone)]
28pub struct Version {
29    epoch: u32,
30    partial: u32,
31    date: DateTime,
32}
33
34impl Default for Version {
35    fn default() -> Self {
36        Version {
37            epoch: 0,
38            partial: 0,
39            date: now(),
40        }
41    }
42}
43
44impl Version {
45    /// Construct from explicit `epoch` and `partial` components.
46    pub fn new(epoch: u32, partial: u32, date: DateTime) -> Self {
47        Self { epoch, partial, date, }
48    }
49
50    /// Alias for [`Self::epoch`].
51    pub fn epoch(&self) -> u32 {
52        self.epoch
53    }
54
55    /// Set `epoch` and reset `partial` to `0`.
56    pub fn set_epoch(&mut self, epoch: u32) {
57        self.epoch = epoch;
58        self.partial = 0;
59    }
60
61    /// Increment `epoch` and reset `partial` to `0`.
62    pub fn increment_epoch(&mut self) {
63        self.epoch += 1;
64        self.partial = 0;
65        self.date = now();
66    }
67
68    /// Minor version component.
69    pub fn partial(&self) -> u32 {
70        self.partial
71    }
72
73    /// Set `partial` without changing `epoch`.
74    pub fn set_partial(&mut self, partial: u32) {
75        self.partial = partial;
76    }
77
78    /// Increment `partial` within current `epoch`.
79    pub fn increment_partial(&mut self) {
80        self.partial += 1;
81        self.date = now();
82    }
83
84    pub fn date(&self) -> DateTime {
85        self.date
86    }
87
88    pub fn set_date(&mut self, date: DateTime)  {
89        self.date = date;
90    }
91
92    pub fn print(&self, writer: &mut Writer) {
93        writer.write_char('[');
94        writer.write_unsigned(self.epoch);
95        if self.partial > 0 {
96            writer.write_char(':');
97            writer.write_unsigned(self.partial);
98        }
99        writer.write_str(" @");
100        writer.write_date(&self.date);
101        writer.write_char(']');
102        writer.write_eol();
103    }
104
105    /// Return the formula-string representation (round-trippable by the parser).
106    pub fn to_formula(&self) -> String {
107        let mut writer = Writer::formulizer();
108        self.print(&mut writer);
109        writer.finish()
110    }
111}
112
113impl WriterLike for Version {
114    fn write(&self, writer: &mut Writer) {
115        self.print(writer);
116    }
117}
118
119impl fmt::Display for Version {
120    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121        write!(f, "{}", self.to_stringized())
122    }
123}
124
125/// Parse version `[epoch{:partial}]` components.
126///
127/// Accepts optional whitespace and missing `partial` (defaults to `0`).
128pub fn parse_version(input: &str) -> IResult<&str, Version> {
129    delimited(
130        multispace0,
131        delimited(
132            char('['),
133                delimited(
134                    multispace0, 
135                    parse_epoc, 
136                    multispace0
137                ),
138            char(']')
139        ),
140        multispace0
141    )
142    .parse(input)
143}
144
145/// Internal parser for `epoch[:partial]`.
146///
147/// `partial` is optional and defaults to `0`.
148fn parse_epoc(input: &str) -> IResult<&str, Version> {
149    let (input, (epoch, partial_opt, date_opt)) = (
150        parse_unsigned,
151        opt((
152            delimited(multispace0, char(':'), multispace0),
153            parse_unsigned,
154        )),
155        opt((
156            delimited(multispace0, char('@'), multispace0),
157            parse_date,
158        )),
159    )
160    .parse(input)?;
161    let partial = partial_opt.map(|(_, p)| p).unwrap_or(0);
162    let date = date_opt.map(|(_, d)| d).unwrap_or(now());
163    
164    Ok((input, Version { epoch, partial, date }))
165}