Skip to main content

oxihuman_core/
timezone_offset.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Timezone offset calculator stub.
6
7#[derive(Debug, Clone, PartialEq)]
8pub struct TimezoneOffset {
9    /// Offset in minutes from UTC (positive = east).
10    pub minutes: i32,
11    /// Optional IANA-style name.
12    pub name: String,
13}
14
15impl TimezoneOffset {
16    pub fn new(minutes: i32, name: &str) -> Self {
17        TimezoneOffset {
18            minutes,
19            name: name.to_string(),
20        }
21    }
22
23    pub fn utc() -> Self {
24        TimezoneOffset::new(0, "UTC")
25    }
26
27    pub fn hours(&self) -> f32 {
28        self.minutes as f32 / 60.0
29    }
30
31    pub fn is_positive(&self) -> bool {
32        self.minutes >= 0
33    }
34
35    pub fn format_offset(&self) -> String {
36        format_offset(self.minutes)
37    }
38}
39
40pub fn format_offset(minutes: i32) -> String {
41    let sign = if minutes >= 0 { '+' } else { '-' };
42    let abs_min = minutes.unsigned_abs();
43    let h = abs_min / 60;
44    let m = abs_min % 60;
45    format!("{}{:02}:{:02}", sign, h, m)
46}
47
48pub fn parse_offset(s: &str) -> Option<TimezoneOffset> {
49    let s = s.trim();
50    if s == "UTC" || s == "Z" {
51        return Some(TimezoneOffset::utc());
52    }
53    let (sign, rest) = if let Some(r) = s.strip_prefix('+') {
54        (1i32, r)
55    } else if let Some(r) = s.strip_prefix('-') {
56        (-1i32, r)
57    } else {
58        return None;
59    };
60    let parts: Vec<&str> = rest.splitn(2, ':').collect();
61    let hours: i32 = parts.first()?.parse().ok()?;
62    let mins: i32 = if parts.len() > 1 {
63        parts[1].parse().ok()?
64    } else {
65        0
66    };
67    Some(TimezoneOffset::new(sign * (hours * 60 + mins), s))
68}
69
70pub fn offset_difference(a: &TimezoneOffset, b: &TimezoneOffset) -> i32 {
71    a.minutes - b.minutes
72}
73
74pub fn convert_utc_minutes(utc_minutes: i64, offset: &TimezoneOffset) -> i64 {
75    utc_minutes + offset.minutes as i64
76}
77
78pub fn known_offsets() -> Vec<TimezoneOffset> {
79    vec![
80        TimezoneOffset::new(0, "UTC"),
81        TimezoneOffset::new(540, "Asia/Tokyo"),
82        TimezoneOffset::new(-300, "America/New_York"),
83        TimezoneOffset::new(-480, "America/Los_Angeles"),
84        TimezoneOffset::new(60, "Europe/Paris"),
85    ]
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn test_utc_offset() {
94        let tz = TimezoneOffset::utc();
95        assert_eq!(tz.minutes, 0);
96        assert_eq!(tz.format_offset(), "+00:00");
97    }
98
99    #[test]
100    fn test_tokyo_offset() {
101        let tz = TimezoneOffset::new(540, "Asia/Tokyo");
102        assert_eq!(tz.hours(), 9.0);
103        assert_eq!(tz.format_offset(), "+09:00");
104    }
105
106    #[test]
107    fn test_negative_offset() {
108        let tz = TimezoneOffset::new(-300, "America/New_York");
109        assert_eq!(tz.format_offset(), "-05:00");
110        assert!(!tz.is_positive() /* negative offset */,);
111    }
112
113    #[test]
114    fn test_parse_offset_utc() {
115        let tz = parse_offset("UTC").expect("should succeed");
116        assert_eq!(tz.minutes, 0);
117    }
118
119    #[test]
120    fn test_parse_offset_positive() {
121        let tz = parse_offset("+09:00").expect("should succeed");
122        assert_eq!(tz.minutes, 540);
123    }
124
125    #[test]
126    fn test_parse_offset_negative() {
127        let tz = parse_offset("-05:00").expect("should succeed");
128        assert_eq!(tz.minutes, -300);
129    }
130
131    #[test]
132    fn test_offset_difference() {
133        let a = TimezoneOffset::new(540, "JST");
134        let b = TimezoneOffset::new(0, "UTC");
135        assert_eq!(offset_difference(&a, &b), 540);
136    }
137
138    #[test]
139    fn test_convert_utc() {
140        let tz = TimezoneOffset::new(540, "JST");
141        let local = convert_utc_minutes(0, &tz);
142        assert_eq!(local, 540);
143    }
144
145    #[test]
146    fn test_known_offsets_nonempty() {
147        let list = known_offsets();
148        assert!(!list.is_empty(), /* known offsets list should not be empty */);
149    }
150}