【Chrono库】 时区转换规则(TransitionRule)实现详解(src/offset/local/tz_info/rule.rs)

核心数据结构

1. TransitionRule 枚举

表示时区转换规则,有两种类型:

rust 复制代码
pub(super) enum TransitionRule {
    Fixed(LocalTimeType),      // 固定时区(无夏令时)
    Alternate(AlternateTime),  // 交替时区(有夏令时)
}

2. AlternateTime 结构体

表示包含夏令时的交替时区规则:

rust 复制代码
pub(super) struct AlternateTime {
    pub(super) std: LocalTimeType,     // 标准时间类型
    pub(super) dst: LocalTimeType,     // 夏令时时间类型
    dst_start: RuleDay,                // 夏令时开始日规则
    dst_start_time: i32,               // 夏令时开始时间(秒)
    dst_end: RuleDay,                  // 夏令时结束日规则
    dst_end_time: i32,                 // 夏令时结束时间(秒)
}

3. RuleDay 枚举

定义夏令时转换日期的三种表示方式:

rust 复制代码
enum RuleDay {
    // 儒略日 [1,365],不考虑闰年的2月29日
    Julian1WithoutLeap(u16),
    // 儒略日 [0,365],考虑闰年的2月29日
    Julian0WithLeap(u16),
    // 月-周-星期表示法
    MonthWeekday {
        month: u8,      // 月份 [1,12]
        week: u8,       // 第几周 [1,5],5表示最后一周
        week_day: u8,   // 星期几 [0,6],0=周日
    },
}

POSIX TZ字符串解析

1. TZ字符串格式

POSIX TZ字符串的几种格式:

rust 复制代码
// 格式1: 固定时区
"EST5"                    // 东五区,无夏令时
"HST10"                   // 西十区,无夏令时

// 格式2: 完整夏令时时区
"EST5EDT"                // 东五区,默认夏令时规则
"EST5EDT,M3.2.0,M11.1.0" // 完整夏令时规则

// 格式3: 带偏移量
"IST-1GMT0"              // 西一区标准时间,UTC夏令时
"<-03>3<-02>"            // 带名称的时区

2. 解析流程

from_tz_string() 方法的解析逻辑:

rust 复制代码
pub(super) fn from_tz_string(
    tz_string: &[u8],
    use_string_extensions: bool,
) -> Result<Self, Error> {
    let mut cursor = Cursor::new(tz_string);
    
    // 1. 解析标准时区名称
    let std_time_zone = Some(parse_name(&mut cursor)?);
    
    // 2. 解析标准时区偏移
    let std_offset = parse_offset(&mut cursor)?;
    
    // 3. 如果是固定时区
    if cursor.is_empty() {
        return Ok(LocalTimeType::new(-std_offset, false, std_time_zone)?.into());
    }
    
    // 4. 解析夏令时时区名称
    let dst_time_zone = Some(parse_name(&mut cursor)?);
    
    // 5. 解析夏令时偏移(可省略,默认为标准偏移-1小时)
    let dst_offset = match cursor.peek() {
        Some(&b',') => std_offset - 3600,  // 默认偏移
        Some(_) => parse_offset(&mut cursor)?,
        None => return Err(Error::UnsupportedTzString(...)),
    };
    
    // 6. 解析夏令时开始规则
    cursor.read_tag(b",")?;
    let (dst_start, dst_start_time) = RuleDay::parse(&mut cursor, use_string_extensions)?;
    
    // 7. 解析夏令时结束规则
    cursor.read_tag(b",")?;
    let (dst_end, dst_end_time) = RuleDay::parse(&mut cursor, use_string_extensions)?;
    
    // 8. 构建AlternateTime
    Ok(AlternateTime::new(...)?.into())
}

3. 时间解析函数

rust 复制代码
// 解析时区偏移量(格式: [+-]HH[:MM[:SS]])
fn parse_offset(cursor: &mut Cursor) -> Result<i32, Error>

// 解析转换时间(格式: HH[:MM[:SS]])
fn parse_rule_time(cursor: &mut Cursor) -> Result<i32, Error>

// 解析扩展转换时间(格式: [+-]HH[:MM[:SS]])
fn parse_rule_time_extended(cursor: &mut Cursor) -> Result<i32, Error>

RuleDay 的实现

1. 日期规则解析

rust 复制代码
impl RuleDay {
    fn parse(cursor: &mut Cursor, use_string_extensions: bool) -> Result<(Self, i32), Error> {
        match cursor.peek() {
            Some(b'M') => {  // 月-周-星期格式: Mmonth.week.weekday
                cursor.read_exact(1)?;
                let month = cursor.read_int()?;
                cursor.read_tag(b".")?;
                let week = cursor.read_int()?;
                cursor.read_tag(b".")?;
                let week_day = cursor.read_int()?;
                RuleDay::month_weekday(month, week, week_day)?
            }
            Some(b'J') => {  // 儒略日格式(不含2月29日): Jday
                cursor.read_exact(1)?;
                RuleDay::julian_1(cursor.read_int()?)?
            }
            _ => {  // 儒略日格式(含2月29日): day
                RuleDay::julian_0(cursor.read_int()?)?
            }
        }
    }
}

2. 计算转换日期

rust 复制代码
fn transition_date(&self, year: i32) -> (usize, i64) {
    match *self {
        RuleDay::Julian1WithoutLeap(year_day) => {
            // 不考虑闰年,直接计算
            let month = match CUMUL_DAY_IN_MONTHS_NORMAL_YEAR.binary_search(&(year_day - 1)) {
                Ok(x) => x + 1,
                Err(x) => x,
            };
            (month, year_day - CUMUL_DAY_IN_MONTHS_NORMAL_YEAR[month - 1])
        }
        RuleDay::Julian0WithLeap(year_day) => {
            // 考虑闰年,动态计算累计天数
            let leap = is_leap_year(year) as i64;
            let cumul_day_in_months = [
                0, 31, 59 + leap, 90 + leap, // ... 各月累计天数
            ];
            // 类似方法计算月份和日期
        }
        RuleDay::MonthWeekday { month, week, week_day } => {
            // 计算指定月份的指定星期几的第几周
            // 1. 计算当月第一天是星期几
            // 2. 计算目标星期几的第一次出现
            // 3. 加上(week-1)周
            // 4. 如果超出当月天数,退回一周
        }
    }
}

夏令时判断算法

1. 核心算法逻辑

AlternateTime::find_local_time_type() 方法:

rust 复制代码
fn find_local_time_type(&self, unix_time: i64) -> Result<&LocalTimeType, Error> {
    // 计算当前年份
    let current_year = UtcDateTime::from_timespec(unix_time)?.year;
    
    // 计算夏令时开始/结束的UTC时间
    let dst_start_utc = self.dst_start_time as i64 - self.std.ut_offset as i64;
    let dst_end_utc = self.dst_end_time as i64 - self.dst.ut_offset as i64;
    
    // 计算当前年份的转换时间
    let current_year_dst_start = self.dst_start.unix_time(current_year, dst_start_utc);
    let current_year_dst_end = self.dst_end.unix_time(current_year, dst_end_utc);
    
    // 根据开始/结束时间的关系,分两种情况处理
    match current_year_dst_start.cmp(&current_year_dst_end) {
        Ordering::Less | Ordering::Equal => {
            // 情况1: 开始时间 ≤ 结束时间(北半球常规模式)
            // 检查前一年、当前年、下一年的转换时间
        }
        Ordering::Greater => {
            // 情况2: 开始时间 > 结束时间(南半球模式)
            // 反向检查时间范围
        }
    }
}

