-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
mktime
in a non-UTC zone?
#26
Comments
The hard part is not the normalization of In fact, even glibc is not coherent with itself with the following code: Code#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
void print_time(int tm_hour, int tm_min)
{
struct tm t = {
.tm_sec = 0,
.tm_min = tm_min,
.tm_hour = tm_hour,
.tm_mday = 31,
.tm_mon = 9,
.tm_year = 121,
.tm_isdst = -1,
};
time_t unix_time = mktime(&t);
char *s = asctime(&t);
s[strlen(s) - 1] = '\0';
printf("asctime: %s, mktime: %li, tm_gmtoff: %li\n", s, unix_time, t.tm_gmtoff);
}
int main(int argc, char *argv[])
{
setenv("TZ", "Europe/Paris", 1);
print_time(1, 30);
print_time(2, 0);
print_time(2, 30);
print_time(3, 0);
print_time(3, 30);
print_time(3, 0);
print_time(2, 30);
print_time(2, 0);
print_time(1, 30);
return 0;
} In Linux, it prints the following output (Godbolt): Output
You can see that we have different results of Related Chrono issue: chronotope/chrono#668. I am current working on the next version of the library, where I will add the following functions: Codeimpl DateTime {
/// Construct a date time
///
/// ## Inputs
///
/// * `year`: Year
/// * `month`: Month in `[1, 12]`
/// * `month_day`: Day of the month in `[1, 31]`
/// * `hour`: Hours since midnight in `[0, 23]`
/// * `minute`: Minutes in `[0, 59]`
/// * `second`: Seconds in `[0, 60]`, with a possible leap second
/// * `nanoseconds`: Nanoseconds in `[0, 999_999_999]`
/// * `local_time_type`: Local time type associated to a time zone
///
pub fn new(
year: i32,
month: u8,
month_day: u8,
hour: u8,
minute: u8,
second: u8,
nanoseconds: u32,
local_time_type: LocalTimeType,
) -> Result<Self, TzError> {
todo!()
}
/// Find the date time correponding to the inputs
///
/// ## Inputs
///
/// * `year`: Year
/// * `month`: Month in `[1, 12]`
/// * `month_day`: Day of the month in `[1, 31]`
/// * `hour`: Hours since midnight in `[0, 23]`
/// * `minute`: Minutes in `[0, 59]`
/// * `second`: Seconds in `[0, 60]`, with a possible leap second
/// * `nanoseconds`: Nanoseconds in `[0, 999_999_999]`
/// * `time_zone_ref`: Reference to a time zone
///
pub fn find(
year: i32,
month: u8,
month_day: u8,
hour: u8,
minute: u8,
second: u8,
nanoseconds: u32,
time_zone_ref: TimeZoneRef,
) -> Result<FoundDateTimeKind, TzError> {
todo!()
}
}
/// Output of [`DateTime::find`] function
pub enum FoundDateTimeKind {
/// A unique valid and unambiguous date time was found
Unique(DateTime),
/// Found date time is invalid or ambiguous (at least one transition includes the date time)
InvalidOrAmbiguous(Vec<OnTransition>),
}
/// Type of transitions including the date time
pub enum OnTransition {
/// Date time is invalid because it was skipped by a forward transition.
///
/// This variant gives the two [`DateTime`] corresponding to the transition instant, just before and just after the transition.
///
/// This is different from the `mktime` behavior, which allows invalid datetimes when no DST information is available (by specifying `tm_isdst = -1`).
///
ForwardTransition {
/// Date time just before the forward transition
before_transition: DateTime,
/// Date time just after the forward transition
after_transition: DateTime,
},
/// Date time is ambiguous because it was repeated after a backward transition
BackwardTransition {
/// Earlier date time before the backward transition
earlier: DateTime,
/// Later date time after the backward transition
later: DateTime,
},
} Then you can use it like this for your use case: Codefn bounds(year: i32, month: u8, month_day: u8, local_time_zone_ref: TimeZoneRef) -> Result<Range<i64>, TzError> {
const SECONDS_PER_DAY: i64 = 86400;
let naive_day_start = UtcDateTime::new(year, month, month_day, 0, 0, 0, 0)?;
let naive_day_end = UtcDateTime::from_timespec(naive_day_start.unix_time() + SECONDS_PER_DAY, 0)?;
let day_start = DateTime::find(naive_day_start.year(), naive_day_start.month(), naive_day_start.month_day(), 0, 0, 0, 0, local_time_zone_ref)?;
let day_end = DateTime::find(naive_day_end.year(), naive_day_end.month(), naive_day_end.month_day(), 0, 0, 0, 0, local_time_zone_ref)?;
// process day_start and day_end
let chosen_day_start: DateTime = todo!();
let chosen_day_end: DateTime = todo!();
Ok(chosen_day_start.unix_time()..chosen_day_end.unix_time())
}
fn compute_unix_time(
year: i32,
month: u8,
month_day: u8,
hour: u8,
minute: u8,
second: u8,
ut_offset: Option<i32>,
local_time_zone_ref: TimeZoneRef,
) -> Result<i64, TzError> {
match ut_offset {
Some(ut_offset) => Ok(DateTime::new(year, month, month_day, hour, minute, second, 0, LocalTimeType::with_ut_offset(ut_offset)?)?.unix_time()),
None => {
let result_kind = DateTime::find(year, month, month_day, hour, minute, second, 0, local_time_zone_ref)?;
// process result_kind
let chosen_date_time: DateTime = todo!();
Ok(chosen_date_time.unix_time())
}
}
} |
Yuck! That's a bug in my present code then. The call sites I pointed out don't particularly care how the ambiguity is resolved but they need it to be deterministic for a given input, or my in-memory index can effectively be corrupted. More reason to ditch the libc API.
Awesome! I'm excited to see you're already working on this. Question: should it be considered valid for (and is there any actual case in the current IANA database where) transition periods overlap, so there are more than two possible offsets for a given ISO-8601 calendar time? It seems crazy to me. I just checked a couple APIs that I believe are considered among the best, and they don't seem to support this:
so they must reject zone definitions with overlapping transitions or at least not ever return all possible offsets if this happens. It'd be super convenient if let day_start = DateTime::find(...)?.earlier();
// or maybe even defer the `Result` conversion until after the choice,
// so there's no redundant error path for `reject`:
let day_start = DateTime::find(...).earlier()?;
let day_start = DateTime::find(...).reject()?; |
Exemple of possible overlapping transitions:
The TZif file format is defined in the RFC 8536, which doesn't disallow overlapping transitions, so the case is definitely possible. However I don't know if there is such a case in the current IANA database. I will add the following modifications to my code: Code/// Output of [`DateTime::find`] function
pub enum FoundDateTimeKind {
/// A unique valid and unambiguous date time was found
Unique(DateTime),
/// Found date time is invalid or ambiguous (at least one transition includes the date time)
InvalidOrAmbiguous {
/// First transition
first_transition: OnTransition,
/// Additional transitions
additional_transitions: Vec<OnTransition>,
},
}
impl FoundDateTimeKind {
pub fn unique(&self) -> Option<DateTime> {
match *self {
Self::Unique(date_time) => Some(date_time),
_ => None,
}
}
pub fn earlier(&self) -> DateTime {
match self {
&Self::Unique(date_time) => date_time,
Self::InvalidOrAmbiguous { first_transition, additional_transitions } => {
let first_earlier = match first_transition {
OnTransition::ForwardTransition { before_transition, .. } => before_transition,
OnTransition::BackwardTransition { earlier, .. } => earlier,
};
*additional_transitions.iter().fold(first_earlier, |current_earlier, transition| match transition {
OnTransition::ForwardTransition { before_transition, .. } if before_transition < current_earlier => before_transition,
OnTransition::BackwardTransition { earlier, .. } if earlier < current_earlier => earlier,
_ => current_earlier,
})
}
}
}
pub fn later(&self) -> DateTime {
match self {
&Self::Unique(date_time) => date_time,
Self::InvalidOrAmbiguous { first_transition, additional_transitions } => {
let first_later = match first_transition {
OnTransition::ForwardTransition { after_transition, .. } => after_transition,
OnTransition::BackwardTransition { later, .. } => later,
};
*additional_transitions.iter().fold(first_later, |current_later, transition| match transition {
OnTransition::ForwardTransition { after_transition, .. } if after_transition > current_later => after_transition,
OnTransition::BackwardTransition { later, .. } if later > current_later => later,
_ => current_later,
})
}
}
}
} There won't be a Javascript-compatible method since it is not well-defined when several transitions are overlapping. |
Implemented in 98bbeac with some API simplifications compared to above. |
First, thank you for writing this crate. I hate libc's time API for reasons beyond the soundness problem, so it's great to have pure Rust implementations of time zones.
The crate's blurb says this:
but I can't figure out how to replace
mktime
when operating in a non-UTC time zone. I want to go from a calendar representation (YYYYmmddHTT:MM:SS
) in a political time zone likeAmerica/Los_Angeles
to seconds since epoch.[editing to add: also in particular, my code here relies on
mktime
accepting a "non-normalized" time struct, as described in the man page:to find the boundary of the next day in both seconds since epoch and in proper calendar terms. I'm not sure if you consider that in-scope for
tz-rs
or day math is better handled by some other more full-featured datetime crate built on top of it.]I see how to create a
UtcDateTime
from this kind of spec (UtcDateTime::new
is mentioned in the crate-level doc even) but not aDateTime
.Is this possible today? If not, could it be? Ideally with a way of resolving ambiguity like the Javascript TC39
Temporal
API offers.For context, I'm trying to replace Moonfire NVR's
time
0.1-based code here, here, and here.The text was updated successfully, but these errors were encountered: