brightdate 0.5.8

Universal Decimal Time System anchored at J2000.0 — a scientifically grounded, timezone-free time representation
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
//! BrightDate — Universal Decimal Time System
//!
//! A scientifically grounded, timezone-free time representation anchored at
//! J2000.0. One `f64` value. Trivially sortable, diffable, and storable.
//!
//! # Format
//!
//! ```text
//! DDDDD.ddddd
//! ↑           ↑
//! │           Fractional day (decimal time-of-day)
//! Integer days since J2000.0 epoch
//! ```
//!
//! - **Epoch:** J2000.0 = `2000-01-01T12:00:00.000 TT`
//!   = `2000-01-01T11:58:55.816 UTC` (Unix ms `946_727_935_816`)
//!   = `JD 2_451_545.0` = `MJD 51_544.5`.
//! - **Unit:** SI days (86400 SI seconds).
//! - **Timescale:** TAI-coherent. The BrightDate clock ticks uniformly and
//!   has no leap-second discontinuities; leap seconds intervene only when
//!   converting to/from UTC labels (Unix ms, ISO strings).
//!
//! # Quick Start
//!
//! ```rust
//! use brightdate::BrightDate;
//!
//! let bd = BrightDate::from_unix_ms(1_700_000_000_000.0).unwrap();
//! println!("{}", bd);          // e.g. "8704.09722"
//! println!("{}", bd.to_log_string()); // "[8704.09722]"
//!
//! let tomorrow = bd.add_days(1.0);
//! let elapsed = tomorrow.difference(&bd); // 1.0
//! ```

pub mod arithmetic;
pub mod astronomy;
pub mod calendar;
pub mod civil_time;
pub mod comparisons;
pub mod constants;
pub mod conversions;
pub mod display_label;
pub mod exact;
pub mod exact_atto;
pub mod lens;
pub mod formatting;
pub mod geodesy;
pub mod instant;
pub mod interplanetary;
pub mod intervals;
pub mod leap_seconds;
pub mod relativity;
pub mod scheduling;
pub mod serialization;
pub mod spacetime;
pub mod timezones;
pub mod types;
pub mod validation;

pub use display_label::{
    compare_bd_labels, format_bd, format_bd_label, parse_bd, parse_bd_label,
    BrightLabel, DEFAULT_BD_PRECISION,
};
pub use exact::ExactBrightDate;
pub use exact_atto::ExactBrightAtto;
pub use lens::{
    brightdate_to_attoseconds, brightdate_to_picoseconds, ticks_to_brightdate,
    ATTOSECONDS_PER_DAY, ATTOSECONDS_PER_PICOSECOND, ATTOSECONDS_PER_SECOND,
    PICOSECONDS_PER_DAY,
};
pub use instant::BrightInstant;
pub use types::{BrightDateComponents, BrightDateOptions, BrightDuration, Precision};

use crate::constants::DEFAULT_PRECISION;
use crate::conversions::{
    from_gps_time, from_iso, from_julian_date, from_modified_julian_date, from_unix_ms,
    from_unix_seconds, to_date_time, to_gps_time, to_iso, to_julian_date,
    to_modified_julian_date, to_unix_ms, to_unix_seconds, tai_utc_offset_seconds_at,
};
use crate::arithmetic::{
    add, add_microdays, add_millidays, ceil_to_day, compare, difference,
    absolute_difference, equals, floor_to_day, is_in_range, lerp, midpoint,
    round_to_microday, round_to_milliday, subtract,
};
use crate::formatting::{
    decompose, format_bright_date, format_duration, format_full, format_log,
    format_prefixed, format_range, to_duration,
};
use chrono::{DateTime, Utc};

/// Immutable BrightDate value — decimal days since J2000.0 epoch.
///
/// This is the primary type for nearly all time operations. Wrap a raw `f64`
/// value to get the full BrightDate API including formatting, arithmetic, and
/// conversion methods.
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, serde::Serialize, serde::Deserialize)]
pub struct BrightDate {
    /// Raw decimal-day value since J2000.0
    pub value: f64,
    /// Display precision (decimal places)
    pub precision: Precision,
    /// Whether this value is on the TAI timescale
    pub is_tai: bool,
}

impl BrightDate {
    // ── Factory ───────────────────────────────────────────────────────────

    /// Lossy v1 view of canonical [`ExactBrightAtto`].
    pub fn from_exact_bright_atto(exact: ExactBrightAtto) -> Self {
        Self::from_value(exact.to_brightdate())
    }

    /// Lossy v1 view of [`ExactBrightDate`].
    pub fn from_exact_brightdate(exact: ExactBrightDate) -> Self {
        Self::from_value(exact.to_brightdate())
    }

