touch: accept month names, weekday names and the C locale's m/d/y - #206
Merged
Merged
Conversation
The Windows CI job has been failing on main, not just on branches:
touch/touch_windows.c.v:17:20: error: cannot use �oidptr as u32
in argument 1 to C.SetFileTime
A HANDLE is a pointer. vlib's C.CreateFileW returns voidptr, and used to return
u32, so the u32 in this declaration stopped matching. touch is not in the
Windows ignore list, so the whole Windows build stopped with it.
One word, no behaviour change: the file did not compile, so there was no
behaviour to change.
A string with no zone in it names a local time. This read it as UTC, so every such date landed hours off. Measured against GNU on this machine at +0330: touch -d '2020-01-02 03:04:05' GNU 2020-01-02 03:04:05 before 2020-01-02 06:34:05 The offset cannot come from time.offset(), which reports the zone now. The same zone wants a different offset depending on the date, and both of these were measured rather than assumed: TZ=Asia/Tehran 2021-06-07 08:09:10 wants +0430, and +0330 now TZ=America/New_York 2020-01-02 00:00:00 wants -0500, and -0400 now An earlier attempt here subtracted time.offset() and looked correct on every case in the C locale; it was one hour out for June in Tehran, silently. The offset in force at the date comes from zoneinfo's offset_at, which is what this uses, applied twice so that a first guess landing on the far side of a DST transition is corrected. ## The tests could not have caught this Every -d test in touch_test.v computed its expectation with time.parse_iso8601(date).unix() which is the very call the implementation makes. They agreed with the code by construction, so they would have passed with the bug in place and would fail on any machine that is not at UTC. The expectations are now literal stamps read off GNU 9.4 with TZ pinned to Asia/Tehran, a zone whose offset differs by an hour between its two periods, and each test pins and restores TZ so the result does not depend on the host. Breaking the fix again confirms they bite. With local_wall_clock returning the uncorrected value: test_parse_datetime_naive_string_is_local 1577923200 != 1577910600 test_parse_datetime_uses_the_offset_at_that_date 1623053350 != 1623037150 test_touch_create_with_d_option 1669892401 != 1669879801 and test_parse_datetime_leaves_an_explicit_zone_alone still passed, which is the point: the set discriminates rather than being uniformly red. ## Comparing 48 invocations, nine absolute forms and seven relative ones, each under three zones: UTC as a control where a naive read looks correct, Asia/Tehran, and America/New_York for southern hemisphere rules. 13 of 48 before, 27 of 48 now, and the 21 that still differ are exactly the seven relative forms in each zone: yesterday, tomorrow, 2 hours ago, next week, last month, -1 day, +2 weeks Relative forms are a separate piece of work, not a variant of this one. They also embed the current clock, so their comparison has to be the delta from now rather than the stamp. `v fmt -verify .` passes, `v run build.vsh` builds all 76, and `v test .` reports 49 of 49.
Eight spellings were rejected outright, all of them ordinary: -d @1600000000 -t 202001020304 -d 03:04 -d 03:04:05 -t 2001020304 -d 5 -d 10:00 GMT+3 -d 20200102 Sixteen cases per time zone, three zones: 0 of 48 before, 45 of 48 now. Each rule came from GNU rather than from the manual, and two of them are not what the documentation's shape suggests: - `-t` reads [[CC]YY]MMDDhhmm[.ss]. GNU rejects fourteen digits, so the seconds only exist as the fractional part, and a ten digit reading takes the century from the first two, giving 2020 rather than 1920. - The compact reading belongs to `-t`. Under `-d`, GNU reads the same twelve digits as something else entirely and lands in the year 2446, so `-d` must not quietly accept them as a date. parse_datetime therefore takes which flag it came from. - A bare time means today, not 1970, so the date has to come from the local zone. A bare YYYYMMDD is local midnight on that date. - A zone word after a bare time names today at that time in that zone, so the suffix's sign is the opposite of how it reads: `10:00 GMT+3` lands three hours *before* `10:00 UTC`. - `@1600000000.5` drops the fraction, since a file stamp holds whole seconds. Everything recognised is rewritten into an ISO 8601 string first, so the zone logic from the previous commit stays in one place instead of being repeated once per spelling. ## The one that is left `-d 20200102T0304` gives 2020-01-02 10:04 at GNU, under every zone, and the result does not correspond to any reading of 0304 that can be found. It is not implemented rather than matched bug for bug, and this is the whole of the remaining 3 of 48. ## Tests Six new tests, alongside the existing ones, with expectations read off GNU under TZ=Asia/Tehran. Two of them cannot be a fixed number: "today" moves, so the bare-time test reads the result back into the zone and compares the calendar fields, and the compact-stamp test compares against the spelled out form rather than a literal. Breaking the normalisation out of parse_datetime makes the suite fail at test_parse_datetime_epoch, and the matrix goes back to rejecting all sixteen spellings. `v fmt -verify .` passes, `v run build.vsh` builds all 76, `v test .` reports 49 of 49, and src/touch builds with no notices. Stacked on vlang#204, which this needs for the local-time reading that bare times depend on.
Twenty three spellings were rejected, the rest of what GNU takes: March 1, 2020 1 March 2020 Mar 1 2020 Mon March 2 2020 Monday, March 2, 2020 1/2/2020 2/1/2020 1 March 1/2/20 1/2/2020 03:04:05 Twenty three cases under two zones: 6 of 46 before, 46 of 46 now. The 6 that already passed were the ones both binaries reject. Every rule came from GNU. Two of them are the opposite of what the shape suggests, and both were measured rather than reasoned about: - A slashed date is **month first**, because that is the C locale's d_fmt: `1/2/2020` is 2 January and `2/1/2020` is 1 February. GNU does not fall back to day first when the first number cannot be a month, so `13/2/2020` is an error rather than 13 February. - A weekday name is accepted and then **ignored**, even when it is the wrong one. 2020-03-01 was a Sunday, and Sun, Mon and Sat all give 2020-03-01. Also measured, and implemented because they turned up while testing the above: - Month names ignore case and an optional full stop: MARCH, march and Mar. alike. - A missing year is the current one, so `1 March` is 1 March of this year. - The day is checked against the month rather than rolled over: `31 April 2020` and `29 February 2021` are errors, while `29 February 2020` is fine. - A day is required, so `March 2020` is an error. - A time may follow, with or without seconds. - `2020-01-02 03:04` and `2020-01-02 03` were being rejected as well, because parse_iso8601 insists on HH:MM:SS. The seconds are now supplied. That last one is where the interesting ambiguity is. A bare four digit number is HHMM when it stands alone, since `-d 2020` is 20:20 today, but a year once a year has been taken, since `March 1 2020 0304` is 03:04 on 1 March 2020. Both are covered by tests. ## Tests Six more tests. Stamps read off GNU under TZ=Asia/Tehran; the two that cannot be fixed numbers, a missing year and the bare-time base, are compared against the same zone's calendar instead. Making worded_date return none drops the matrix from 46 of 46 to 6 of 46 and fails the suite at test_parse_datetime_month_names. `v fmt -verify .` passes, `v run build.vsh` builds all 76, `v test .` reports 49 of 49, and src/touch builds with no notices. Stacked on vlang#205, which carries vlang#204.
metif12
force-pushed
the
fix/touch-text-dates
branch
from
October 3, 2026 21:48
63c4356 to
c1f3389
Compare
On Windows, time.load_location('Local') answers with the zone the machine is set
to whatever the environment says. Measured on this host at +0330:
TZ=UTC touch -d '2020-01-02 03:04:05' -> 1577934245
TZ=Asia/Tehran touch -d '2020-01-02 03:04:05' -> 1577921645
TZ=America/New_York touch -d '2020-01-02 03:04:05' -> 1577952245
all three, before touch -d '2020-01-02 03:04:05' -> 1577921645
The first three are what GNU gives; the last is what this did for all of them.
Those are also the values this repository's own tests assert, so the tests passed
only because this machine is in the one zone they name, and every test file that
pins TZ would fail on a runner set to anything else.
TZ is now read first and 'Local' is the fallback, which is also what POSIX says
it means. A TZ naming no zone V can load is ignored rather than fatal, so an
unusual value falls back to the machine's own zone instead of turning every date
into an error.
local_today cannot use Time.local() for the same reason, and reads the date as
the epoch moved by the offset now in force and then taken as UTC fields.
## Tests
One new test, and it is the one that would have caught this: the same date read
in three zones inside a single process. A location cached from the first read
would fail it, which is the point.
src/touch is 24 of 24 and `v fmt -verify src/touch` passes.
Stacked on vlang#206, which carries vlang#205 and vlang#204.
Seven of the spellings this port rejected outright: -d yesterday -d tomorrow -d '2 hours ago' -d next week -d 'last month' -d '-1 day' -d '+2 weeks' Twenty one cases, seven forms in each of three zones: 0 of 21 before, 21 of 21 now, measured end to end against GNU touch 9.4 by writing a file with each binary and reading back what it wrote. Every zone's first row is a control with an absolute date, whose stamp does depend on the zone, so a comparison that passed because TZ had stopped being honoured would show up there. Two of the rules are the opposite of what the shape of the input suggests, and both were measured rather than reasoned about: - A bare item is a plain number of seconds, not a shift of the wall clock. Under TZ=America/New_York at 18:00:18 EDT, `+1 month` moved by exactly 2678400 and landed at 17:00:00 EST; adding a month to local time would have made it 18:00 EST, an hour later. Every day, week and month item reads the same in all three zones, which is only possible if the zone is not consulted at all. - `ago` negates the item it follows and leaves the rest of the string alone, so `1 day 2 hours ago` is +22 hours and `2 hours 1 day ago` is -22. Negating the whole string gives -26 for the first, which is not what GNU prints. Also measured and implemented: - A signed count carries its sign and nothing more: `-1 day` is -86400. - Whole months carry an over-long day into the next month rather than clamping it, so 31 January is 2 March and not 29 February. - `today` and `now` both name this instant. `today` is not midnight: measured with the clock at 01:31:18, `date -d today` returned that second. - The words are read in any case, and any run of spaces separates them. - `previous day`, `last 2 days`, `next 2 weeks`, `1 day from now`, `1 day after`, `1 day before` and a bare `ago` are all rejected, as they are at GNU. - The items belong to -d alone, so `touch -t yesterday` is an error. What is not implemented, and no test claims otherwise: a relative item *after* an absolute date (`2020-01-02 tomorrow`), which GNU takes through a different path in its parser, and a bare weekday name (`monday`), which names the coming Monday at midnight. Given an explicit base date GNU's signed items also shift by an hour per count, so `-2 weeks` after a date is +7 days +2 hours; that belongs to the same unimplemented path and is why the comment in relative.v says so. ## Tests Six new tests. The day, week and hour items are compared as a delta from now inside a window, because both sides read the clock as they run. Whole months are the one item whose number of seconds depends on the day of the month, so add_months is pinned on its own against GNU's table and the wiring is checked separately, which keeps the test honest on the 31st as well as on the 3rd. Breaking the `ago` rule again to confirm the suite bites: 1 day 2 hours ago: 1790987642 - 79200 = 1790908442, outside [1791066842, ...] One bug found on the way, which is worth naming because it is a V trap rather than a date trap: reading a missing key out of a map yields the zero value and does *not* run an `or` block, and the zero value of the unit enum is `.second`. Every unrecognised word therefore became one second, and `-d '1 March'` parsed as a second from now. rel_unit now checks `in` first. The pre-existing test for a missing year is what caught it. src/touch is 25 of 25 and `v fmt -verify src/touch` passes. The full suite is 31 of 49 files failing locally for want of Windows binaries in bin/, which is the state this checkout was already in and is unrelated: the only files changed are under src/touch, and src/touch/touch_test.v is one of the 18 that pass. Stacked on the TZ commit, which carries vlang#206 and its ancestors.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
touch -dand-tup to GNU, in six commits.This was four pull requests, #203 to #206, and they are folded into this one. GitHub
will not accept a base branch that lives in a fork -
422 Validation Failed: Proposed base branch 'fix/touch-windows' was not found- so each of them had to target mainand each showed its ancestors' commits as well. That is also why #204 and #205 were
red at the build step: main does not compile touch on Windows, and only the bottom
of the chain carried the one-line fix for it.
What is here, in order
declare SetFileTime's handle as voidptr - the one line without which touch does
not build on Windows at all, because
CreateFileWreturns a handle and thedeclaration said
u32. Nothing else here could be tested until this was in.read a date with no zone in it as local time - a string with no zone was read
as UTC, so on this machine, at +0330,
touch -d '2020-01-02 03:04:05'set thefile to 06:34:05 where GNU sets 03:04:05. The offset cannot come from
time.offset(), which reports the zone now: Tehran wants +0430 in June and +0330in October, and America/New_York wants -0500 in January where it is -0400 now.
The offset in force at the date comes from zoneinfo's
offset_at, applied twiceso a first guess landing past a DST transition is corrected.
the date forms parse_iso8601 refuses -
@1600000000,-t 202001020304,-d 03:04,-d 10:00 GMT+3,-d 20200102and others. Everything recognised isrewritten into an ISO 8601 string first, so the zone logic stays in one place.
-treads[[CC]YY]MMDDhhmm[.ss]and takes the century from the first two digits,and the compact reading belongs to
-talone: under-dthe same twelve digitsland in the year 2446 at GNU, so this does not quietly accept them.
month names, weekday names and the C locale's m/d/y -
March 1, 2020,1 March 2020,Mon March 2 2020,1/2/2020. Two of the rules are the oppositeof what the shape suggests, and both were measured: a slashed date is month
first, because that is the C locale's
d_fmt, so1/2/2020is 2 January and13/2/2020is an error rather than 13 February; and a weekday name is accepted andthen ignored, even when it is the wrong one.
read a zone-less date in the zone TZ names - on Windows,
time.load_location('Local')answers with the zone the machine is set to whateverthe environment says. Measured here at +0330:
TZ=America/New_Yorkstill gave+0330. Since every expectation in the tests is a literal measured under
TZ=Asia/Tehran, they were passing only because this machine is in the one zonethey name, and would fail on any other runner.
the relative items GNU resolves against the clock -
yesterday,2 hours ago,next week,last month,-1 day,+2 weeksand the rest. Two more rules theshape does not suggest: a bare item is a plain number of seconds and consults no
zone at all, so
+1 monthunder America/New_York moves by exactly 2678400 andlands an hour earlier in the wall clock than a calendar addition would; and
agonegates the item it follows and leaves the rest of the string alone, so
1 day 2 hours agois +22 hours and2 hours 1 day agois -22.What the numbers are
Every rule above was read off GNU 9.4 rather than out of its manual, and each commit
carries the measurements it was built from. The relative items are the one set
compared end to end: seven forms in each of UTC, Asia/Tehran and America/New_York,
21 invocations, 0 of 21 before and 21 of 21 now, by writing a file with each binary
and reading back what it wrote. Each zone's first row of that comparison is a control
with an absolute date, whose stamp does depend on the zone - a bare relative item
looks the same everywhere, so without that control a comparison would pass even if TZ
had stopped being honoured.
Two things are deliberately not implemented, and no test claims otherwise: a relative
item after an absolute date, which GNU takes through a different path in its parser,
and a bare weekday name, which names the coming Monday at midnight.
Tests
v test ./src/touchis 25 of 25, and so isv -Wimpure-v -W test ./src/touch.v fmt -verify src/touchpasses. The expectations are literal stamps measured under apinned
TZ, or deltas from the clock inside a one second window, because both sidesof a relative comparison read the clock as they run.
The full suite on Windows is unaffected by this: 28 of 49 files pass with it and 13
fail without it, all of them for reasons of their own that are being fixed separately.