2. 时间范围检查逻辑

对于北半球模式(开始时间 ≤ 结束时间):

rust 复制代码
if unix_time < current_year_dst_start {
    // 在当前年开始时间之前
    let prev_year_dst_end = self.dst_end.unix_time(current_year - 1, dst_end_utc);
    if unix_time < prev_year_dst_end {
        // 检查是否在前一年的夏令时期间
        let prev_year_dst_start = self.dst_start.unix_time(current_year - 1, dst_start_utc);
        prev_year_dst_start <= unix_time  // 是夏令时
    } else {
        false  // 不是夏令时
    }
} else if unix_time < current_year_dst_end {
    true  // 在夏令时期间
} else {
    // 在当前年结束时间之后
    let next_year_dst_start = self.dst_start.unix_time(current_year + 1, dst_start_utc);
    if next_year_dst_start <= unix_time {
        // 检查是否在下一年的夏令时期间
        let next_year_dst_end = self.dst_end.unix_time(current_year + 1, dst_end_utc);
        unix_time < next_year_dst_end  // 是夏令时
    } else {
        false  // 不是夏令时
    }
}

本地时间查找算法

1. 处理模糊时间

find_local_time_type_from_local() 方法处理四种情况:

rust 复制代码
fn find_local_time_type_from_local(
    &self,
    local_time: NaiveDateTime,
) -> Result<MappedLocalTime<LocalTimeType>, Error> {
    match self.std.ut_offset.cmp(&self.dst.ut_offset) {
        Ordering::Equal => {
            // 情况1: 偏移相同,没有真正转换
            MappedLocalTime::Single(self.std)
        }
        Ordering::Less => {
            // 情况2: 夏令时偏移更大(时钟向前跳)
            if dst_start < dst_end {
                // 北半球: 春季向前跳,秋季向后跳
                self.handle_northern_hemisphere(...)
            } else {
                // 南半球: 相反的季节
                self.handle_southern_hemisphere(...)
            }
        }
        Ordering::Greater => {
            // 情况3: 夏令时偏移更小(反向夏令时)
            if dst_start < dst_end {
                // 南半球反向模式
                self.handle_southern_reverse(...)
            } else {
                // 北半球反向模式
                self.handle_northern_reverse(...)
            }
        }
    }
}

2. 北半球常规模式处理

rust 复制代码
// dst_start < dst_end 且 std.offset < dst.offset
if local_time <= dst_start_transition_start {
    // 转换开始前:标准时间
    MappedLocalTime::Single(self.std)
} else if local_time > dst_start_transition_start 
        && local_time < dst_start_transition_end {
    // 转换期间:不存在的时间(向前跳)
    MappedLocalTime::None
} else if local_time >= dst_start_transition_end 
        && local_time < dst_end_transition_end {
    // 夏令时期间:夏令时
    MappedLocalTime::Single(self.dst)
} else if local_time >= dst_end_transition_end 
        && local_time <= dst_end_transition_start {
    // 转换结束期间:模糊时间(向后跳)
    MappedLocalTime::Ambiguous(self.std, self.dst)
} else {
    // 转换结束后:标准时间
    MappedLocalTime::Single(self.std)
}

3. 时间计算

转换时间的计算:

rust 复制代码
// 夏令时开始转换:
// 开始:标准时间的本地时间
dst_start_transition_start = dst_start.unix_time(year, 0) + dst_start_time
// 结束:夏令时的本地时间(考虑了偏移变化)
dst_start_transition_end = dst_start_transition_start + dst.offset - std.offset

// 夏令时结束转换:
// 开始:夏令时的本地时间
dst_end_transition_start = dst_end.unix_time(year, 0) + dst_end_time
// 结束:标准时间的本地时间
dst_end_transition_end = dst_end_transition_start + std.offset - dst.offset

时间日期转换工具

1. UtcDateTime 结构体

用于在Unix时间和日期时间之间转换:

rust 复制代码
pub(crate) struct UtcDateTime {
    pub(crate) year: i32,
    pub(crate) month: u8,       // 1-12
    pub(crate) month_day: u8,   // 1-31
    pub(crate) hour: u8,        // 0-23
    pub(crate) minute: u8,      // 0-59
    pub(crate) second: u8,      // 0-60(包含闰秒)
}

2. Unix时间转日期算法

from_timespec() 方法使用优化算法:

rust 复制代码
pub(crate) fn from_timespec(unix_time: i64) -> Result<Self, Error> {
    // 1. 转换为自2000-03-01以来的秒数
    let seconds = unix_time - UNIX_OFFSET_SECS;
    
    // 2. 计算天数
    let mut days = seconds / SECONDS_PER_DAY;
    let mut secs = seconds % SECONDS_PER_DAY;
    
    // 3. 按400年周期分组计算
    let cycles_400 = days / DAYS_PER_400_YEARS;      // 400年周期数
    days %= DAYS_PER_400_YEARS;
    
    let cycles_100 = days / DAYS_PER_100_YEARS;      // 100年周期数(最多3)
    days -= cycles_100 * DAYS_PER_100_YEARS;
    
    let cycles_4 = days / DAYS_PER_4_YEARS;          // 4年周期数(最多24)
    days -= cycles_4 * DAYS_PER_4_YEARS;
    
    let years = days / DAYS_PER_NORMAL_YEAR;         // 剩余年数(最多3)
    days -= years * DAYS_PER_NORMAL_YEAR;
    
    // 4. 计算年份
    let year = 2000 + years + cycles_4 * 4 + cycles_100 * 100 + cycles_400 * 400;
    
    // 5. 计算月份(从3月开始,简化闰年处理)
    let mut month = 0;
    while month < DAY_IN_MONTHS_LEAP_YEAR_FROM_MARCH.len() {
        if days < DAY_IN_MONTHS_LEAP_YEAR_FROM_MARCH[month] {
            break;
        }
        days -= DAY_IN_MONTHS_LEAP_YEAR_FROM_MARCH[month];
        month += 1;
    }
    
    // 6. 调整月份(从3月转回1月起始)
    month += 2;
    if month >= 12 {
        month -= 12;
        year += 1;
    }
    month += 1;
    
    // 7. 计算日、时、分、秒
    let month_day = 1 + days;
    let hour = secs / 3600;
    let minute = (secs / 60) % 60;
    let second = secs % 60;
    
    Ok(UtcDateTime { ... })
}

辅助函数

1. 天数计算