    /// Create from a raw `f64` value (decimal days since J2000.0).
    pub fn from_value(value: f64) -> Self {
        Self {
            value,
            precision: DEFAULT_PRECISION,
            is_tai: false,
        }
    }

    /// Create from a raw value with full options.
    pub fn from_value_with_options(value: f64, options: BrightDateOptions) -> Self {
        Self {
            value,
            precision: options.precision.unwrap_or(DEFAULT_PRECISION),
            is_tai: options.use_tai.unwrap_or(false),
        }
    }

    /// Current time as a BrightDate (UTC).
    pub fn now() -> Self {
        let now_ms = Utc::now().timestamp_millis();
        Self::from_value(from_unix_ms(now_ms as f64).unwrap_or(0.0))
    }

    /// Create from a `chrono::DateTime<Utc>`.
    pub fn from_date_time(dt: DateTime<Utc>) -> Self {
        Self::from_value(from_unix_ms(dt.timestamp_millis() as f64).unwrap_or(0.0))
    }

    /// Create from a Unix timestamp in milliseconds.
    pub fn from_unix_ms(ms: f64) -> Result<Self, crate::types::BrightDateError> {
        Ok(Self::from_value(from_unix_ms(ms)?))
    }

    /// Create from a Unix timestamp in seconds.
    pub fn from_unix_seconds(s: f64) -> Result<Self, crate::types::BrightDateError> {
        Ok(Self::from_value(from_unix_seconds(s)?))
    }

    /// Create from a Julian Date.
    pub fn from_julian_date(jd: f64) -> Self {
        Self::from_value(from_julian_date(jd))
    }

    /// Create from a Modified Julian Date.
    pub fn from_modified_julian_date(mjd: f64) -> Self {
        Self::from_value(from_modified_julian_date(mjd))
    }

    /// Create from an ISO 8601 string.
    pub fn from_iso(s: &str) -> Result<Self, crate::types::BrightDateError> {
        Ok(Self::from_value(from_iso(s)?))
    }

    /// Create from GPS week number and seconds within that week.
    pub fn from_gps_time(gps_week: u32, gps_seconds: f64) -> Self {
        Self::from_value(from_gps_time(gps_week, gps_seconds))
    }

    /// The J2000.0 epoch itself (value = 0.0).
    pub fn epoch() -> Self {
        Self::from_value(0.0)
    }

    // ── Conversions ───────────────────────────────────────────────────────

    /// Convert to a `chrono::DateTime<Utc>`.
    pub fn to_date_time(&self) -> DateTime<Utc> {
        to_date_time(self.value)
    }

    /// Convert to Unix milliseconds.
    pub fn to_unix_ms(&self) -> f64 {
        to_unix_ms(self.value)
    }

    /// Convert to Unix seconds.
    pub fn to_unix_seconds(&self) -> f64 {
        to_unix_seconds(self.value)
    }

    /// Convert to Julian Date.
    pub fn to_julian_date(&self) -> f64 {
        to_julian_date(self.value)
    }

    /// Convert to Modified Julian Date.
    pub fn to_modified_julian_date(&self) -> f64 {
        to_modified_julian_date(self.value)
    }

    /// Convert to ISO 8601 string.
    pub fn to_iso(&self) -> String {
        to_iso(self.value)
    }

    /// Convert to GPS time `(gps_week, gps_seconds)`.
    pub fn to_gps_time(&self) -> (u32, f64) {
        to_gps_time(self.value)
    }

    /// Mark this BrightDate as TAI-flagged. In v1.0 the underlying value is
    /// always TAI-coherent, so this only toggles the display flag.
    pub fn to_tai(&self) -> Self {
        Self { value: self.value, precision: self.precision, is_tai: true }
    }

    /// Mark this BrightDate as UTC-flagged. In v1.0 the underlying value is
    /// always TAI-coherent, so this only toggles the display flag.
    pub fn to_utc(&self) -> Self {
        Self { value: self.value, precision: self.precision, is_tai: false }
    }

    /// Current TAI − UTC offset in whole seconds at this instant.
    pub fn tai_utc_offset_seconds(&self) -> i32 {
        tai_utc_offset_seconds_at(self.value)
    }

    // ── Arithmetic ────────────────────────────────────────────────────────

    /// Add `days` (decimal days) to this BrightDate.
    pub fn add_days(&self, days: f64) -> Self {
        Self { value: add(self.value, days), ..*self }
    }

    /// Subtract `days` (decimal days) from this BrightDate.
    pub fn sub_days(&self, days: f64) -> Self {
        Self { value: subtract(self.value, days), ..*self }
    }

    /// Add millidays.
    pub fn add_millidays(&self, md: f64) -> Self {
        Self { value: add_millidays(self.value, md), ..*self }
    }

