tzif-codec 0.1.2

Codec, validator, and builder for RFC 9636 TZif files
Documentation
use crate::{DataBlock, TzifError, Version};

pub fn validate_leap_seconds(block: &DataBlock, version: Version) -> Result<(), TzifError> {
    let Some(first) = block.leap_seconds.first() else {
        return Ok(());
    };
    if first.occurrence < 0 {
        return Err(TzifError::FirstLeapSecondOccurrenceNegative {
            value: first.occurrence,
        });
    }
    for (index, pair) in block.leap_seconds.windows(2).enumerate() {
        let [previous, next] = pair else {
            continue;
        };
        if previous.occurrence >= next.occurrence {
            return Err(TzifError::LeapSecondOccurrencesNotAscending { index: index + 1 });
        }
    }

    let truncated_at_start = first.correction != 1 && first.correction != -1;
    if truncated_at_start && version != Version::V4 {
        return Err(TzifError::LeapSecondTruncationRequiresVersion4 { version });
    }

    for (index, pair) in block.leap_seconds.windows(2).enumerate() {
        let index = index + 1;
        let [previous, next] = pair else {
            continue;
        };
        let correction_delta = i64::from(next.correction) - i64::from(previous.correction);
        if correction_delta == 1 || correction_delta == -1 {
            continue;
        }
        let is_expiration = version == Version::V4
            && index == block.leap_seconds.len() - 1
            && next.correction == previous.correction;
        if is_expiration {
            continue;
        }
        if next.correction == previous.correction {
            return Err(TzifError::LeapSecondExpirationRequiresVersion4 { version });
        }
        return Err(TzifError::InvalidLeapSecondCorrection { index });
    }

    validate_leap_second_month_boundaries(block)?;

    if truncated_at_start && version == Version::V4 {
        return Ok(());
    }
    if first.correction != 1 && first.correction != -1 {
        return Err(TzifError::InvalidFirstLeapSecondCorrection {
            correction: first.correction,
        });
    }
    Ok(())
}

fn validate_leap_second_month_boundaries(block: &DataBlock) -> Result<(), TzifError> {
    for (index, leap_second) in block.leap_seconds.iter().enumerate() {
        let previous = index
            .checked_sub(1)
            .and_then(|previous| block.leap_seconds.get(previous));
        let is_expiration = index > 0
            && index == block.leap_seconds.len() - 1
            && previous.is_some_and(|previous| leap_second.correction == previous.correction);
        if is_expiration {
            continue;
        }
        let previous_correction = if index == 0 {
            if leap_second.correction > 0 {
                leap_second.correction - 1
            } else {
                leap_second.correction + 1
            }
        } else {
            previous
                .ok_or(TzifError::LeapSecondOccurrenceNotAtMonthEnd { index })?
                .correction
        };
        let unix_after_leap = leap_second
            .occurrence
            .checked_sub(i64::from(previous_correction))
            .ok_or(TzifError::LeapSecondOccurrenceNotAtMonthEnd { index })?;
        if !is_utc_month_boundary(unix_after_leap) {
            return Err(TzifError::LeapSecondOccurrenceNotAtMonthEnd { index });
        }
    }
    Ok(())
}

const fn is_utc_month_boundary(unix_seconds: i64) -> bool {
    const SECONDS_PER_DAY: i64 = 86_400;
    let days = unix_seconds.div_euclid(SECONDS_PER_DAY);
    let seconds_of_day = unix_seconds.rem_euclid(SECONDS_PER_DAY);
    if seconds_of_day != 0 {
        return false;
    }
    let (_, _, day) = civil_from_days(days);
    day == 1
}

const fn civil_from_days(days_since_epoch: i64) -> (i64, i64, i64) {
    let z = days_since_epoch + 719_468;
    let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
    let doe = z - era * 146_097;
    let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
    let mut year = yoe + era * 400;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    let mp = (5 * doy + 2) / 153;
    let day = doy - (153 * mp + 2) / 5 + 1;
    let month = mp + if mp < 10 { 3 } else { -9 };
    year += (month <= 2) as i64;
    (year, month, day)
}