From 329f699e51993e9b2ecc76a9eb4a82cdf3400432 Mon Sep 17 00:00:00 2001 From: aj Date: Sun, 7 Nov 2010 12:52:51 -0800 Subject: [PATCH] alerted to buginess in README. added notes & zic reference. removed local_to_utc conversion for now. Time comparisons are patently wrong. They do not consider s/w/u/g/z time modifiers, so the comparisons can be off by hours. At least 24 hours within any time or zone change, ezic will give unreliable results. I think a solution can involve "flattening" the data, starting from the earliest time possible for each zone/rule, going forward. There exist ambiguous local times for zones and rules. For example, on Nov 7th, in Los Angeles, the times from 1:00:00am to 1:59:59am were repeated, once for PDT and once for PST. Given a local time in that zone between those times, we cannot determine what UTC time is unambiguously. Similar for Zone transitions where fall-back occurs. --- README.md | 20 +-- notes/local_to_utc.svg | 204 +++++++++++++++++++++++++++ notes/zic.8.txt | 313 +++++++++++++++++++++++++++++++++++++++++ src/ezic.erl | 46 +++--- src/ezic_date.erl | 15 +- src/ezic_rule.erl | 14 +- src/ezic_zone.erl | 48 +++++-- 7 files changed, 609 insertions(+), 51 deletions(-) create mode 100644 notes/local_to_utc.svg create mode 100644 notes/zic.8.txt diff --git a/README.md b/README.md index f6f2ef4..93eac70 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,18 @@ ezic is a set of erlang utilities for the Olsen timezone database files. See the +Status +------ + +2010.11.07 + + * The time comparisons don't consider time relativity, so they're plain wrong. Within at least 24 hours of timechange or zone change, the results cannot be trusted. + +2010.11.04 + + * ezic is "apparently functional". It has been tested very lightly, a bit by unit tests, but mostly by inspection. It's probably not fit for production use. + + License ------- @@ -18,14 +30,6 @@ To maintain ezic's status as a public domain work, all contributions must also b -Status ------- - -2010.11.04 - - * ezic is "apparently functional". It has been tested very lightly, a bit by unit tests, but mostly by inspection. It's probably not fit for production use. - - API --- diff --git a/notes/local_to_utc.svg b/notes/local_to_utc.svg new file mode 100644 index 0000000..68e7aec --- /dev/null +++ b/notes/local_to_utc.svg @@ -0,0 +1,204 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + local_to_utc(LocalDatetime, TimeZoneName) + + + + Los Angeles + + 2:00am Nov 7th, 2010, PDT-7 + + 1:00am Nov 7th, 2010, PST-8 + + + + + On Nov 7th, 2010, 1:00am to 1:59am will happen twice.There is no definitive answer to: "What was UTC time given that in Los Angeles on Nov 7th, 2010, it was 1:30am" + + diff --git a/notes/zic.8.txt b/notes/zic.8.txt new file mode 100644 index 0000000..1feada2 --- /dev/null +++ b/notes/zic.8.txt @@ -0,0 +1,313 @@ +NAME + + zic - time zone compiler + +SYNOPSIS + zic [ --version ] [ -v ] [ -d directory ] [ -l localtime ] [ + -p posixrules ] [ -L leapsecondfilename ] [ -s ] [ -y + command ] [ filename ... ] + +DESCRIPTION + Zic reads text from the file(s) named on the command line + and creates the time conversion information files specified + in this input. If a filename is -, the standard input is + read. + + These options are available: + + --version + Output version information and exit. + + -d directory + Create time conversion information files in the named + directory rather than in the standard directory named + below. + + -l timezone + Use the given time zone as local time. Zic will act as + if the input contained a link line of the form + + Link timezone localtime + + -p timezone + Use the given time zone's rules when handling POSIX- + format time zone environment variables. Zic will act + as if the input contained a link line of the form + + Link timezone posixrules + + -L leapsecondfilename + Read leap second information from the file with the + given name. If this option is not used, no leap second + information appears in output files. + + -v Complain if a year that appears in a data file is + outside the range of years representable by time(2) + values. Also complain if a time of 24:00 (which cannot + be handled by pre-1998 versions of zic) appears in the + input. + + -s Limit time values stored in output files to values that + are the same whether they're taken to be signed or + unsigned. You can use this option to generate SVVS- + compatible files. + + -y command + Use the given command rather than yearistype when + checking year types (see below). + + Input lines are made up of fields. Fields are separated + from one another by any number of white space characters. + Leading and trailing white space on input lines is ignored. + An unquoted sharp character (#) in the input introduces a + comment which extends to the end of the line the sharp + character appears on. White space characters and sharp + characters may be enclosed in double quotes (") if they're + to be used as part of a field. Any line that is blank + (after comment stripping) is ignored. Non-blank lines are + expected to be of one of three types: rule lines, zone + lines, and link lines. + + Names (such as month names) must be in English and are case + insensitive. Abbreviations, if used, must be unambiguous in + context. + + A rule line has the form + + Rule NAME FROM TO TYPE IN ON AT SAVE LETTER/S + + For example: + + Rule US 1967 1973 - Apr lastSun 2:00 1:00 D + + The fields that make up a rule line are: + + NAME Gives the (arbitrary) name of the set of rules this + rule is part of. + + FROM Gives the first year in which the rule applies. Any + integer year can be supplied; the Gregorian calendar + is assumed. The word minimum (or an abbreviation) + means the minimum year representable as an integer. + The word maximum (or an abbreviation) means the + maximum year representable as an integer. Rules can + describe times that are not representable as time + values, with the unrepresentable times ignored; this + allows rules to be portable among hosts with + differing time value types. + + TO Gives the final year in which the rule applies. In + addition to minimum and maximum (as above), the word + only (or an abbreviation) may be used to repeat the + value of the FROM field. + + TYPE Gives the type of year in which the rule applies. + If TYPE is - then the rule applies in all years + between FROM and TO inclusive. If TYPE is something + else, then zic executes the command + yearistype year type + to check the type of a year: an exit status of zero + is taken to mean that the year is of the given type; + an exit status of one is taken to mean that the year + is not of the given type. + + IN Names the month in which the rule takes effect. + Month names may be abbreviated. + + ON Gives the day on which the rule takes effect. + Recognized forms include: + + 5 the fifth of the month + lastSun the last Sunday in the month + lastMon the last Monday in the month + Sun>=8 first Sunday on or after the eighth + Sun<=25 last Sunday on or before the 25th + + Names of days of the week may be abbreviated or + spelled out in full. Note that there must be no + spaces within the ON field. + + AT Gives the time of day at which the rule takes + effect. Recognized forms include: + + 2 time in hours + 2:00 time in hours and minutes + 15:00 24-hour format time (for times after noon) + 1:28:14 time in hours, minutes, and seconds + - equivalent to 0 + + where hour 0 is midnight at the start of the day, + and hour 24 is midnight at the end of the day. Any + of these forms may be followed by the letter w if + the given time is local "wall clock" time, s if the + given time is local "standard" time, or u (or g or + z) if the given time is universal time; in the + absence of an indicator, wall clock time is assumed. + + SAVE Gives the amount of time to be added to local + standard time when the rule is in effect. This + field has the same format as the AT field (although, + of course, the w and s suffixes are not used). + + LETTER/S + Gives the "variable part" (for example, the "S" or + "D" in "EST" or "EDT") of time zone abbreviations to + be used when this rule is in effect. If this field + is -, the variable part is null. + + A zone line has the form + + Zone NAME GMTOFF RULES/SAVE FORMAT [UNTILYEAR [MONTH [DAY [TIME]]]] + + For example: + + Zone Australia/Adelaide 9:30 Aus CST 1971 Oct 31 2:00 + + The fields that make up a zone line are: + + NAME The name of the time zone. This is the name used in + creating the time conversion information file for the + zone. + + GMTOFF + The amount of time to add to UTC to get standard time + in this zone. This field has the same format as the + AT and SAVE fields of rule lines; begin the field with + a minus sign if time must be subtracted from UTC. + + RULES/SAVE + The name of the rule(s) that apply in the time zone + or, alternately, an amount of time to add to local + standard time. If this field is - then standard time + always applies in the time zone. + + FORMAT + The format for time zone abbreviations in this time + zone. The pair of characters %s is used to show where + the "variable part" of the time zone abbreviation + goes. Alternately, a slash (/) separates standard and + daylight abbreviations. + + UNTILYEAR [MONTH [DAY [TIME]]] + The time at which the UTC offset or the rule(s) change + for a location. It is specified as a year, a month, a + day, and a time of day. If this is specified, the + time zone information is generated from the given UTC + offset and rule change until the time specified. The + month, day, and time of day have the same format as + the IN, ON, and AT fields of a rule; trailing fields + can be omitted, and default to the earliest possible + value for the missing fields. + + The next line must be a "continuation" line; this has + the same form as a zone line except that the string + "Zone" and the name are omitted, as the continuation + line will place information starting at the time + specified as the "until" information in the previous + line in the file used by the previous line. + Continuation lines may contain "until" information, + just as zone lines do, indicating that the next line + is a further continuation. + + A link line has the form + + Link LINK-FROM LINK-TO + + For example: + + Link Europe/Istanbul Asia/Istanbul + + The LINK-FROM field should appear as the NAME field in some + zone line; the LINK-TO field is used as an alternate name + for that zone. + + Except for continuation lines, lines may appear in any order + in the input. + + Lines in the file that describes leap seconds have the + following form: + + Leap YEAR MONTH DAY HH:MM:SS CORR R/S + + For example: + + Leap 1974 Dec 31 23:59:60 + S + + The YEAR, MONTH, DAY, and HH:MM:SS fields tell when the leap + second happened. The CORR field should be "+" if a second + was added or "-" if a second was skipped. The R/S field + should be (an abbreviation of) "Stationary" if the leap + second time given by the other fields should be interpreted + as UTC or (an abbreviation of) "Rolling" if the leap second + time given by the other fields should be interpreted as + local wall clock time. + +EXTENDED EXAMPLE + Here is an extended example of zic input, intended to + illustrate many of its features. + + # Rule NAME FROM TO TYPE IN ON AT SAVE LETTER/S + Rule Swiss 1940 only - Nov 2 0:00 1:00 S + Rule Swiss 1940 only - Dec 31 0:00 0 - + Rule Swiss 1941 1942 - May Sun>=1 2:00 1:00 S + Rule Swiss 1941 1942 - Oct Sun>=1 0:00 0 + Rule EU 1977 1980 - Apr Sun>=1 1:00u 1:00 S + Rule EU 1977 only - Sep lastSun 1:00u 0 - + Rule EU 1978 only - Oct 1 1:00u 0 - + Rule EU 1979 1995 - Sep lastSun 1:00u 0 - + Rule EU 1981 max - Mar lastSun 1:00u 1:00 S + Rule EU 1996 max - Oct lastSun 1:00u 0 - + + # Zone NAME GMTOFF RULES FORMAT UNTIL + Zone Europe/Zurich 0:34:08 - LMT 1848 Sep 12 + 0:29:44 - BMT 1894 Jun + 1:00 Swiss CE%sT 1981 + 1:00 EU CE%sT + + Link Europe/Zurich Switzerland + + In this example, the zone is named Europe/Zurich but it has + an alias as Switzerland. Zurich was 34 minutes and 8 + seconds west of GMT until 1848-09-12 at 00:00, when the + offset changed to 29 minutes and 44 seconds. After + 1894-06-01 at 00:00 Swiss daylight saving rules (defined + with lines beginning with "Rule Swiss") apply, and the GMT + offset became one hour. From 1981 to the present, EU + daylight saving rules have applied, and the UTC offset has + remained at one hour. + + In 1940, daylight saving time applied from November 2 at + 00:00 to December 31 at 00:00. In 1941 and 1942, daylight + saving time applied from the first Sunday in May at 02:00 to + the first Sunday in October at 00:00. The pre-1981 EU + daylight-saving rules have no effect here, but are included + for completeness. Since 1981, daylight saving has begun on + the last Sunday in March at 01:00 UTC. Until 1995 it ended + the last Sunday in September at 01:00 UTC, but this changed + to the last Sunday in October starting in 1996. + + For purposes of display, "LMT" and "BMT" were initially + used, respectively. Since Swiss rules and later EU rules + were applied, the display name for the timezone has been CET + for standard time and CEST for daylight saving time. + +NOTES + For areas with more than two types of local time, you may + need to use local standard time in the AT field of the + earliest transition time's rule to ensure that the earliest + transition time recorded in the compiled file is correct. + + If, for a particular zone, a clock advance caused by the + start of daylight saving coincides with and is equal to a + clock retreat caused by a change in UTC offset, zic produces + a single transition to daylight saving at the new UTC offset + (without any change in wall clock time). To get separate + transitions use multiple zone continuation lines specifying + transition instants using universal time. + +FILE + /usr/local/etc/zoneinfo standard directory used for + created files + +SEE ALSO + newctime(3), tzfile(5), zdump(8) diff --git a/src/ezic.erl b/src/ezic.erl index e5bf040..518127c 100644 --- a/src/ezic.erl +++ b/src/ezic.erl @@ -4,8 +4,8 @@ -export([ localtime/1 , utc_to_local/2 - , utc_from_local/2 - , zone_convert/3 +% , local_to_utc/2 +% , zone_convert/3 , next_timechange/1 , next_timechange/2 ]). @@ -28,28 +28,28 @@ localtime(TzName) -> utc_to_local(erlang:universaltime(), TzName). -utc_to_local(Datetime, TzName) -> - {ok, Zone, Rule}= current(Datetime, TzName), - SecDiff= offset(Zone, Rule), - date_add(Datetime, SecDiff). +utc_to_local(UTCDatetime, TzName) -> + {ok, Zone, Rule}= current_as_of_utc(UTCDatetime, TzName), + SecDiff= time_offset(Zone, Rule), + date_add(UTCDatetime, SecDiff). -utc_from_local(Datetime, TzName) -> - {ok, Zone, Rule}= current(Datetime, TzName), - SecDiff= offset(Zone, Rule), - date_subtract(Datetime, SecDiff). +%% local_to_utc(Datetime, TzName) -> +%% {ok, Zone, Rule}= current_as_of_local(Datetime, TzName), +%% SecDiff= time_offset(Zone, Rule), +%% date_subtract(Datetime, SecDiff). -zone_convert(Datetime, FromTimeZone, ToTimeZone) -> - UTC= utc_from_local(Datetime, FromTimeZone), - utc_to_local(UTC, ToTimeZone). +%% zone_convert(Datetime, FromTimeZone, ToTimeZone) -> +%% UTC= local_to_utc(Datetime, FromTimeZone), +%% utc_to_local(UTC, ToTimeZone). next_timechange(TzName) -> - next_timechange(localtime(TzName), TzName). + next_timechange(erlang:universaltime(), TzName). -next_timechange(Datetime, TzName) -> +next_timechange(UtcDatetime, TzName) -> not_done. @@ -84,23 +84,19 @@ test() -> %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -current(Datetime, TzName) -> - Zones= ezic_db:zones(TzName), -% ?debug("All Zones: ~p", [Zones]), - CZone= ezic_zone:current(Datetime, Zones), -% ?debug("Current Zone: ~p", [CZone]), - +current_as_of_utc(UTCDatetime, TzName) -> + CZone= ezic_zone:current_as_of_utc(UTCDatetime, TzName), RuleName= CZone#zone.rule, Rules= ezic_db:rules(RuleName), -% ?debug("All Rules: ~p", [Rules]), - CRule= ezic_rule:current(Datetime, Rules), -% ?debug("Current Rule: ~p", [CRule]), + CRule= ezic_rule:current_as_of_utc(UTCDatetime, Rules), {ok, CZone, CRule}. +%%current_as_of_local(Datetime, TzName) -> +%% not_done. -offset(Zone, Rule) -> +time_offset(Zone, Rule) -> OffsetSec= ezic_zone:offset_sec(Zone), DSTSec= ezic_rule:dst_sec(Rule), OffsetSec + DSTSec. diff --git a/src/ezic_date.erl b/src/ezic_date.erl index ab6d85c..750c803 100644 --- a/src/ezic_date.erl +++ b/src/ezic_date.erl @@ -16,7 +16,8 @@ -% returns the concrete date tuple for a given rule and year, irrespective of zone +% returns the date tuple for a given rule and year +% same for all timezones. % e.g. -> {Y,M,D} for(#rule{in=M, on={last, D}}, Y) -> last_day_of(D, Y,M); @@ -24,13 +25,15 @@ for(#rule{in=M, on=#tzon{day=Day, filter=Filter}}, Y) -> first_day_limited(Day, Filter, Y,M). - +% returns the date on which the last Day (sun,mon,tue,etc.) occurs in a given month/year +% same for all timezones last_day_of(Day, Y,M) -> LastDay= calendar:last_day_of_the_month(Y,M), previous_day(Day, {Y,M,LastDay}). % returns a date tuple {Y,M,D} representing the first available date, given the filter +% same for all timezones first_day_limited(Day, {geq, N}, Y,M) -> next_day(Day, {Y,M,N}); first_day_limited(Day, {leq, N}, Y,M) -> @@ -38,7 +41,8 @@ first_day_limited(Day, {leq, N}, Y,M) -> - +% Returns the soonest date on which Day (sun/mon/tue/etc.) occurs BEFORE the given date. +% same for all timezones previous_day(Day, Date={Y,M,D}) -> Daynum= day_to_num(Day), LeqDoW= calendar:day_of_the_week(Date), @@ -56,6 +60,8 @@ previous_day(Day, Date={Y,M,D}) -> end. +% Returns the soonest date on which Day (sun/mon/tue/etc.) occurs AFTER the given date. +% same for all timezones next_day(Day, Date={Y,M,D}) -> Daynum= day_to_num(Day), LeqDoW= calendar:day_of_the_week(Date), @@ -115,6 +121,8 @@ compare_datetimes_normal(current, current) -> true; compare_datetimes_normal(current, _) -> false; compare_datetimes_normal(_,current) -> true; +% @bug this is not true if the times are from different time zones +% @todo check types for D1 and D2 compare_datetimes_normal({D1, _}, {D2, _}) when D1 < D2 -> true; compare_datetimes_normal({D1, _}, {D2, _}) when D1 > D2 -> @@ -144,6 +152,7 @@ normalize(only) -> only; normalize(maximum) -> maximum; normalize(minimum) -> minimum; + normalize(Y) when is_integer(Y) -> {{Y,1,1}, #tztime{}}; normalize(Date={_,_,D}) when is_integer(D) -> diff --git a/src/ezic_rule.erl b/src/ezic_rule.erl index 9436326..a9f66eb 100644 --- a/src/ezic_rule.erl +++ b/src/ezic_rule.erl @@ -2,7 +2,7 @@ -include("include/ezic.hrl"). --export([current/2, relevant/2, sort/2]). +-export([current/2, current_set/2, sort/2]). -export([from_time/1, dst_sec/1]). @@ -10,9 +10,9 @@ current(_, []) -> none; current(Now, Rules) -> - RelevantRules= lists:filter(fun(R)-> relevant(Now, R) end, Rules), -% ?debug("RelevantRules: ~p", [RelevantRules]), - SRules= lists:sort(fun sort/2, RelevantRules), + Current_SetRules= lists:filter(fun(R)-> current_set(Now, R) end, Rules), +% ?debug("Current_SetRules: ~p", [Current_SetRules]), + SRules= lists:sort(fun sort/2, Current_SetRules), CRule= choose_rule(SRules), CRule. @@ -27,15 +27,15 @@ sort(R1, R2) -> -relevant(Now, Rule=#rule{from=F, to=T}) -> +current_set(Now, Rule=#rule{from=F, to=T}) -> {{Y,_,_},_} = Now, case ezic_date:date_between(Y, {F,T}) of false -> false; - true -> relevant2(Now, Rule) + true -> current_set2(Now, Rule) end. -relevant2(Now, #rule{in=Month, on=Day, at=Time}) -> +current_set2(Now, #rule{in=Month, on=Day, at=Time}) -> {{Y,_,_},_} = Now, RTime= {{Y, Month, Day}, Time}, ezic_date:compare_datetimes(RTime, Now). diff --git a/src/ezic_zone.erl b/src/ezic_zone.erl index aeb233a..ea07cb7 100644 --- a/src/ezic_zone.erl +++ b/src/ezic_zone.erl @@ -1,16 +1,45 @@ -module(ezic_zone). -include("include/ezic.hrl"). --export([current/2, sort/2, revsort/2, offset_sec/1]). +-export([ + current/1 + , current_as_of_utc/2 + , sort/2 + , revsort/2 + , offset_sec/1 + ]). -current(_, []) -> + + +current(TzName) -> + current_as_of_utc(erlang:universaltime(), TzName). + +current_as_of_utc(UTCDatetime, TzName) -> + Zones= ezic_db:zones(TzName), + get_zone_utc(UTCDatetime, Zones). + +%%current_as_of_local(_Datetime, _TzName) -> +%% not_done. + + + + + +get_zone_utc(_, []) -> erlang:error(no_current); -current(Now, Zones) -> +get_zone_utc(UTCDatetime, Zones) -> SortedZones= lists:sort(fun revsort/2, Zones), -% ?debug("SortedZones: ~p", [SortedZones]), - [CZone|_]= lists:dropwhile(fun(SZ)-> older_zone(SZ, Now) end, SortedZones), - CZone. +%% [CZone|_]= lists:dropwhile(fun(SZ)-> older_zone_utc(SZ, UTCDatetime) end, SortedZones), +%% CZone. + + + %% begin with GMT offset. + %% foreach rule (oldest first) + %% see whether until=(standard|wall|utc) time + %% + + not_done. @@ -20,8 +49,11 @@ sort(#zone{until=U1}, #zone{until=U2}) -> revsort(Z1=#zone{}, Z2=#zone{}) -> not sort(Z1, Z2). -older_zone(#zone{until=Until}, Now) -> - ezic_date:compare_datetimes(Until, Now). + + +% @bug doesn't normalize times. Times are self-relative (to standard time), and Now may come from any timezone unless we're careful about that. +older_zone(#zone{until=Until}, UTCDatetime) -> + ezic_date:compare_datetimes(Until, UTCDatetime).