rust 复制代码
// 计算自Unix纪元以来的天数
const fn days_since_unix_epoch(year: i32, month: usize, month_day: i64) -> i64 {
    let is_leap = is_leap_year(year);
    let year = year as i64;
    
    let mut days = (year - 1970) * 365;
    
    if year >= 1970 {
        // 1970年后的闰年处理
        days += (year - 1968) / 4;
        days -= (year - 1900) / 100;
        days += (year - 1600) / 400;
        if is_leap && month < 3 {
            days -= 1;
        }
    } else {
        // 1970年前的闰年处理
        days += (year - 1972) / 4;
        days -= (year - 2000) / 100;
        days += (year - 2000) / 400;
        if is_leap && month >= 3 {
            days += 1;
        }
    }
    
    days + CUMUL_DAY_IN_MONTHS_NORMAL_YEAR[month - 1] + month_day - 1
}

// 闰年判断
const fn is_leap_year(year: i32) -> bool {
    year % 400 == 0 || (year % 4 == 0 && year % 100 != 0)
}

使用示例

1. 解析TZ字符串

rust 复制代码
// 固定时区
let fixed = TransitionRule::from_tz_string(b"EST5", false)?;
// AlternateTime { std: EST(-18000), dst: 同std }

// 完整夏令时时区
let dst = TransitionRule::from_tz_string(
    b"EST5EDT,M3.2.0,M11.1.0", 
    false
)?;
// AlternateTime { std: EST(-18000), dst: EDT(-14400), ... }

// 带引号的时区名称
let quoted = TransitionRule::from_tz_string(
    b"<-03>3<-02>,M3.5.0,M10.5.0", 
    true
)?;

2. 时间查找

rust 复制代码
// 创建转换规则
let rule = TransitionRule::from_tz_string(
    b"EST5EDT,M3.2.0/02:00,M11.1.0/02:00",
    false
)?;

// 查找特定时间的时区类型
let winter_time = rule.find_local_time_type(1609459200)?;  // 2021-01-01: 标准时间
let summer_time = rule.find_local_time_type(1625097600)?;  // 2021-07-01: 夏令时

// 处理本地时间(考虑模糊时间)
let ambiguous_time = NaiveDateTime::from_ymd(2023, 11, 5)
    .and_hms(1, 30, 0);
    
match rule.find_local_time_type_from_local(ambiguous_time)? {
    MappedLocalTime::Single(t) => println!("Unique: {:?}", t),
    MappedLocalTime::Ambiguous(t1, t2) => {
        println!("Ambiguous: {:?} or {:?}", t1, t2);
    }
    MappedLocalTime::None => println!("Time does not exist"),
}

关键特性

1. 扩展支持

  • 标准POSIX TZ字符串:基本格式支持
  • RFC 8536扩展:支持负时间、超过24小时的时间
  • 引号时区名称 :支持<Name>格式

2. 健壮性处理

  • 闰年处理:正确识别2月29日
  • 溢出检查:防止整数溢出
  • 边界情况:处理跨年转换

3. 性能优化

  • 二分查找:快速定位月份
  • 预计算常量:减少运行时计算
  • 避免浮点数:全部使用整数运算

这个实现提供了完整的POSIX TZ字符串解析和时区转换功能,能够正确处理各种夏令时规则和边界情况。

附源码

rust 复制代码
use super::parser::Cursor;
use super::timezone::{LocalTimeType, SECONDS_PER_WEEK};
use super::{
    CUMUL_DAY_IN_MONTHS_NORMAL_YEAR, DAY_IN_MONTHS_NORMAL_YEAR, DAYS_PER_WEEK, Error,
    SECONDS_PER_DAY,
};
use crate::{Datelike, NaiveDateTime};
use std::cmp::Ordering;

/// Transition rule
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub(super) enum TransitionRule {
    /// Fixed local time type
    Fixed(LocalTimeType),
    /// Alternate local time types
    Alternate(AlternateTime),
}

impl TransitionRule {
    /// Parse a POSIX TZ string containing a time zone description, as described in [the POSIX documentation of the `TZ` environment variable](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html).
    ///
    /// TZ string extensions from [RFC 8536](https://datatracker.ietf.org/doc/html/rfc8536#section-3.3.1) may be used.
    pub(super) fn from_tz_string(
        tz_string: &[u8],
        use_string_extensions: bool,
    ) -> Result<Self, Error> {
        let mut cursor = Cursor::new(tz_string);

        let std_time_zone = Some(parse_name(&mut cursor)?);
        let std_offset = parse_offset(&mut cursor)?;

        if cursor.is_empty() {
            return Ok(LocalTimeType::new(-std_offset, false, std_time_zone)?.into());
        }

        let dst_time_zone = Some(parse_name(&mut cursor)?);

        let dst_offset = match cursor.peek() {
            Some(&b',') => std_offset - 3600,
            Some(_) => parse_offset(&mut cursor)?,
            None => {
                return Err(Error::UnsupportedTzString("DST start and end rules must be provided"));
            }
        };

        if cursor.is_empty() {
            return Err(Error::UnsupportedTzString("DST start and end rules must be provided"));
        }

        cursor.read_tag(b",")?;
        let (dst_start, dst_start_time) = RuleDay::parse(&mut cursor, use_string_extensions)?;

        cursor.read_tag(b",")?;
        let (dst_end, dst_end_time) = RuleDay::parse(&mut cursor, use_string_extensions)?;

        if !cursor.is_empty() {
            return Err(Error::InvalidTzString("remaining data after parsing TZ string"));
        }

        Ok(AlternateTime::new(
            LocalTimeType::new(-std_offset, false, std_time_zone)?,
            LocalTimeType::new(-dst_offset, true, dst_time_zone)?,
            dst_start,
            dst_start_time,
            dst_end,
            dst_end_time,
        )?
        .into())
    }

    /// Find the local time type associated to the transition rule at the specified Unix time in seconds
    pub(super) fn find_local_time_type(&self, unix_time: i64) -> Result<&LocalTimeType, Error> {
        match self {
            TransitionRule::Fixed(local_time_type) => Ok(local_time_type),
            TransitionRule::Alternate(alternate_time) => {
                alternate_time.find_local_time_type(unix_time)
            }
        }
    }

    /// Find the local time type associated to the transition rule at the specified Unix time in seconds
    pub(super) fn find_local_time_type_from_local(
        &self,
        local_time: NaiveDateTime,
    ) -> Result<crate::MappedLocalTime<LocalTimeType>, Error> {
        match self {
            TransitionRule::Fixed(local_time_type) => {
                Ok(crate::MappedLocalTime::Single(*local_time_type))
            }
            TransitionRule::Alternate(alternate_time) => {
                alternate_time.find_local_time_type_from_local(local_time)
            }
        }
    }
}

impl From<LocalTimeType> for TransitionRule {
    fn from(inner: LocalTimeType) -> Self {
        TransitionRule::Fixed(inner)
    }
}

impl From<AlternateTime> for TransitionRule {
    fn from(inner: AlternateTime) -> Self {
        TransitionRule::Alternate(inner)
    }
}

/// Transition rule representing alternate local time types
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub(super) struct AlternateTime {
    /// Local time type for standard time
    pub(super) std: LocalTimeType,
    /// Local time type for Daylight Saving Time
    pub(super) dst: LocalTimeType,
    /// Start day of Daylight Saving Time
    dst_start: RuleDay,
    /// Local start day time of Daylight Saving Time, in seconds
    dst_start_time: i32,
    /// End day of Daylight Saving Time
    dst_end: RuleDay,
    /// Local end day time of Daylight Saving Time, in seconds
    dst_end_time: i32,
}

impl AlternateTime {
    /// Construct a transition rule representing alternate local time types
    const fn new(
        std: LocalTimeType,
        dst: LocalTimeType,
        dst_start: RuleDay,
        dst_start_time: i32,
        dst_end: RuleDay,
        dst_end_time: i32,
    ) -> Result<Self, Error> {
        // Overflow is not possible
        if !((dst_start_time as i64).abs() < SECONDS_PER_WEEK
            && (dst_end_time as i64).abs() < SECONDS_PER_WEEK)
        {
            return Err(Error::TransitionRule("invalid DST start or end time"));
        }

        Ok(Self { std, dst, dst_start, dst_start_time, dst_end, dst_end_time })
    }

    /// Find the local time type associated to the alternate transition rule at the specified Unix time in seconds
    fn find_local_time_type(&self, unix_time: i64) -> Result<&LocalTimeType, Error> {
        // Overflow is not possible
        let dst_start_time_in_utc = self.dst_start_time as i64 - self.std.ut_offset as i64;
        let dst_end_time_in_utc = self.dst_end_time as i64 - self.dst.ut_offset as i64;

        let current_year = match UtcDateTime::from_timespec(unix_time) {
            Ok(dt) => dt.year,
            Err(error) => return Err(error),
        };

        // Check if the current year is valid for the following computations
        if !(i32::MIN + 2..=i32::MAX - 2).contains(&current_year) {
            return Err(Error::OutOfRange("out of range date time"));
        }

        let current_year_dst_start_unix_time =
            self.dst_start.unix_time(current_year, dst_start_time_in_utc);
        let current_year_dst_end_unix_time =
            self.dst_end.unix_time(current_year, dst_end_time_in_utc);

        // Check DST start/end Unix times for previous/current/next years to support for transition day times outside of [0h, 24h] range
        let is_dst =
            match Ord::cmp(&current_year_dst_start_unix_time, &current_year_dst_end_unix_time) {
                Ordering::Less | Ordering::Equal => {
                    if unix_time < current_year_dst_start_unix_time {
                        let previous_year_dst_end_unix_time =
                            self.dst_end.unix_time(current_year - 1, dst_end_time_in_utc);
                        if unix_time < previous_year_dst_end_unix_time {
                            let previous_year_dst_start_unix_time =
                                self.dst_start.unix_time(current_year - 1, dst_start_time_in_utc);
                            previous_year_dst_start_unix_time <= unix_time
                        } else {
                            false
                        }
                    } else if unix_time < current_year_dst_end_unix_time {
                        true
                    } else {
                        let next_year_dst_start_unix_time =
                            self.dst_start.unix_time(current_year + 1, dst_start_time_in_utc);
                        if next_year_dst_start_unix_time <= unix_time {
                            let next_year_dst_end_unix_time =
                                self.dst_end.unix_time(current_year + 1, dst_end_time_in_utc);
                            unix_time < next_year_dst_end_unix_time
                        } else {
                            false
                        }
                    }
                }
                Ordering::Greater => {
                    if unix_time < current_year_dst_end_unix_time {
                        let previous_year_dst_start_unix_time =
                            self.dst_start.unix_time(current_year - 1, dst_start_time_in_utc);
                        if unix_time < previous_year_dst_start_unix_time {
                            let previous_year_dst_end_unix_time =
                                self.dst_end.unix_time(current_year - 1, dst_end_time_in_utc);
                            unix_time < previous_year_dst_end_unix_time
                        } else {
                            true
                        }
                    } else if unix_time < current_year_dst_start_unix_time {
                        false
                    } else {
                        let next_year_dst_end_unix_time =
                            self.dst_end.unix_time(current_year + 1, dst_end_time_in_utc);
                        if next_year_dst_end_unix_time <= unix_time {
                            let next_year_dst_start_unix_time =
                                self.dst_start.unix_time(current_year + 1, dst_start_time_in_utc);
                            next_year_dst_start_unix_time <= unix_time
                        } else {
                            true
                        }
                    }
                }
            };

        if is_dst { Ok(&self.dst) } else { Ok(&self.std) }
    }

    fn find_local_time_type_from_local(
        &self,
        local_time: NaiveDateTime,
    ) -> Result<crate::MappedLocalTime<LocalTimeType>, Error> {
        // Year must be between i32::MIN + 2 and i32::MAX - 2, year in NaiveDate is always smaller.
        let current_year = local_time.year();
        let local_time = local_time.and_utc().timestamp();

        let dst_start_transition_start =
            self.dst_start.unix_time(current_year, 0) + i64::from(self.dst_start_time);
        let dst_start_transition_end = self.dst_start.unix_time(current_year, 0)
            + i64::from(self.dst_start_time)
            + i64::from(self.dst.ut_offset)
            - i64::from(self.std.ut_offset);

        let dst_end_transition_start =
            self.dst_end.unix_time(current_year, 0) + i64::from(self.dst_end_time);
        let dst_end_transition_end = self.dst_end.unix_time(current_year, 0)
            + i64::from(self.dst_end_time)
            + i64::from(self.std.ut_offset)
            - i64::from(self.dst.ut_offset);

        match self.std.ut_offset.cmp(&self.dst.ut_offset) {
            Ordering::Equal => Ok(crate::MappedLocalTime::Single(self.std)),
            Ordering::Less => {
                if self.dst_start.transition_date(current_year).0
                    < self.dst_end.transition_date(current_year).0
                {
                    // northern hemisphere
                    // For the DST END transition, the `start` happens at a later timestamp than the `end`.
                    if local_time <= dst_start_transition_start {
                        Ok(crate::MappedLocalTime::Single(self.std))
                    } else if local_time > dst_start_transition_start
                        && local_time < dst_start_transition_end
                    {
                        Ok(crate::MappedLocalTime::None)
                    } else if local_time >= dst_start_transition_end
                        && local_time < dst_end_transition_end
                    {
                        Ok(crate::MappedLocalTime::Single(self.dst))
                    } else if local_time >= dst_end_transition_end
                        && local_time <= dst_end_transition_start
                    {
                        Ok(crate::MappedLocalTime::Ambiguous(self.std, self.dst))
                    } else {
                        Ok(crate::MappedLocalTime::Single(self.std))
                    }
                } else {
                    // southern hemisphere regular DST
                    // For the DST END transition, the `start` happens at a later timestamp than the `end`.
                    if local_time < dst_end_transition_end {
                        Ok(crate::MappedLocalTime::Single(self.dst))
                    } else if local_time >= dst_end_transition_end
                        && local_time <= dst_end_transition_start
                    {
                        Ok(crate::MappedLocalTime::Ambiguous(self.std, self.dst))
                    } else if local_time > dst_end_transition_end
                        && local_time < dst_start_transition_start
                    {
                        Ok(crate::MappedLocalTime::Single(self.std))
                    } else if local_time >= dst_start_transition_start
                        && local_time < dst_start_transition_end
                    {
                        Ok(crate::MappedLocalTime::None)
                    } else {
                        Ok(crate::MappedLocalTime::Single(self.dst))
                    }
                }
            }
            Ordering::Greater => {
                if self.dst_start.transition_date(current_year).0
                    < self.dst_end.transition_date(current_year).0
                {
                    // southern hemisphere reverse DST
                    // For the DST END transition, the `start` happens at a later timestamp than the `end`.
                    if local_time < dst_start_transition_end {
                        Ok(crate::MappedLocalTime::Single(self.std))
                    } else if local_time >= dst_start_transition_end
                        && local_time <= dst_start_transition_start
                    {
                        Ok(crate::MappedLocalTime::Ambiguous(self.dst, self.std))
                    } else if local_time > dst_start_transition_start
                        && local_time < dst_end_transition_start
                    {
                        Ok(crate::MappedLocalTime::Single(self.dst))
                    } else if local_time >= dst_end_transition_start
                        && local_time < dst_end_transition_end
                    {
                        Ok(crate::MappedLocalTime::None)
                    } else {
                        Ok(crate::MappedLocalTime::Single(self.std))
                    }
                } else {
                    // northern hemisphere reverse DST
                    // For the DST END transition, the `start` happens at a later timestamp than the `end`.
                    if local_time <= dst_end_transition_start {
                        Ok(crate::MappedLocalTime::Single(self.dst))
                    } else if local_time > dst_end_transition_start
                        && local_time < dst_end_transition_end
                    {
                        Ok(crate::MappedLocalTime::None)
                    } else if local_time >= dst_end_transition_end
                        && local_time < dst_start_transition_end
                    {
                        Ok(crate::MappedLocalTime::Single(self.std))
                    } else if local_time >= dst_start_transition_end
                        && local_time <= dst_start_transition_start
                    {
                        Ok(crate::MappedLocalTime::Ambiguous(self.dst, self.std))
                    } else {
                        Ok(crate::MappedLocalTime::Single(self.dst))
                    }
                }
            }
        }
    }
}

