From dc922c276cc9d4315d6f8105c75c497bfc5fa04b Mon Sep 17 00:00:00 2001 From: youdie006 Date: Tue, 15 Sep 2026 01:00:29 +0900 Subject: [PATCH] Deprecate the fraction of an hour in a zone offset 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. --- ext/date/date_core.c | 23 ++++++++++++++------- ext/date/date_parse.c | 20 +++++++++++++++++- test/date/test_date_new.rb | 40 ++++++++++++++++++++++++++++++++++++ test/date/test_date_parse.rb | 20 ++++++++++++++++++ 4 files changed, 95 insertions(+), 8 deletions(-) diff --git a/ext/date/date_core.c b/ext/date/date_core.c index 0d69633..cd3567a 100644 --- a/ext/date/date_core.c +++ b/ext/date/date_core.c @@ -2562,6 +2562,7 @@ valid_nth_kday_p(VALUE y, int m, int n, int k, double sg, #endif VALUE date_zone_to_diff(VALUE); +VALUE date_zone_to_diff_frac(VALUE, int *, long *); static int offset_to_sec(VALUE vof, int *rof) @@ -2633,13 +2634,21 @@ offset_to_sec(VALUE vof, int *rof) } case T_STRING: { - VALUE vs = date_zone_to_diff(vof); - long n; - - if (!FIXNUM_P(vs)) - return 0; - n = FIX2LONG(vs); - if (n < -DAY_IN_SECONDS || n > DAY_IN_SECONDS) + int frac, ok = 0; + long whole, n = 0; + VALUE vs = date_zone_to_diff_frac(vof, &frac, &whole); + + if (FIXNUM_P(vs)) { + n = FIX2LONG(vs); + if (n >= -DAY_IN_SECONDS && n <= DAY_IN_SECONDS) + ok = 1; + } + /* warn only where dropping the fraction would give an offset + * different from the one this returns today */ + if (frac && (ok ? n : 0) != whole) + rb_warning("fraction of hour in a zone offset is deprecated" + " and will be ignored"); + if (!ok) return 0; *rof = (int)n; return 1; diff --git a/ext/date/date_parse.c b/ext/date/date_parse.c index 37748a1..0a468a8 100644 --- a/ext/date/date_parse.c +++ b/ext/date/date_parse.c @@ -413,12 +413,15 @@ shrink_space(char *d, const char *s, long l) } VALUE -date_zone_to_diff(VALUE str) +date_zone_to_diff_frac(VALUE str, int *fracp, long *whole) { VALUE offset = Qnil; long l = RSTRING_LEN(str); const char *s = RSTRING_PTR(str); + *fracp = 0; + *whole = 0; + { int dst = 0; int w; @@ -533,6 +536,8 @@ date_zone_to_diff(VALUE str) offset = rb_rational_num(offset); } } + *fracp = sec != 0; + *whole = hour * 3600; goto ok; } else if (l > 2) { @@ -558,6 +563,19 @@ date_zone_to_diff(VALUE str) return offset; } +VALUE +date_zone_to_diff(VALUE str) +{ + int frac; + long whole; + VALUE offset = date_zone_to_diff_frac(str, &frac, &whole); + + if (frac) + rb_warning("fraction of hour in a zone offset is deprecated" + " and will be ignored"); + return offset; +} + static int day_num(VALUE s) { diff --git a/test/date/test_date_new.rb b/test/date/test_date_new.rb index b839c52..0081052 100644 --- a/test/date/test_date_new.rb +++ b/test/date/test_date_new.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'test/unit' require 'date' +require 'stringio' class TestDateNew < Test::Unit::TestCase @@ -213,6 +214,45 @@ def test_civil__offset assert_equal(0, d.offset) end + def test_civil__fractional_zone_offset_deprecated + # A fraction that makes the offset non-integral is already dropped whole, + # so ignoring the fraction will not change these; only the pre-existing + # "invalid offset" warning may appear. + ['+00.123', '-00.123'].each do |z| + out = capture_zone_warning { assert_equal(0, DateTime.civil(2001,2,3, 0,0,0, z).offset) } + assert_match(/invalid offset is ignored/, out) + refute_match(/fraction of hour/, out) + end + + # Here the fraction reaches the result, or dropping it would give an + # offset different from the 0 returned today. + out = capture_zone_warning do + assert_equal(Rational(1800, 86400), DateTime.civil(2001,2,3, 0,0,0, '+00.5').offset) + end + assert_match(/fraction of hour/, out) + + out = capture_zone_warning do + assert_equal(0, DateTime.civil(2001,2,3, 0,0,0, '+01.123').offset) + end + assert_match(/fraction of hour/, out) + + # A zero fraction and the colon forms change nothing. + ['+01.0', '+00:30'].each do |z| + out = capture_zone_warning { DateTime.civil(2001,2,3, 0,0,0, z) } + refute_match(/fraction of hour/, out) + end + end + + def capture_zone_warning + verbose, $VERBOSE = $VERBOSE, true + err, $stderr = $stderr, StringIO.new + yield + $stderr.string + ensure + $stderr = err + $VERBOSE = verbose + end + def test_civil__reform d = Date.jd(Date::ENGLAND, Date::ENGLAND) dt = DateTime.jd(Date::ENGLAND, 0,0,0,0, Date::ENGLAND) diff --git a/test/date/test_date_parse.rb b/test/date/test_date_parse.rb index 8308f25..4a73cc8 100644 --- a/test/date/test_date_parse.rb +++ b/test/date/test_date_parse.rb @@ -5,6 +5,26 @@ class TestDateParse < Test::Unit::TestCase + def test__parse_fractional_zone_offset_deprecated + # The fractional hour is honoured today and will be ignored; warn only + # where dropping it changes the answer this returns now. + assert_warning(/fraction of hour/) do + assert_equal(Rational(2214, 5), Date._parse('2001-02-03T00:00:00+00.123')[:offset]) + end + assert_warning(/fraction of hour/) do + 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 + assert_equal(3600, Date._parse('2001-02-03T00:00:00+01.0')[:offset]) + end + # The colon forms have no fractional hour at all. + assert_warning('') do + assert_equal(1800, Date._parse('2001-02-03T00:00:00+00:30')[:offset]) + assert_equal(86399, Date._parse('2001-02-03T00:00:00+23:59:59')[:offset]) + end + end + def test__parse [ # ctime(3), asctime(3)