Skip to content

Commit bae5812

Browse files
committed
Replace Date and DateTime classes from C to Ruby.
C implementation has been rewritten as faithfully as possible in pure Ruby. [Feature #21264] https://bugs.ruby-lang.org/issues/21264
1 parent 6e0f59f commit bae5812

File tree

14 files changed

+9619
-53
lines changed

14 files changed

+9619
-53
lines changed

Rakefile

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,47 @@
11
require "bundler/gem_tasks"
22
require "rake/testtask"
3-
require "shellwords"
4-
require "rake/extensiontask"
53

6-
extask = Rake::ExtensionTask.new("date") do |ext|
7-
ext.name = "date_core"
8-
ext.lib_dir.sub!(%r[(?=/|\z)], "/#{RUBY_VERSION}/#{ext.platform}")
9-
end
4+
if RUBY_VERSION >= "3.3"
5+
# Pure Ruby — no compilation needed
6+
Rake::TestTask.new(:test) do |t|
7+
t.libs << "lib"
8+
t.libs << "test/lib"
9+
t.ruby_opts << "-rhelper"
10+
t.test_files = FileList['test/**/test_*.rb']
11+
end
1012

11-
Rake::TestTask.new(:test) do |t|
12-
t.libs << extask.lib_dir
13-
t.libs << "test/lib"
14-
t.ruby_opts << "-rhelper"
15-
t.test_files = FileList['test/**/test_*.rb']
16-
end
13+
task :compile # no-op
14+
15+
else
16+
# C extension for Ruby < 3.3
17+
require "shellwords"
18+
require "rake/extensiontask"
19+
20+
extask = Rake::ExtensionTask.new("date") do |ext|
21+
ext.name = "date_core"
22+
ext.lib_dir.sub!(%r[(?=/|\z)], "/#{RUBY_VERSION}/#{ext.platform}")
23+
end
24+
25+
Rake::TestTask.new(:test) do |t|
26+
t.libs << extask.lib_dir
27+
t.libs << "test/lib"
28+
t.ruby_opts << "-rhelper"
29+
t.test_files = FileList['test/**/test_*.rb']
30+
end
31+
32+
task test: :compile
1733

18-
task compile: "ext/date/zonetab.h"
19-
file "ext/date/zonetab.h" => "ext/date/zonetab.list" do |t|
20-
dir, hdr = File.split(t.name)
21-
make_program_name =
22-
ENV['MAKE'] || ENV['make'] ||
23-
RbConfig::CONFIG['configure_args'][/with-make-prog\=\K\w+/] ||
24-
(/mswin/ =~ RUBY_PLATFORM ? 'nmake' : 'make')
25-
make_program = Shellwords.split(make_program_name)
26-
sh(*make_program, "-f", "prereq.mk", "top_srcdir=.."+"/.."*dir.count("/"),
27-
hdr, chdir: dir)
34+
task compile: "ext/date/zonetab.h"
35+
file "ext/date/zonetab.h" => "ext/date/zonetab.list" do |t|
36+
dir, hdr = File.split(t.name)
37+
make_program_name =
38+
ENV['MAKE'] || ENV['make'] ||
39+
RbConfig::CONFIG['configure_args'][/with-make-prog\=\K\w+/] ||
40+
(/mswin/ =~ RUBY_PLATFORM ? 'nmake' : 'make')
41+
make_program = Shellwords.split(make_program_name)
42+
sh(*make_program, "-f", "prereq.mk", "top_srcdir=.."+"/.."*dir.count("/"),
43+
hdr, chdir: dir)
44+
end
2845
end
2946

3047
task :default => [:compile, :test]

date.gemspec

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,19 @@
11
# frozen_string_literal: true
22

3-
version = File.foreach(File.expand_path("../lib/date.rb", __FILE__)).find do |line|
4-
/^\s*VERSION\s*=\s*["'](.*)["']/ =~ line and break $1
5-
end
3+
require_relative "lib/date/version"
64

75
Gem::Specification.new do |s|
86
s.name = "date"
9-
s.version = version
7+
s.version = Date::VERSION
108
s.summary = "The official date library for Ruby."
119
s.description = "The official date library for Ruby."
1210

13-
if Gem::Platform === s.platform and s.platform =~ 'java' or RUBY_ENGINE == 'jruby'
14-
s.platform = 'java'
15-
# No files shipped, no require path, no-op for now on JRuby
16-
else
17-
s.require_path = %w{lib}
11+
s.require_path = %w{lib}
1812

19-
s.files = [
20-
"README.md", "COPYING", "BSDL",
21-
"lib/date.rb", "ext/date/date_core.c", "ext/date/date_parse.c", "ext/date/date_strftime.c",
22-
"ext/date/date_strptime.c", "ext/date/date_tmx.h", "ext/date/extconf.rb", "ext/date/prereq.mk",
23-
"ext/date/zonetab.h", "ext/date/zonetab.list"
24-
]
25-
s.extensions = "ext/date/extconf.rb"
26-
end
13+
s.files = Dir["README.md", "COPYING", "BSDL", "lib/**/*.rb",
14+
"ext/date/*.c", "ext/date/*.h", "ext/date/extconf.rb",
15+
"ext/date/prereq.mk", "ext/date/zonetab.list"]
16+
s.extensions = ["ext/date/extconf.rb"]
2717

2818
s.required_ruby_version = ">= 2.6.0"
2919

ext/date/extconf.rb

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
# frozen_string_literal: true
22
require 'mkmf'
33

4-
config_string("strict_warnflags") {|w| $warnflags += " #{w}"}
5-
6-
append_cflags("-Wno-compound-token-split-by-macro") if RUBY_VERSION < "2.7."
7-
have_func("rb_category_warn")
8-
with_werror("", {:werror => true}) do |opt, |
9-
have_var("timezone", "time.h", opt)
10-
have_var("altzone", "time.h", opt)
4+
if RUBY_VERSION >= "3.3"
5+
# Pure Ruby implementation; skip C extension build
6+
File.write("Makefile", dummy_makefile($srcdir).join(""))
7+
else
8+
config_string("strict_warnflags") {|w| $warnflags += " #{w}"}
9+
append_cflags("-Wno-compound-token-split-by-macro") if RUBY_VERSION < "2.7."
10+
have_func("rb_category_warn")
11+
with_werror("", {:werror => true}) do |opt, |
12+
have_var("timezone", "time.h", opt)
13+
have_var("altzone", "time.h", opt)
14+
end
15+
create_makefile('date_core')
1116
end
12-
13-
create_makefile('date_core')

lib/date.rb

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
# frozen_string_literal: true
22
# date.rb: Written by Tadayoshi Funaba 1998-2011
33

4-
require 'date_core'
4+
if RUBY_VERSION >= "3.3"
5+
require_relative "date/version"
6+
require_relative "date/constants"
7+
require_relative "date/core"
8+
require_relative "date/strftime"
9+
require_relative "date/parse"
10+
require_relative "date/strptime"
11+
require_relative "date/time"
12+
require_relative "date/datetime"
13+
else
14+
require 'date_core'
15+
end
516

617
class Date
7-
VERSION = "3.5.1" # :nodoc:
8-
918
# call-seq:
1019
# infinite? -> false
1120
#
@@ -64,7 +73,5 @@ def to_f
6473
-Float::INFINITY
6574
end
6675
end
67-
6876
end
69-
7077
end

lib/date/constants.rb

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# frozen_string_literal: true
2+
3+
# Constants
4+
class Date
5+
HAVE_JD = 0b00000001 # 1
6+
HAVE_DF = 0b00000010 # 2
7+
HAVE_CIVIL = 0b00000100 # 4
8+
HAVE_TIME = 0b00001000 # 8
9+
COMPLEX_DAT = 0b10000000 # 128
10+
private_constant :HAVE_JD, :HAVE_DF, :HAVE_CIVIL, :HAVE_TIME, :COMPLEX_DAT
11+
12+
MONTHNAMES = [nil, "January", "February", "March", "April", "May", "June",
13+
"July", "August", "September", "October", "November", "December"]
14+
.map { |s| s&.encode(Encoding::US_ASCII)&.freeze }.freeze
15+
ABBR_MONTHNAMES = [nil, "Jan", "Feb", "Mar", "Apr", "May", "Jun",
16+
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
17+
.map { |s| s&.encode(Encoding::US_ASCII)&.freeze }.freeze
18+
DAYNAMES = %w[Sunday Monday Tuesday Wednesday Thursday Friday Saturday]
19+
.map { |s| s.encode(Encoding::US_ASCII).freeze }.freeze
20+
ABBR_DAYNAMES = %w[Sun Mon Tue Wed Thu Fri Sat]
21+
.map { |s| s.encode(Encoding::US_ASCII).freeze }.freeze
22+
23+
# Pattern constants for regex
24+
ABBR_DAYS_PATTERN = 'sun|mon|tue|wed|thu|fri|sat'
25+
DAYS_PATTERN = 'sunday|monday|tuesday|wednesday|thursday|friday|saturday'
26+
ABBR_MONTHS_PATTERN = 'jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec'
27+
private_constant :ABBR_DAYS_PATTERN, :DAYS_PATTERN, :ABBR_MONTHS_PATTERN
28+
29+
ITALY = 2299161 # 1582-10-15
30+
ENGLAND = 2361222 # 1752-09-14
31+
JULIAN = Float::INFINITY
32+
GREGORIAN = -Float::INFINITY
33+
34+
DEFAULT_SG = ITALY
35+
private_constant :DEFAULT_SG
36+
37+
MINUTE_IN_SECONDS = 60
38+
HOUR_IN_SECONDS = 3600
39+
DAY_IN_SECONDS = 86400
40+
HALF_DAYS_IN_SECONDS = DAY_IN_SECONDS / 2
41+
SECOND_IN_MILLISECONDS = 1000
42+
SECOND_IN_NANOSECONDS = 1_000_000_000
43+
private_constant :MINUTE_IN_SECONDS, :HOUR_IN_SECONDS, :DAY_IN_SECONDS, :SECOND_IN_MILLISECONDS, :SECOND_IN_NANOSECONDS, :HALF_DAYS_IN_SECONDS
44+
45+
JC_PERIOD0 = 1461 # 365.25 * 4
46+
GC_PERIOD0 = 146097 # 365.2425 * 400
47+
CM_PERIOD0 = 71149239 # (lcm 7 1461 146097)
48+
CM_PERIOD = (0xfffffff / CM_PERIOD0) * CM_PERIOD0
49+
CM_PERIOD_JCY = (CM_PERIOD / JC_PERIOD0) * 4
50+
CM_PERIOD_GCY = (CM_PERIOD / GC_PERIOD0) * 400
51+
private_constant :JC_PERIOD0, :GC_PERIOD0, :CM_PERIOD0, :CM_PERIOD, :CM_PERIOD_JCY, :CM_PERIOD_GCY
52+
53+
REFORM_BEGIN_YEAR = 1582
54+
REFORM_END_YEAR = 1930
55+
REFORM_BEGIN_JD = 2298874 # ns 1582-01-01
56+
REFORM_END_JD = 2426355 # os 1930-12-31
57+
private_constant :REFORM_BEGIN_YEAR, :REFORM_END_YEAR, :REFORM_BEGIN_JD, :REFORM_END_JD
58+
59+
SEC_WIDTH = 6
60+
MIN_WIDTH = 6
61+
HOUR_WIDTH = 5
62+
MDAY_WIDTH = 5
63+
MON_WIDTH = 4
64+
private_constant :SEC_WIDTH, :MIN_WIDTH, :HOUR_WIDTH, :MDAY_WIDTH, :MON_WIDTH
65+
66+
SEC_SHIFT = 0
67+
MIN_SHIFT = SEC_WIDTH
68+
HOUR_SHIFT = MIN_WIDTH + SEC_WIDTH
69+
MDAY_SHIFT = HOUR_WIDTH + MIN_WIDTH + SEC_WIDTH
70+
MON_SHIFT = MDAY_WIDTH + HOUR_WIDTH + MIN_WIDTH + SEC_WIDTH
71+
private_constant :SEC_SHIFT, :MIN_SHIFT, :HOUR_SHIFT, :MDAY_SHIFT, :MON_SHIFT
72+
73+
PK_MASK = ->(x) { (1 << x) - 1 }
74+
private_constant :PK_MASK
75+
76+
# Days in each month (non-leap and leap year)
77+
MONTH_DAYS = [
78+
[0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], # non-leap
79+
[0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] # leap
80+
].freeze
81+
private_constant :MONTH_DAYS
82+
83+
YEARTAB = [
84+
[0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334], # non-leap
85+
[0, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335] # leap
86+
].freeze
87+
private_constant :YEARTAB
88+
89+
# Neri-Schneider algorithm constants
90+
# JDN of March 1, Year 0 in proleptic Gregorian calendar
91+
NS_EPOCH = 1721120
92+
private_constant :NS_EPOCH
93+
94+
# Days in a 4-year cycle (3 normal years + 1 leap year)
95+
NS_DAYS_IN_4_YEARS = 1461
96+
private_constant :NS_DAYS_IN_4_YEARS
97+
98+
# Days in a 400-year Gregorian cycle (97 leap years in 400 years)
99+
NS_DAYS_IN_400_YEARS = 146097
100+
private_constant :NS_DAYS_IN_400_YEARS
101+
102+
# Years per century
103+
NS_YEARS_PER_CENTURY = 100
104+
private_constant :NS_YEARS_PER_CENTURY
105+
106+
# Multiplier for extracting year within century using fixed-point arithmetic.
107+
# This is ceil(2^32 / NS_DAYS_IN_4_YEARS) for the Euclidean affine function.
108+
NS_YEAR_MULTIPLIER = 2939745
109+
private_constant :NS_YEAR_MULTIPLIER
110+
111+
# Coefficients for month calculation from day-of-year.
112+
# Maps day-of-year to month using: month = (NS_MONTH_COEFF * doy + NS_MONTH_OFFSET) >> 16
113+
NS_MONTH_COEFF = 2141
114+
NS_MONTH_OFFSET = 197913
115+
private_constant :NS_MONTH_COEFF, :NS_MONTH_OFFSET
116+
117+
# Coefficients for civil date to JDN month contribution.
118+
# Maps month to accumulated days: days = (NS_CIVIL_MONTH_COEFF * m - NS_CIVIL_MONTH_OFFSET) / 32
119+
NS_CIVIL_MONTH_COEFF = 979
120+
NS_CIVIL_MONTH_OFFSET = 2919
121+
NS_CIVIL_MONTH_DIVISOR = 32
122+
private_constant :NS_CIVIL_MONTH_COEFF, :NS_CIVIL_MONTH_OFFSET, :NS_CIVIL_MONTH_DIVISOR
123+
124+
# Days from March 1 to December 31 (for Jan/Feb year adjustment)
125+
NS_DAYS_BEFORE_NEW_YEAR = 306
126+
private_constant :NS_DAYS_BEFORE_NEW_YEAR
127+
128+
# Safe bounds for Neri-Schneider algorithm to avoid integer overflow.
129+
# These correspond to approximately years -1,000,000 to +1,000,000.
130+
NS_JD_MIN = -364_000_000
131+
NS_JD_MAX = 538_000_000
132+
private_constant :NS_JD_MIN, :NS_JD_MAX
133+
134+
JULIAN_EPOCH_DATE = "-4712-01-01"
135+
JULIAN_EPOCH_DATETIME = "-4712-01-01T00:00:00+00:00"
136+
JULIAN_EPOCH_DATETIME_RFC2822 = "Mon, 1 Jan -4712 00:00:00 +0000"
137+
JULIAN_EPOCH_DATETIME_HTTPDATE = "Mon, 01 Jan -4712 00:00:00 GMT"
138+
private_constant :JULIAN_EPOCH_DATE, :JULIAN_EPOCH_DATETIME, :JULIAN_EPOCH_DATETIME_RFC2822, :JULIAN_EPOCH_DATETIME_HTTPDATE
139+
140+
JISX0301_ERA_INITIALS = 'mtshr'
141+
JISX0301_DEFAULT_ERA = 'H' # obsolete
142+
private_constant :JISX0301_ERA_INITIALS, :JISX0301_DEFAULT_ERA
143+
144+
HAVE_ALPHA = 1 << 0
145+
HAVE_DIGIT = 1 << 1
146+
HAVE_DASH = 1 << 2
147+
HAVE_DOT = 1 << 3
148+
HAVE_SLASH = 1 << 4
149+
private_constant :HAVE_ALPHA, :HAVE_DIGIT, :HAVE_DASH, :HAVE_DOT, :HAVE_SLASH
150+
151+
# C: default strftime format is US-ASCII
152+
STRFTIME_DEFAULT_FMT = '%F'.encode(Encoding::US_ASCII)
153+
private_constant :STRFTIME_DEFAULT_FMT
154+
155+
# strftime spec categories
156+
NUMERIC_SPECS = %w[Y C y m d j H I M S L N G g U W V u w s Q].freeze
157+
SPACE_PAD_SPECS = %w[e k l].freeze
158+
CHCASE_UPPER_SPECS = %w[A a B b h].freeze
159+
CHCASE_LOWER_SPECS = %w[Z p].freeze
160+
private_constant :NUMERIC_SPECS, :SPACE_PAD_SPECS,
161+
:CHCASE_UPPER_SPECS, :CHCASE_LOWER_SPECS
162+
163+
# strptime digit-consuming specs
164+
NUM_PATTERN_SPECS = "CDdeFGgHIjkLlMmNQRrSsTUuVvWwXxYy"
165+
private_constant :NUM_PATTERN_SPECS
166+
167+
# Fragment completion table for DateTime parsing
168+
COMPLETE_FRAGS_TABLE = [
169+
[:time, [:hour, :min, :sec].freeze],
170+
[nil, [:jd].freeze],
171+
[:ordinal, [:year, :yday, :hour, :min, :sec].freeze],
172+
[:civil, [:year, :mon, :mday, :hour, :min, :sec].freeze],
173+
[:commercial, [:cwyear, :cweek, :cwday, :hour, :min, :sec].freeze],
174+
[:wday, [:wday, :hour, :min, :sec].freeze],
175+
[:wnum0, [:year, :wnum0, :wday, :hour, :min, :sec].freeze],
176+
[:wnum1, [:year, :wnum1, :wday, :hour, :min, :sec].freeze],
177+
[nil, [:cwyear, :cweek, :wday, :hour, :min, :sec].freeze],
178+
[nil, [:year, :wnum0, :cwday, :hour, :min, :sec].freeze],
179+
[nil, [:year, :wnum1, :cwday, :hour, :min, :sec].freeze],
180+
].each { |a| a.freeze }.freeze
181+
private_constant :COMPLETE_FRAGS_TABLE
182+
end

0 commit comments

Comments
 (0)