/// Parse time zone name
fn parse_name<'a>(cursor: &mut Cursor<'a>) -> Result<&'a [u8], Error> {
    match cursor.peek() {
        Some(b'<') => {}
        _ => return Ok(cursor.read_while(u8::is_ascii_alphabetic)?),
    }

    cursor.read_exact(1)?;
    let unquoted = cursor.read_until(|&x| x == b'>')?;
    cursor.read_exact(1)?;
    Ok(unquoted)
}

/// Parse time zone offset
fn parse_offset(cursor: &mut Cursor) -> Result<i32, Error> {
    let (sign, hour, minute, second) = parse_signed_hhmmss(cursor)?;

    if !(0..=24).contains(&hour) {
        return Err(Error::InvalidTzString("invalid offset hour"));
    }
    if !(0..=59).contains(&minute) {
        return Err(Error::InvalidTzString("invalid offset minute"));
    }
    if !(0..=59).contains(&second) {
        return Err(Error::InvalidTzString("invalid offset second"));
    }

    Ok(sign * (hour * 3600 + minute * 60 + second))
}

/// Parse transition rule time
fn parse_rule_time(cursor: &mut Cursor) -> Result<i32, Error> {
    let (hour, minute, second) = parse_hhmmss(cursor)?;

    if !(0..=24).contains(&hour) {
        return Err(Error::InvalidTzString("invalid day time hour"));
    }
    if !(0..=59).contains(&minute) {
        return Err(Error::InvalidTzString("invalid day time minute"));
    }
    if !(0..=59).contains(&second) {
        return Err(Error::InvalidTzString("invalid day time second"));
    }

    Ok(hour * 3600 + minute * 60 + second)
}

/// Parse transition rule time with TZ string extensions
fn parse_rule_time_extended(cursor: &mut Cursor) -> Result<i32, Error> {
    let (sign, hour, minute, second) = parse_signed_hhmmss(cursor)?;

    if !(-167..=167).contains(&hour) {
        return Err(Error::InvalidTzString("invalid day time hour"));
    }
    if !(0..=59).contains(&minute) {
        return Err(Error::InvalidTzString("invalid day time minute"));
    }
    if !(0..=59).contains(&second) {
        return Err(Error::InvalidTzString("invalid day time second"));
    }

    Ok(sign * (hour * 3600 + minute * 60 + second))
}

/// Parse hours, minutes and seconds
fn parse_hhmmss(cursor: &mut Cursor) -> Result<(i32, i32, i32), Error> {
    let hour = cursor.read_int()?;

    let mut minute = 0;
    let mut second = 0;

    if cursor.read_optional_tag(b":")? {
        minute = cursor.read_int()?;

        if cursor.read_optional_tag(b":")? {
            second = cursor.read_int()?;
        }
    }

    Ok((hour, minute, second))
}

/// Parse signed hours, minutes and seconds
fn parse_signed_hhmmss(cursor: &mut Cursor) -> Result<(i32, i32, i32, i32), Error> {
    let mut sign = 1;
    if let Some(&c) = cursor.peek() {
        if c == b'+' || c == b'-' {
            cursor.read_exact(1)?;
            if c == b'-' {
                sign = -1;
            }
        }
    }

    let (hour, minute, second) = parse_hhmmss(cursor)?;
    Ok((sign, hour, minute, second))
}

/// Transition rule day
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
enum RuleDay {
    /// Julian day in `[1, 365]`, without taking occasional Feb 29 into account, which is not referenceable
    Julian1WithoutLeap(u16),
    /// Zero-based Julian day in `[0, 365]`, taking occasional Feb 29 into account
    Julian0WithLeap(u16),
    /// Day represented by a month, a month week and a week day
    MonthWeekday {
        /// Month in `[1, 12]`
        month: u8,
        /// Week of the month in `[1, 5]`, with `5` representing the last week of the month
        week: u8,
        /// Day of the week in `[0, 6]` from Sunday
        week_day: u8,
    },
}

impl RuleDay {
    /// Parse transition rule
    fn parse(cursor: &mut Cursor, use_string_extensions: bool) -> Result<(Self, i32), Error> {
        let date = match cursor.peek() {
            Some(b'M') => {
                cursor.read_exact(1)?;
                let month = cursor.read_int()?;
                cursor.read_tag(b".")?;
                let week = cursor.read_int()?;
                cursor.read_tag(b".")?;
                let week_day = cursor.read_int()?;
                RuleDay::month_weekday(month, week, week_day)?
            }
            Some(b'J') => {
                cursor.read_exact(1)?;
                RuleDay::julian_1(cursor.read_int()?)?
            }
            _ => RuleDay::julian_0(cursor.read_int()?)?,
        };

        Ok((
            date,
            match (cursor.read_optional_tag(b"/")?, use_string_extensions) {
                (false, _) => 2 * 3600,
                (true, true) => parse_rule_time_extended(cursor)?,
                (true, false) => parse_rule_time(cursor)?,
            },
        ))
    }

    /// Construct a transition rule day represented by a Julian day in `[1, 365]`, without taking occasional Feb 29 into account, which is not referenceable
    fn julian_1(julian_day_1: u16) -> Result<Self, Error> {
        if !(1..=365).contains(&julian_day_1) {
            return Err(Error::TransitionRule("invalid rule day julian day"));
        }

        Ok(RuleDay::Julian1WithoutLeap(julian_day_1))
    }