    /// Add microdays.
    pub fn add_microdays(&self, ud: f64) -> Self {
        Self { value: add_microdays(self.value, ud), ..*self }
    }

    /// Signed difference `self − other` in decimal days.
    pub fn difference(&self, other: &Self) -> f64 {
        difference(self.value, other.value)
    }

    /// Absolute difference from `other` in decimal days.
    pub fn absolute_difference(&self, other: &Self) -> f64 {
        absolute_difference(self.value, other.value)
    }

    /// Compare ordering to `other`.
    pub fn compare(&self, other: &Self) -> std::cmp::Ordering {
        compare(self.value, other.value)
    }

    /// Test equality within `tolerance` decimal days (default: 1 microday).
    pub fn approx_eq(&self, other: &Self, tolerance: Option<f64>) -> bool {
        equals(self.value, other.value, tolerance)
    }

    /// True if `self < other`.
    pub fn is_before(&self, other: &Self) -> bool {
        self.value < other.value
    }

    /// True if `self > other`.
    pub fn is_after(&self, other: &Self) -> bool {
        self.value > other.value
    }

    /// True if `self` falls in `[start, end]`.
    pub fn is_in_range(&self, start: &Self, end: &Self) -> bool {
        is_in_range(self.value, start.value, end.value)
    }

    /// Linear interpolation between `self` and `other` at parameter `t ∈ [0,1]`.
    pub fn lerp(&self, other: &Self, t: f64) -> Self {
        Self { value: lerp(self.value, other.value, t), ..*self }
    }

    /// Midpoint between `self` and `other`.
    pub fn midpoint(&self, other: &Self) -> Self {
        Self { value: midpoint(self.value, other.value), ..*self }
    }

    /// Floor to the nearest whole day boundary.
    pub fn floor_to_day(&self) -> Self {
        Self { value: floor_to_day(self.value), ..*self }
    }

    /// Ceiling to the nearest whole day boundary.
    pub fn ceil_to_day(&self) -> Self {
        Self { value: ceil_to_day(self.value), ..*self }
    }

    /// Round to nearest milliday.
    pub fn round_to_milliday(&self) -> Self {
        Self { value: round_to_milliday(self.value), ..*self }
    }

    /// Round to nearest microday.
    pub fn round_to_microday(&self) -> Self {
        Self { value: round_to_microday(self.value), ..*self }
    }

    /// Lossy: canonical attosecond engine (v2).
    pub fn to_exact_bright_atto(&self) -> Result<ExactBrightAtto, crate::types::BrightDateError> {
        ExactBrightAtto::from_brightdate(self.value)
    }

    /// Lossy: picosecond engine.
    pub fn to_exact_brightdate(&self) -> Result<ExactBrightDate, crate::types::BrightDateError> {
        ExactBrightDate::from_brightdate(self.value)
    }

    // ── Formatting ────────────────────────────────────────────────────────

    /// Format as decimal-day string with this instance's precision, e.g. `"9622.50417"`.
    pub fn format(&self) -> String {
        format_bright_date(self.value, self.precision)
    }

    /// Full decomposed struct.
    pub fn decompose(&self) -> BrightDateComponents {
        decompose(self.value)
    }

    /// Full formatted breakdown.
    pub fn format_full(&self) -> crate::types::FormattedBrightDate {
        format_full(self.value, self.precision)
    }

    /// Compact log string, e.g. `"[9622.50417]"`.
    pub fn to_log_string(&self) -> String {
        format_log(self.value, self.precision)
    }

    /// Prefixed string, e.g. `"BD:9622.50417"`.
    pub fn to_prefixed_string(&self, prefix: Option<&str>) -> String {
        format_prefixed(self.value, self.precision, prefix)
    }

    /// Format duration from `self` to `other`.
    pub fn format_duration_to(&self, other: &Self) -> String {
        let days = other.value - self.value;
        format_duration(days)
    }

    /// Duration breakdown.
    pub fn duration_to(&self, other: &Self) -> BrightDuration {
        to_duration(other.value - self.value)
    }

    /// Range string for `self..=other`.
    pub fn format_range_to(&self, other: &Self) -> String {
        format_range(self.value, other.value, self.precision)
    }
}

impl std::fmt::Display for BrightDate {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.format())
    }
}

impl std::ops::Add<f64> for BrightDate {
    type Output = Self;
    fn add(self, rhs: f64) -> Self {
        self.add_days(rhs)
    }
}

impl std::ops::Sub<f64> for BrightDate {
    type Output = Self;
    fn sub(self, rhs: f64) -> Self {
        self.sub_days(rhs)
    }
}

impl std::ops::Sub<BrightDate> for BrightDate {
    type Output = f64;
    fn sub(self, rhs: BrightDate) -> f64 {
        self.difference(&rhs)
    }
}