tzif-codec 0.1.5

Codec, validator, and builder for RFC 9636 TZif files
Documentation
use crate::common::{AssertErr, AssertOk, TestResult};
use tzif_codec::{PosixFooter, TzifBuildError, TzifBuilder, Version, VersionPolicy};

#[test]
fn explicit_transition_builder_generates_fixed_posix_footer() -> TestResult {
    let tzif = TzifBuilder::transitions()
        .local_time_type("NPT", 5 * 3600 + 45 * 60, false)
        .posix_footer(PosixFooter::fixed("NPT", 5 * 3600 + 45 * 60))
        .build()
        .assert_ok()?;

    assert_eq!(tzif.version, Version::V2);
    assert_eq!(tzif.footer.as_deref(), Some("NPT-5:45"));
    tzif.to_bytes().assert_ok()?;

    Ok(())
}

#[test]
fn explicit_transition_builder_hides_designation_indexes() -> TestResult {
    let tzif = TzifBuilder::transitions()
        .designation("EST")
        .designation("EDT")
        .local_time_type("EST", -5 * 3600, false)
        .local_time_type("EDT", -4 * 3600, true)
        .transition(1_710_054_000, "EDT")
        .transition(1_730_613_600, "EST")
        .footer("EST5EDT,M3.2.0,M11.1.0")
        .build()
        .assert_ok()?;

    assert_eq!(tzif.version, Version::V2);
    assert_eq!(tzif.footer.as_deref(), Some("EST5EDT,M3.2.0,M11.1.0"));
    let block = tzif.v2_plus.as_ref().assert_ok()?;
    assert_eq!(block.designations, b"EST\0EDT\0");
    assert_eq!(block.local_time_types[0].designation_index, 0);
    assert_eq!(block.local_time_types[1].designation_index, 4);
    assert_eq!(block.transition_types, vec![1, 0]);
    tzif.to_bytes().assert_ok()?;

    Ok(())
}

#[test]
fn explicit_transition_builder_rejects_unknown_and_unsorted_transitions() -> TestResult {
    assert!(matches!(
        TzifBuilder::transitions()
            .local_time_type("EST", -5 * 3600, false)
            .transition(0, "EDT")
            .build()
            .assert_err()?,
        TzifBuildError::UnknownDesignation(designation) if designation == "EDT"
    ));

    assert!(matches!(
        TzifBuilder::transitions()
            .local_time_type("EST", -5 * 3600, false)
            .transition(10, "EST")
            .transition(10, "EST")
            .build()
            .assert_err()?,
        TzifBuildError::UnsortedTransitions
    ));

    assert!(matches!(
        TzifBuilder::transitions()
            .local_time_type("EST", -5 * 3600, false)
            .transition(10, "EST")
            .transition(9, "EST")
            .build()
            .assert_err()?,
        TzifBuildError::UnsortedTransitions
    ));

    Ok(())
}

#[test]
fn explicit_transition_builder_rejects_duplicate_and_missing_local_time_types() -> TestResult {
    assert!(matches!(
        TzifBuilder::transitions()
            .local_time_type("UTC", i32::MIN, false)
            .build()
            .assert_err()?,
        TzifBuildError::InvalidUtcOffset
    ));

    assert!(matches!(
        TzifBuilder::transitions()
            .designation("UTC")
            .designation("UTC")
            .local_time_type("UTC", 0, false)
            .build()
            .assert_err()?,
        TzifBuildError::DuplicateDesignation(designation) if designation == "UTC"
    ));

    assert!(matches!(
        TzifBuilder::transitions()
            .local_time_type("UTC", 0, false)
            .local_time_type("UTC", 3600, true)
            .build()
            .assert_err()?,
        TzifBuildError::DuplicateDesignation(designation) if designation == "UTC"
    ));

    assert!(matches!(
        TzifBuilder::transitions().build().assert_err()?,
        TzifBuildError::UnknownDesignation(message) if message == "local time type"
    ));

    Ok(())
}

#[test]
fn explicit_transition_builder_rejects_v1_when_footer_is_present() -> TestResult {
    let raw_err = TzifBuilder::transitions()
        .local_time_type("UTC", 0, false)
        .footer("UTC0")
        .version(Version::V1)
        .build()
        .assert_err()?;
    assert!(matches!(
        raw_err,
        TzifBuildError::VersionCannotIncludeFooter {
            version: Version::V1
        }
    ));

    let posix_err = TzifBuilder::transitions()
        .local_time_type("UTC", 0, false)
        .posix_footer(PosixFooter::fixed("UTC", 0))
        .version(Version::V1)
        .build()
        .assert_err()?;
    assert!(matches!(
        posix_err,
        TzifBuildError::VersionCannotIncludeFooter {
            version: Version::V1
        }
    ));

    Ok(())
}

#[test]
fn explicit_transition_builder_auto_selects_v2_even_when_v1_could_represent_the_data() -> TestResult
{
    let tzif = TzifBuilder::transitions()
        .local_time_type("UTC", 0, false)
        .transition(0, "UTC")
        .build()
        .assert_ok()?;

    assert_eq!(tzif.version, Version::V2);
    assert_eq!(tzif.footer.as_deref(), Some(""));
    tzif.to_bytes().assert_ok()?;

    Ok(())
}

#[test]
fn explicit_transition_builder_supports_explicit_legacy_v1_override() -> TestResult {
    let tzif = TzifBuilder::transitions()
        .local_time_type("UTC", 0, false)
        .transition(0, "UTC")
        .version(Version::V1)
        .build()
        .assert_ok()?;

    assert_eq!(tzif.version, Version::V1);
    assert!(tzif.footer.is_none());
    assert!(tzif.v2_plus.is_none());
    tzif.to_bytes().assert_ok()?;

    Ok(())
}

#[test]
fn explicit_transition_builder_auto_selects_v2_for_64_bit_transitions() -> TestResult {
    let timestamp = i64::from(i32::MAX) + 1;
    let tzif = TzifBuilder::transitions()
        .local_time_type("UTC", 0, false)
        .transition(timestamp, "UTC")
        .build()
        .assert_ok()?;

    assert_eq!(tzif.version, Version::V2);
    assert_eq!(tzif.v1.transition_times, Vec::<i64>::new());
    assert_eq!(
        tzif.v2_plus.as_ref().assert_ok()?.transition_times,
        vec![timestamp]
    );
    tzif.to_bytes().assert_ok()?;

    Ok(())
}

#[test]
fn explicit_transition_builder_supports_version_override() -> TestResult {
    let tzif = TzifBuilder::transitions()
        .local_time_type("UTC", 0, false)
        .transition(0, "UTC")
        .version_policy(VersionPolicy::Exact(Version::V2))
        .build()
        .assert_ok()?;

    assert_eq!(tzif.version, Version::V2);
    assert_eq!(tzif.footer.as_deref(), Some(""));
    tzif.to_bytes().assert_ok()?;

    let err = TzifBuilder::transitions()
        .local_time_type("UTC", 0, false)
        .transition(i64::from(i32::MAX) + 1, "UTC")
        .version(Version::V1)
        .build()
        .assert_err()?;
    assert!(matches!(
        err,
        TzifBuildError::TransitionOutOfRangeForVersion {
            version: Version::V1,
            ..
        }
    ));

    Ok(())
}