    /// Construct a transition rule day represented by a zero-based Julian day in `[0, 365]`, taking occasional Feb 29 into account
    const fn julian_0(julian_day_0: u16) -> Result<Self, Error> {
        if julian_day_0 > 365 {
            return Err(Error::TransitionRule("invalid rule day julian day"));
        }

        Ok(RuleDay::Julian0WithLeap(julian_day_0))
    }

    /// Construct a transition rule day represented by a month, a month week and a week day
    fn month_weekday(month: u8, week: u8, week_day: u8) -> Result<Self, Error> {
        if !(1..=12).contains(&month) {
            return Err(Error::TransitionRule("invalid rule day month"));
        }

        if !(1..=5).contains(&week) {
            return Err(Error::TransitionRule("invalid rule day week"));
        }

        if week_day > 6 {
            return Err(Error::TransitionRule("invalid rule day week day"));
        }

        Ok(RuleDay::MonthWeekday { month, week, week_day })
    }

    /// Get the transition date for the provided year
    ///
    /// ## Outputs
    ///
    /// * `month`: Month in `[1, 12]`
    /// * `month_day`: Day of the month in `[1, 31]`
    fn transition_date(&self, year: i32) -> (usize, i64) {
        match *self {
            RuleDay::Julian1WithoutLeap(year_day) => {
                let year_day = year_day as i64;

                let month = match CUMUL_DAY_IN_MONTHS_NORMAL_YEAR.binary_search(&(year_day - 1)) {
                    Ok(x) => x + 1,
                    Err(x) => x,
                };

                let month_day = year_day - CUMUL_DAY_IN_MONTHS_NORMAL_YEAR[month - 1];

                (month, month_day)
            }
            RuleDay::Julian0WithLeap(year_day) => {
                let leap = is_leap_year(year) as i64;

                let cumul_day_in_months = [
                    0,
                    31,
                    59 + leap,
                    90 + leap,
                    120 + leap,
                    151 + leap,
                    181 + leap,
                    212 + leap,
                    243 + leap,
                    273 + leap,
                    304 + leap,
                    334 + leap,
                ];

                let year_day = year_day as i64;

                let month = match cumul_day_in_months.binary_search(&year_day) {
                    Ok(x) => x + 1,
                    Err(x) => x,
                };

                let month_day = 1 + year_day - cumul_day_in_months[month - 1];

                (month, month_day)
            }
            RuleDay::MonthWeekday { month: rule_month, week, week_day } => {
                let leap = is_leap_year(year) as i64;

                let month = rule_month as usize;

                let mut day_in_month = DAY_IN_MONTHS_NORMAL_YEAR[month - 1];
                if month == 2 {
                    day_in_month += leap;
                }

                let week_day_of_first_month_day =
                    (4 + days_since_unix_epoch(year, month, 1)).rem_euclid(DAYS_PER_WEEK);
                let first_week_day_occurrence_in_month =
                    1 + (week_day as i64 - week_day_of_first_month_day).rem_euclid(DAYS_PER_WEEK);

                let mut month_day =
                    first_week_day_occurrence_in_month + (week as i64 - 1) * DAYS_PER_WEEK;
                if month_day > day_in_month {
                    month_day -= DAYS_PER_WEEK
                }

                (month, month_day)
            }
        }
    }

    /// Returns the UTC Unix time in seconds associated to the transition date for the provided year
    fn unix_time(&self, year: i32, day_time_in_utc: i64) -> i64 {
        let (month, month_day) = self.transition_date(year);
        days_since_unix_epoch(year, month, month_day) * SECONDS_PER_DAY + day_time_in_utc
    }
}

/// UTC date time exprimed in the [proleptic gregorian calendar](https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar)
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub(crate) struct UtcDateTime {
    /// Year
    pub(crate) year: i32,
    /// Month in `[1, 12]`
    pub(crate) month: u8,
    /// Day of the month in `[1, 31]`
    pub(crate) month_day: u8,
    /// Hours since midnight in `[0, 23]`
    pub(crate) hour: u8,
    /// Minutes in `[0, 59]`
    pub(crate) minute: u8,
    /// Seconds in `[0, 60]`, with a possible leap second
    pub(crate) second: u8,
}

impl UtcDateTime {
    /// Construct a UTC date time from a Unix time in seconds and nanoseconds
    pub(crate) fn from_timespec(unix_time: i64) -> Result<Self, Error> {
        let seconds = match unix_time.checked_sub(UNIX_OFFSET_SECS) {
            Some(seconds) => seconds,
            None => return Err(Error::OutOfRange("out of range operation")),
        };

        let mut remaining_days = seconds / SECONDS_PER_DAY;
        let mut remaining_seconds = seconds % SECONDS_PER_DAY;
        if remaining_seconds < 0 {
            remaining_seconds += SECONDS_PER_DAY;
            remaining_days -= 1;
        }

        let mut cycles_400_years = remaining_days / DAYS_PER_400_YEARS;
        remaining_days %= DAYS_PER_400_YEARS;
        if remaining_days < 0 {
            remaining_days += DAYS_PER_400_YEARS;
            cycles_400_years -= 1;
        }

        let cycles_100_years = Ord::min(remaining_days / DAYS_PER_100_YEARS, 3);
        remaining_days -= cycles_100_years * DAYS_PER_100_YEARS;

        let cycles_4_years = Ord::min(remaining_days / DAYS_PER_4_YEARS, 24);
        remaining_days -= cycles_4_years * DAYS_PER_4_YEARS;

        let remaining_years = Ord::min(remaining_days / DAYS_PER_NORMAL_YEAR, 3);
        remaining_days -= remaining_years * DAYS_PER_NORMAL_YEAR;

        let mut year = OFFSET_YEAR
            + remaining_years
            + cycles_4_years * 4
            + cycles_100_years * 100
            + cycles_400_years * 400;

        let mut month = 0;
        while month < DAY_IN_MONTHS_LEAP_YEAR_FROM_MARCH.len() {
            let days = DAY_IN_MONTHS_LEAP_YEAR_FROM_MARCH[month];
            if remaining_days < days {
                break;
            }
            remaining_days -= days;
            month += 1;
        }
        month += 2;

        if month >= MONTHS_PER_YEAR as usize {
            month -= MONTHS_PER_YEAR as usize;
            year += 1;
        }
        month += 1;

        let month_day = 1 + remaining_days;

        let hour = remaining_seconds / SECONDS_PER_HOUR;
        let minute = (remaining_seconds / SECONDS_PER_MINUTE) % MINUTES_PER_HOUR;
        let second = remaining_seconds % SECONDS_PER_MINUTE;

        let year = match year >= i32::MIN as i64 && year <= i32::MAX as i64 {
            true => year as i32,
            false => return Err(Error::OutOfRange("i64 is out of range for i32")),
        };

        Ok(Self {
            year,
            month: month as u8,
            month_day: month_day as u8,
            hour: hour as u8,
            minute: minute as u8,
            second: second as u8,
        })
    }
}

