Conversation
jeremyevans
left a comment
There was a problem hiding this comment.
I think it would be better to deprecate fractional hour support in offsets, and only handle full hour or hour:minute. From some brief searching, I couldn't find any popular programming language support for fractional hour offsets, and Ruby's Time does not support it either.
|
That makes sense to me, and your premise checks out —
One adjustment to the scope, and it is the reason I am posting the table rather than just agreeing: "only full hour or hour:minute" would also drop The other thing worth noting is the last row. Happy to redo this PR as the deprecation. Two questions on shape:
One implementation note carried over from #191: |
|
Deprecation similar to #191 is best. In terms of the warning, we should warn any case where a parsing the fractional offset hour affects the resulting return value. If the fractional offset part is already ignored, there is no need to warn, because the behavior will change to ignoring the value. |
|
Understood on the shape — deprecation like #191. Before I write it, one thing your rule decides differently depending on which API is asked, so I would rather show you than guess. Measured on 3.2.11, decimal-fraction offsets only (the
The last two rows are the awkward ones. Three or more fractional digits make So for Two ways to resolve it:
I lean towards (1) — the thing being deprecated is the |
|
It's really bad to emit a deprecation warning when the behavior will not change. You are not limited to adding a deprecation warning in a single place. You can add a warning in |
Date._parse and friends honour a fractional hour, so ignoring it later will change what they return; they warn whenever the fraction is not zero. DateTime.civil goes through offset_to_sec, which already drops a zone whose fraction makes the offset non-integral. It warns only where the offset it returns today differs from the one the whole hour alone would give, so '+00.123' stays quiet while '+01.123' does not. date_zone_to_diff keeps its signature and warns; offset_to_sec uses a new date_zone_to_diff_frac that reports the fraction instead.
d4b23cf to
dc922c2
Compare
|
Reworked to deprecation only; nothing returns a different value than before. Two warnings, gated separately as you described. That second gate is what splits the two cases you asked about. Measured on master, then on this branch:
Writing the test for the quiet rows caught something worth mentioning:
Disclosure: written with the help of Claude (an AI assistant). The tables are from runs on my machine. |
|
|
||
| if (frac) | ||
| rb_warning("fraction of hour in a zone offset is deprecated" | ||
| " and will be ignored"); |
There was a problem hiding this comment.
| " and will be ignored"); | |
| " and will be ignored in a future version"); |
Similar change for the other warning.
| assert_equal(1800, Date._parse('2001-02-03T00:00:00+00.5')[:offset]) | ||
| end | ||
| # A zero fraction changes nothing, so it must stay quiet. | ||
| assert_warning('') do |
There was a problem hiding this comment.
This should warn, because while the :offset entry returned may not change, the :zone entry should change to remove the .0
| end | ||
| end | ||
|
|
||
| def capture_zone_warning |
There was a problem hiding this comment.
Is there a reason for this approach instead of assert_warning?
| VALUE offset = date_zone_to_diff_frac(str, &frac, &whole); | ||
|
|
||
| if (frac) | ||
| rb_warning("fraction of hour in a zone offset is deprecated" |
There was a problem hiding this comment.
Add the following block up to near the top of the file (copied from date_core.c):
#ifndef HAVE_RB_CATEGORY_WARN
#define rb_category_warn(category, fmt) rb_warn(fmt)
#endifthen switch to:
| rb_warning("fraction of hour in a zone offset is deprecated" | |
| rb_category_warn(RB_WARN_CATEGORY_DEPRECATED, "fraction of hour in a zone offset is deprecated" |
Similar change for the other warning, except there you would just move the block up in the same file.
| if (out_of_range(sec, 0, 59)) return Qnil; | ||
| } | ||
| } | ||
| else if (*p == ',' || *p == '.') { |
There was a problem hiding this comment.
Crazy, this supports , in addition to .
We should do a manual check that removing this else if branch only breaks the deprecated behavior tests and no other tests.
|
|
||
| class TestDateParse < Test::Unit::TestCase | ||
|
|
||
| def test__parse_fractional_zone_offset_deprecated |
There was a problem hiding this comment.
There should be tests added for Date._strptime, which should have appropriate warnings
The inconsistency
date_zone_to_diffreturns a Rational for a fractional-hour zone with morethan two digits (
ext/date/date_parse.c:529-535), and the comment just above itsays that is intended — "no over precision for offset; 10**-7 hour = 0.36
milliseconds should be enough" (
:506-508).offset_to_sec's string branch threw that away:if (!FIXNUM_P(vs)) return 0;at
ext/date/date_core.c:2639.Its two sibling branches in the same function do the opposite — they round and
warn.
T_FLOATat:2590-2592andT_RATIONALat:2621-2623both emitrb_warning("fraction of offset is ignored").So one quantity, three ways, two answers:
442.8 seconds either way. The string form — the only one a user actually types —
is the one that silently becomes
+00:00.Not currently tested
git ls-files -z test | xargs -0 grep -nE "['\"][-+][0-9]{2}\.[0-9]+['\"]"returnsnothing: no test passes a dotted zone as an offset argument. The
.123hits inthe suite are sub-second fractions of the time (
'19990523235521.123456+0900'),not zones, and
grep -n 'fraction of offset' test/finds nothing.The one fractional zone that is tested,
'[-9.50]'attest_date_parse.rb:137-138,has two digits, so it takes
date_zone_to_diff'sn <= 2Integer path(
date_parse.c:524-528) — already consistent, and untouched by this. The Rationalpath is what was untested.
The change
Round it, exactly as the sibling branches do.
k_rational_p,f_round,f_eqeq_pandrb_warningare all already used in this file.The
k_rational_pguard matters:date_zone_to_diffreturnsQnilfor a zone itrejects, and without the guard that
nilreachesf_round.Not a widening
'+24:00'and'+99:00'still give 0 —date_zone_to_diffrejects them beforethis code runs.
'+00:00'→ 0,'+01.5'→ 5400,'+9.50'→ 34200,'+00.1'→360,
'+00.12'→ 432,'+23:59:59'→ 86399 are all unchanged. Then < -DAY_IN_SECONDS || n > DAY_IN_SECONDScheck at:2642still runs on therounded value, and
'+23.9999999'rounds to exactly 86400, which the inclusiveguard accepts — consistent with
Rational(1,1)being legal per the existingtest_civil__offsetat:196-197.Direction
I want to be straight about the tension here. Read shallowly, the recent commits
on these files are a tightening trend (#183's range check, #188's commercial-week
validation,
c98d85dverifying argument classes), which would argue for making thestring branch keep rejecting.
I do not think that is the right read. #183's own commit message frames its defect
as "the Integer 2 is rejected, but Rational(2,1) is the same quantity" — the
principle is consistency between representations of one quantity, which is exactly
this. And rounding is the long-standing behaviour here:
rb_warning("fraction of offset is ignored")already exists at:2592,:2623and:3349. The stringbranch is the deviation, not the rule.
If you would rather go the other way and make Float and Rational reject too, that
is a coherent position and I am happy to write that instead.
Verification
errors. Pristine control run: same 148 tests with exactly one failure, mine —
so nothing else changes behaviour.
since TruffleRuby ships its own
date. It does compile this C extension viaSulong, and I ran it: with master's code
'+00.123'gives 0 and warns "invalidoffset is ignored"; with this patch it gives 443 and warns "fraction of offset is
ignored", while
'+24:00'/'+99:00'stay 0 and'+01.5'/'+23:59:59'areunchanged. So the truffleruby job is fixed by this too rather than broken by it.
warning fails; dropping the
k_rational_pguard raisesNoMethodError: undefined method 'round' for nilon the'+24:00'row.One thing I deliberately left out
DateTime.parse('2001-02-03T00:00:00+00.123')goes throughdt_new_by_frags,which does
of = NUM2INT(t)atdate_core.c:8519and truncates to 442. Soafter this change
DateTime.parseandDateTime.newdiffer by one second on thesame zone string.
That is a real but separate inconsistency in a different function, and bundling it
would make this patch harder to judge. Happy to follow up. (Same spot also narrows
before its range check at
:8520, the inverse of #183's principle — currentlyunreachable, since
date_zone_to_diffcaps output near 3.6e8.)Disclosure
I used an AI assistant to help find and prepare this change. I reviewed and tested
it myself, and the outputs above are from runs I performed.