/// Number of nanoseconds in one second
const NANOSECONDS_PER_SECOND: u32 = 1_000_000_000;
/// Number of seconds in one minute
const SECONDS_PER_MINUTE: i64 = 60;
/// Number of seconds in one hour
const SECONDS_PER_HOUR: i64 = 3600;
/// Number of minutes in one hour
const MINUTES_PER_HOUR: i64 = 60;
/// Number of months in one year
const MONTHS_PER_YEAR: i64 = 12;
/// Number of days in a normal year
const DAYS_PER_NORMAL_YEAR: i64 = 365;
/// Number of days in 4 years (including 1 leap year)
const DAYS_PER_4_YEARS: i64 = DAYS_PER_NORMAL_YEAR * 4 + 1;
/// Number of days in 100 years (including 24 leap years)
const DAYS_PER_100_YEARS: i64 = DAYS_PER_NORMAL_YEAR * 100 + 24;
/// Number of days in 400 years (including 97 leap years)
const DAYS_PER_400_YEARS: i64 = DAYS_PER_NORMAL_YEAR * 400 + 97;
/// Unix time at `2000-03-01T00:00:00Z` (Wednesday)
const UNIX_OFFSET_SECS: i64 = 951868800;
/// Offset year
const OFFSET_YEAR: i64 = 2000;
/// Month days in a leap year from March
const DAY_IN_MONTHS_LEAP_YEAR_FROM_MARCH: [i64; 12] =
    [31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 31, 29];

/// Compute the number of days since Unix epoch (`1970-01-01T00:00:00Z`).
///
/// ## Inputs
///
/// * `year`: Year
/// * `month`: Month in `[1, 12]`
/// * `month_day`: Day of the month in `[1, 31]`
pub(crate) const fn days_since_unix_epoch(year: i32, month: usize, month_day: i64) -> i64 {
    let is_leap_year = is_leap_year(year);

    let year = year as i64;

    let mut result = (year - 1970) * 365;

    if year >= 1970 {
        result += (year - 1968) / 4;
        result -= (year - 1900) / 100;
        result += (year - 1600) / 400;

        if is_leap_year && month < 3 {
            result -= 1;
        }
    } else {
        result += (year - 1972) / 4;
        result -= (year - 2000) / 100;
        result += (year - 2000) / 400;

        if is_leap_year && month >= 3 {
            result += 1;
        }
    }

    result += CUMUL_DAY_IN_MONTHS_NORMAL_YEAR[month - 1] + month_day - 1;

    result
}

/// Check if a year is a leap year
pub(crate) const fn is_leap_year(year: i32) -> bool {
    year % 400 == 0 || (year % 4 == 0 && year % 100 != 0)
}

#[cfg(test)]
mod tests {
    use super::super::timezone::Transition;
    use super::super::{Error, TimeZone};
    use super::{AlternateTime, LocalTimeType, RuleDay, TransitionRule};

    #[test]
    fn test_quoted() -> Result<(), Error> {
        let transition_rule = TransitionRule::from_tz_string(b"<-03>+3<+03>-3,J1,J365", false)?;
        assert_eq!(
            transition_rule,
            AlternateTime::new(
                LocalTimeType::new(-10800, false, Some(b"-03"))?,
                LocalTimeType::new(10800, true, Some(b"+03"))?,
                RuleDay::julian_1(1)?,
                7200,
                RuleDay::julian_1(365)?,
                7200,
            )?
            .into()
        );
        Ok(())
    }

    #[test]
    fn test_full() -> Result<(), Error> {
        let tz_string = b"NZST-12:00:00NZDT-13:00:00,M10.1.0/02:00:00,M3.3.0/02:00:00";
        let transition_rule = TransitionRule::from_tz_string(tz_string, false)?;
        assert_eq!(
            transition_rule,
            AlternateTime::new(
                LocalTimeType::new(43200, false, Some(b"NZST"))?,
                LocalTimeType::new(46800, true, Some(b"NZDT"))?,
                RuleDay::month_weekday(10, 1, 0)?,
                7200,
                RuleDay::month_weekday(3, 3, 0)?,
                7200,
            )?
            .into()
        );
        Ok(())
    }

    #[test]
    fn test_negative_dst() -> Result<(), Error> {
        let tz_string = b"IST-1GMT0,M10.5.0,M3.5.0/1";
        let transition_rule = TransitionRule::from_tz_string(tz_string, false)?;
        assert_eq!(
            transition_rule,
            AlternateTime::new(
                LocalTimeType::new(3600, false, Some(b"IST"))?,
                LocalTimeType::new(0, true, Some(b"GMT"))?,
                RuleDay::month_weekday(10, 5, 0)?,
                7200,
                RuleDay::month_weekday(3, 5, 0)?,
                3600,
            )?
            .into()
        );
        Ok(())
    }

    #[test]
    fn test_negative_hour() -> Result<(), Error> {
        let tz_string = b"<-03>3<-02>,M3.5.0/-2,M10.5.0/-1";
        assert!(TransitionRule::from_tz_string(tz_string, false).is_err());

        assert_eq!(
            TransitionRule::from_tz_string(tz_string, true)?,
            AlternateTime::new(
                LocalTimeType::new(-10800, false, Some(b"-03"))?,
                LocalTimeType::new(-7200, true, Some(b"-02"))?,
                RuleDay::month_weekday(3, 5, 0)?,
                -7200,
                RuleDay::month_weekday(10, 5, 0)?,
                -3600,
            )?
            .into()
        );
        Ok(())
    }

    #[test]
    fn test_all_year_dst() -> Result<(), Error> {
        let tz_string = b"EST5EDT,0/0,J365/25";
        assert!(TransitionRule::from_tz_string(tz_string, false).is_err());

        assert_eq!(
            TransitionRule::from_tz_string(tz_string, true)?,
            AlternateTime::new(
                LocalTimeType::new(-18000, false, Some(b"EST"))?,
                LocalTimeType::new(-14400, true, Some(b"EDT"))?,
                RuleDay::julian_0(0)?,
                0,
                RuleDay::julian_1(365)?,
                90000,
            )?
            .into()
        );
        Ok(())
    }

    #[test]
    fn test_v3_file() -> Result<(), Error> {
        let bytes = b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x04\0\0\x1c\x20\0\0IST\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x01\0\0\0\0\0\0\0\x01\0\0\0\x01\0\0\0\x04\0\0\0\0\x7f\xe8\x17\x80\0\0\0\x1c\x20\0\0IST\0\x01\x01\x0aIST-2IDT,M3.4.4/26,M10.5.0\x0a";

        let time_zone = TimeZone::from_tz_data(bytes)?;

        let time_zone_result = TimeZone::new(
            vec![Transition::new(2145916800, 0)],
            vec![LocalTimeType::new(7200, false, Some(b"IST"))?],
            Vec::new(),
            Some(TransitionRule::from(AlternateTime::new(
                LocalTimeType::new(7200, false, Some(b"IST"))?,
                LocalTimeType::new(10800, true, Some(b"IDT"))?,
                RuleDay::month_weekday(3, 4, 4)?,
                93600,
                RuleDay::month_weekday(10, 5, 0)?,
                7200,
            )?)),
        )?;

        assert_eq!(time_zone, time_zone_result);

        Ok(())
    }

    #[test]
    fn test_rule_day() -> Result<(), Error> {
        let rule_day_j1 = RuleDay::julian_1(60)?;
        assert_eq!(rule_day_j1.transition_date(2000), (3, 1));
        assert_eq!(rule_day_j1.transition_date(2001), (3, 1));
        assert_eq!(rule_day_j1.unix_time(2000, 43200), 951912000);

        let rule_day_j0 = RuleDay::julian_0(59)?;
        assert_eq!(rule_day_j0.transition_date(2000), (2, 29));
        assert_eq!(rule_day_j0.transition_date(2001), (3, 1));
        assert_eq!(rule_day_j0.unix_time(2000, 43200), 951825600);

        let rule_day_mwd = RuleDay::month_weekday(2, 5, 2)?;
        assert_eq!(rule_day_mwd.transition_date(2000), (2, 29));
        assert_eq!(rule_day_mwd.transition_date(2001), (2, 27));
        assert_eq!(rule_day_mwd.unix_time(2000, 43200), 951825600);
        assert_eq!(rule_day_mwd.unix_time(2001, 43200), 983275200);

        Ok(())
    }

    #[test]
    fn test_transition_rule() -> Result<(), Error> {
        let transition_rule_fixed = TransitionRule::from(LocalTimeType::new(-36000, false, None)?);
        assert_eq!(transition_rule_fixed.find_local_time_type(0)?.offset(), -36000);

        let transition_rule_dst = TransitionRule::from(AlternateTime::new(
            LocalTimeType::new(43200, false, Some(b"NZST"))?,
            LocalTimeType::new(46800, true, Some(b"NZDT"))?,
            RuleDay::month_weekday(10, 1, 0)?,
            7200,
            RuleDay::month_weekday(3, 3, 0)?,
            7200,
        )?);

        assert_eq!(transition_rule_dst.find_local_time_type(953384399)?.offset(), 46800);
        assert_eq!(transition_rule_dst.find_local_time_type(953384400)?.offset(), 43200);
        assert_eq!(transition_rule_dst.find_local_time_type(970322399)?.offset(), 43200);
        assert_eq!(transition_rule_dst.find_local_time_type(970322400)?.offset(), 46800);

        let transition_rule_negative_dst = TransitionRule::from(AlternateTime::new(
            LocalTimeType::new(3600, false, Some(b"IST"))?,
            LocalTimeType::new(0, true, Some(b"GMT"))?,
            RuleDay::month_weekday(10, 5, 0)?,
            7200,
            RuleDay::month_weekday(3, 5, 0)?,
            3600,
        )?);

        assert_eq!(transition_rule_negative_dst.find_local_time_type(954032399)?.offset(), 0);
        assert_eq!(transition_rule_negative_dst.find_local_time_type(954032400)?.offset(), 3600);
        assert_eq!(transition_rule_negative_dst.find_local_time_type(972781199)?.offset(), 3600);
        assert_eq!(transition_rule_negative_dst.find_local_time_type(972781200)?.offset(), 0);

        let transition_rule_negative_time_1 = TransitionRule::from(AlternateTime::new(
            LocalTimeType::new(0, false, None)?,
            LocalTimeType::new(0, true, None)?,
            RuleDay::julian_0(100)?,
            0,
            RuleDay::julian_0(101)?,
            -86500,
        )?);

        assert!(transition_rule_negative_time_1.find_local_time_type(8639899)?.is_dst());
        assert!(!transition_rule_negative_time_1.find_local_time_type(8639900)?.is_dst());
        assert!(!transition_rule_negative_time_1.find_local_time_type(8639999)?.is_dst());
        assert!(transition_rule_negative_time_1.find_local_time_type(8640000)?.is_dst());

        let transition_rule_negative_time_2 = TransitionRule::from(AlternateTime::new(
            LocalTimeType::new(-10800, false, Some(b"-03"))?,
            LocalTimeType::new(-7200, true, Some(b"-02"))?,
            RuleDay::month_weekday(3, 5, 0)?,
            -7200,
            RuleDay::month_weekday(10, 5, 0)?,
            -3600,
        )?);

        assert_eq!(
            transition_rule_negative_time_2.find_local_time_type(954032399)?.offset(),
            -10800
        );
        assert_eq!(
            transition_rule_negative_time_2.find_local_time_type(954032400)?.offset(),
            -7200
        );
        assert_eq!(
            transition_rule_negative_time_2.find_local_time_type(972781199)?.offset(),
            -7200
        );
        assert_eq!(
            transition_rule_negative_time_2.find_local_time_type(972781200)?.offset(),
            -10800
        );

        let transition_rule_all_year_dst = TransitionRule::from(AlternateTime::new(
            LocalTimeType::new(-18000, false, Some(b"EST"))?,
            LocalTimeType::new(-14400, true, Some(b"EDT"))?,
            RuleDay::julian_0(0)?,
            0,
            RuleDay::julian_1(365)?,
            90000,
        )?);

        assert_eq!(transition_rule_all_year_dst.find_local_time_type(946702799)?.offset(), -14400);
        assert_eq!(transition_rule_all_year_dst.find_local_time_type(946702800)?.offset(), -14400);

        Ok(())
    }

    #[test]
    fn test_transition_rule_overflow() -> Result<(), Error> {
        let transition_rule_1 = TransitionRule::from(AlternateTime::new(
            LocalTimeType::new(-1, false, None)?,
            LocalTimeType::new(-1, true, None)?,
            RuleDay::julian_1(365)?,
            0,
            RuleDay::julian_1(1)?,
            0,
        )?);

        let transition_rule_2 = TransitionRule::from(AlternateTime::new(
            LocalTimeType::new(1, false, None)?,
            LocalTimeType::new(1, true, None)?,
            RuleDay::julian_1(365)?,
            0,
            RuleDay::julian_1(1)?,
            0,
        )?);

        let min_unix_time = -67768100567971200;
        let max_unix_time = 67767976233532799;

        assert!(matches!(
            transition_rule_1.find_local_time_type(min_unix_time),
            Err(Error::OutOfRange(_))
        ));
        assert!(matches!(
            transition_rule_2.find_local_time_type(max_unix_time),
            Err(Error::OutOfRange(_))
        ));

        Ok(())
    }
}
相关推荐
DongLi011 天前
rustlings 学习笔记 -- exercises/05_vecs
rust
番茄灭世神2 天前
Rust学习笔记第2篇
rust·编程语言
shimly1234562 天前
(done) 速通 rustlings(20) 错误处理1 --- 不涉及Traits
rust
shimly1234562 天前
(done) 速通 rustlings(19) Option
rust
@atweiwei2 天前
rust所有权机制详解
开发语言·数据结构·后端·rust·内存·所有权
shimly1234562 天前
(done) 速通 rustlings(24) 错误处理2 --- 涉及Traits
rust
shimly1234562 天前
(done) 速通 rustlings(23) 特性 Traits
rust
shimly1234562 天前
(done) 速通 rustlings(17) 哈希表
rust
shimly1234563 天前
(done) 速通 rustlings(15) 字符串
rust
shimly1234563 天前
(done) 速通 rustlings(22) 泛型
rust