nepdate 1.0.0.rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +10 -0
- data/LICENSE +21 -0
- data/NOTICE +31 -0
- data/README.md +20 -0
- data/lib/nepdate/calendar.rb +85 -0
- data/lib/nepdate/calendar_table.rb +617 -0
- data/lib/nepdate/date.rb +580 -0
- data/lib/nepdate/date_diff.rb +49 -0
- data/lib/nepdate/date_range.rb +251 -0
- data/lib/nepdate/errors.rb +78 -0
- data/lib/nepdate/fiscal_year.rb +103 -0
- data/lib/nepdate/format.rb +148 -0
- data/lib/nepdate/format_options.rb +64 -0
- data/lib/nepdate/lang.rb +21 -0
- data/lib/nepdate/lenient.rb +175 -0
- data/lib/nepdate/month.rb +84 -0
- data/lib/nepdate/month_overflow.rb +21 -0
- data/lib/nepdate/names.rb +81 -0
- data/lib/nepdate/parse.rb +125 -0
- data/lib/nepdate/quarter.rb +78 -0
- data/lib/nepdate/version.rb +5 -0
- data/lib/nepdate/weekday.rb +85 -0
- data/lib/nepdate.rb +37 -0
- data/sig/nepdate.rbs +435 -0
- metadata +75 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NepDate
|
|
4
|
+
# An inclusive, non-empty span of consecutive BS dates. It includes Enumerable, so +each+,
|
|
5
|
+
# +map+, +select+, +to_a+ and the rest work; +length+ is the inclusive day count.
|
|
6
|
+
class DateRange
|
|
7
|
+
include Enumerable
|
|
8
|
+
|
|
9
|
+
attr_reader :start, :end
|
|
10
|
+
|
|
11
|
+
def initialize(start, finish)
|
|
12
|
+
raise TypeError, "start must be a NepDate::Date, got #{start.class}" unless start.is_a?(Date)
|
|
13
|
+
raise TypeError, "end must be a NepDate::Date, got #{finish.class}" unless finish.is_a?(Date)
|
|
14
|
+
raise OutOfRangeError, "a range must not start after it ends" if start > finish
|
|
15
|
+
|
|
16
|
+
@start = start
|
|
17
|
+
@end = finish
|
|
18
|
+
freeze
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
def of(start, finish)
|
|
23
|
+
new(start, finish)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def single_day(date)
|
|
27
|
+
new(date, date)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def from_day_count(start, days)
|
|
31
|
+
days = Date.integer_argument(days, "days")
|
|
32
|
+
raise OutOfRangeError, "a range must span at least one day" if days < 1
|
|
33
|
+
|
|
34
|
+
new(start, start.add_days(days - 1))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def for_month(year, month)
|
|
38
|
+
first = Date.from_bs(year, month, 1)
|
|
39
|
+
new(first, first.last_day_of_month)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def for_year(year)
|
|
43
|
+
first = Date.from_bs(year, 1, 1)
|
|
44
|
+
new(first, Date.from_bs(year, 12, Calendar.month_length(year, 12)))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def current_month(zone: Date::DEFAULT_ZONE, clock: nil)
|
|
48
|
+
today = Date.today(zone: zone, clock: clock)
|
|
49
|
+
for_month(today.year, today.month)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def current_fiscal_year(zone: Date::DEFAULT_ZONE, clock: nil)
|
|
53
|
+
FiscalYear.containing(Date.today(zone: zone, clock: clock)).range
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def current_calendar_year(zone: Date::DEFAULT_ZONE, clock: nil)
|
|
57
|
+
for_year(Date.today(zone: zone, clock: clock).year)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def length
|
|
62
|
+
self.end.serial - start.serial + 1
|
|
63
|
+
end
|
|
64
|
+
alias_method :size, :length
|
|
65
|
+
|
|
66
|
+
def first(count = nil)
|
|
67
|
+
return start if count.nil?
|
|
68
|
+
|
|
69
|
+
to_a.first(count)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def last(count = nil)
|
|
73
|
+
return self.end if count.nil?
|
|
74
|
+
|
|
75
|
+
to_a.last(count)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def empty?
|
|
79
|
+
false
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def include?(date)
|
|
83
|
+
raise TypeError, "expected a NepDate::Date, got #{date.class}" unless date.is_a?(Date)
|
|
84
|
+
|
|
85
|
+
date.between?(start, self.end)
|
|
86
|
+
end
|
|
87
|
+
alias_method :contains?, :include?
|
|
88
|
+
alias_method :contains, :include?
|
|
89
|
+
|
|
90
|
+
def contains_range?(other)
|
|
91
|
+
expect_range!(other)
|
|
92
|
+
include?(other.start) && include?(other.end)
|
|
93
|
+
end
|
|
94
|
+
alias_method :contains_range, :contains_range?
|
|
95
|
+
|
|
96
|
+
def overlaps?(other)
|
|
97
|
+
expect_range!(other)
|
|
98
|
+
start <= other.end && other.start <= self.end
|
|
99
|
+
end
|
|
100
|
+
alias_method :overlaps, :overlaps?
|
|
101
|
+
|
|
102
|
+
# True when one range starts the day after the other ends. A range cannot start before
|
|
103
|
+
# 1901-01-01 or after 2199-12-30, so a range that ends at the data edge simply has no
|
|
104
|
+
# adjacent successor and is never advanced past it (D-16).
|
|
105
|
+
def adjacent_to?(other)
|
|
106
|
+
expect_range!(other)
|
|
107
|
+
if self.end < other.start
|
|
108
|
+
self.end.add_days(1) == other.start
|
|
109
|
+
elsif other.end < start
|
|
110
|
+
other.end.add_days(1) == start
|
|
111
|
+
else
|
|
112
|
+
false
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
alias_method :is_adjacent_to, :adjacent_to?
|
|
116
|
+
|
|
117
|
+
# The overlap, or +nil+ when the ranges do not touch (D-01, D-16).
|
|
118
|
+
def intersect(other)
|
|
119
|
+
expect_range!(other)
|
|
120
|
+
first = (start >= other.start) ? start : other.start
|
|
121
|
+
last = (self.end <= other.end) ? self.end : other.end
|
|
122
|
+
return nil if first > last
|
|
123
|
+
|
|
124
|
+
DateRange.new(first, last)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# The smallest range covering both, not a set union.
|
|
128
|
+
def union(other)
|
|
129
|
+
expect_range!(other)
|
|
130
|
+
first = (start <= other.start) ? start : other.start
|
|
131
|
+
last = (self.end >= other.end) ? self.end : other.end
|
|
132
|
+
DateRange.new(first, last)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# This range with the other removed: zero, one or two ranges.
|
|
136
|
+
def except(other)
|
|
137
|
+
expect_range!(other)
|
|
138
|
+
overlap = intersect(other)
|
|
139
|
+
return [self] if overlap.nil?
|
|
140
|
+
|
|
141
|
+
result = []
|
|
142
|
+
result << DateRange.new(start, overlap.start.add_days(-1)) if start < overlap.start
|
|
143
|
+
result << DateRange.new(overlap.end.add_days(1), self.end) if self.end > overlap.end
|
|
144
|
+
result
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def split_by_month
|
|
148
|
+
result = []
|
|
149
|
+
cursor = start
|
|
150
|
+
while cursor <= self.end
|
|
151
|
+
month_end = cursor.last_day_of_month
|
|
152
|
+
segment_end = (month_end <= self.end) ? month_end : self.end
|
|
153
|
+
result << DateRange.new(cursor, segment_end)
|
|
154
|
+
break if segment_end == self.end
|
|
155
|
+
|
|
156
|
+
cursor = segment_end.add_days(1)
|
|
157
|
+
end
|
|
158
|
+
result
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def split_by_fiscal_quarter
|
|
162
|
+
result = []
|
|
163
|
+
cursor = start
|
|
164
|
+
while cursor <= self.end
|
|
165
|
+
quarter_end = cursor.fiscal_year.quarter_range(cursor.quarter).end
|
|
166
|
+
segment_end = (quarter_end <= self.end) ? quarter_end : self.end
|
|
167
|
+
result << DateRange.new(cursor, segment_end)
|
|
168
|
+
break if segment_end == self.end
|
|
169
|
+
|
|
170
|
+
cursor = segment_end.add_days(1)
|
|
171
|
+
end
|
|
172
|
+
result
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def dates_with_interval(step = 1)
|
|
176
|
+
step = Date.integer_argument(step, "step")
|
|
177
|
+
raise OutOfRangeError, "the step must be at least 1" if step < 1
|
|
178
|
+
|
|
179
|
+
result = []
|
|
180
|
+
cursor = start
|
|
181
|
+
loop do
|
|
182
|
+
result << cursor
|
|
183
|
+
remaining = cursor.days_until(self.end)
|
|
184
|
+
break if remaining < step
|
|
185
|
+
|
|
186
|
+
cursor = cursor.add_days(step)
|
|
187
|
+
end
|
|
188
|
+
result
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Every day except Saturday, and except Sunday when +exclude_sunday+ is true.
|
|
192
|
+
def working_days(exclude_sunday: false)
|
|
193
|
+
each.select do |date|
|
|
194
|
+
!date.weekday.saturday? && !(exclude_sunday && date.weekday.sunday?)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Every Saturday, and Sunday when +include_sunday+ is true.
|
|
199
|
+
def weekend_days(include_sunday: true)
|
|
200
|
+
each.select do |date|
|
|
201
|
+
date.weekday.saturday? || (include_sunday && date.weekday.sunday?)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def each
|
|
206
|
+
return enum_for(:each) unless block_given?
|
|
207
|
+
|
|
208
|
+
serial = start.serial
|
|
209
|
+
last = self.end.serial
|
|
210
|
+
while serial <= last
|
|
211
|
+
yield Date.from_serial(serial)
|
|
212
|
+
serial += 1
|
|
213
|
+
end
|
|
214
|
+
self
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def ==(other)
|
|
218
|
+
other.is_a?(DateRange) && start == other.start && self.end == other.end
|
|
219
|
+
end
|
|
220
|
+
alias_method :eql?, :==
|
|
221
|
+
|
|
222
|
+
def hash
|
|
223
|
+
[DateRange, start, self.end].hash
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def to_s
|
|
227
|
+
"#{start} - #{self.end}"
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def to_short_string(order: :ymd, separator: :slash, lang: :en)
|
|
231
|
+
"#{start.to_short_string(order: order, separator: separator, lang: lang)} - " \
|
|
232
|
+
"#{self.end.to_short_string(order: order, separator: separator, lang: lang)}"
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def to_h
|
|
236
|
+
{start: start, end: self.end}
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def inspect
|
|
240
|
+
"#<NepDate::DateRange #{start.iso8601}..#{self.end.iso8601}>"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
private
|
|
244
|
+
|
|
245
|
+
def expect_range!(other)
|
|
246
|
+
raise TypeError, "expected a NepDate::DateRange, got #{other.class}" unless other.is_a?(DateRange)
|
|
247
|
+
|
|
248
|
+
other
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NepDate
|
|
4
|
+
# The rule that rejected a date, a parse or an arithmetic operation. The names match the
|
|
5
|
+
# neutral error kinds of PARSING.md section 4; each is available as a symbol.
|
|
6
|
+
module ErrorKind
|
|
7
|
+
OUT_OF_RANGE = :out_of_range
|
|
8
|
+
INVALID_MONTH = :invalid_month
|
|
9
|
+
INVALID_DAY = :invalid_day
|
|
10
|
+
INVALID_GREGORIAN = :invalid_gregorian
|
|
11
|
+
INVALID_CHARACTER = :invalid_character
|
|
12
|
+
WRONG_GROUP_COUNT = :wrong_group_count
|
|
13
|
+
NUMBER_TOO_LONG = :number_too_long
|
|
14
|
+
AMBIGUOUS = :ambiguous
|
|
15
|
+
UNRECOGNIZED = :unrecognized
|
|
16
|
+
|
|
17
|
+
ALL = [
|
|
18
|
+
OUT_OF_RANGE, INVALID_MONTH, INVALID_DAY, INVALID_GREGORIAN,
|
|
19
|
+
INVALID_CHARACTER, WRONG_GROUP_COUNT, NUMBER_TOO_LONG, AMBIGUOUS, UNRECOGNIZED
|
|
20
|
+
].freeze
|
|
21
|
+
|
|
22
|
+
# The PascalCase spelling used by the shared vectors, for example +InvalidMonth+.
|
|
23
|
+
def self.name_for(kind)
|
|
24
|
+
kind.to_s.split("_").map(&:capitalize).join
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Every failure this library raises. +kind+ says which rule failed, so callers match on the
|
|
29
|
+
# symbol instead of the message.
|
|
30
|
+
class Error < StandardError
|
|
31
|
+
attr_reader :kind
|
|
32
|
+
|
|
33
|
+
def initialize(kind, message)
|
|
34
|
+
@kind = kind.to_sym
|
|
35
|
+
super(message)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def kind_name
|
|
39
|
+
ErrorKind.name_for(kind)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Raised when a parsed or constructed date, or an arithmetic result, is outside the supported
|
|
44
|
+
# BS range.
|
|
45
|
+
class OutOfRangeError < Error
|
|
46
|
+
def initialize(message)
|
|
47
|
+
super(ErrorKind::OUT_OF_RANGE, message)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Raised when a month number is outside 1 to 12.
|
|
52
|
+
class InvalidMonthError < Error
|
|
53
|
+
def initialize(message)
|
|
54
|
+
super(ErrorKind::INVALID_MONTH, message)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Raised when a day number is outside the month.
|
|
59
|
+
class InvalidDayError < Error
|
|
60
|
+
def initialize(message)
|
|
61
|
+
super(ErrorKind::INVALID_DAY, message)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Raised when an AD date is not a real Gregorian day.
|
|
66
|
+
class InvalidGregorianError < Error
|
|
67
|
+
def initialize(message)
|
|
68
|
+
super(ErrorKind::INVALID_GREGORIAN, message)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Raised by strict and lenient parsing. +kind+ is the parse error kind from PARSING.md.
|
|
73
|
+
class ParseError < Error
|
|
74
|
+
def initialize(kind, message)
|
|
75
|
+
super
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NepDate
|
|
4
|
+
# A fiscal year, 1 Shrawan through the end of Ashad of the following BS year. The value
|
|
5
|
+
# carries the BS year in which it starts, so a date in months 1 to 3 belongs to the fiscal
|
|
6
|
+
# year that started in the previous BS year (D-08).
|
|
7
|
+
class FiscalYear
|
|
8
|
+
include Comparable
|
|
9
|
+
|
|
10
|
+
attr_reader :start_year
|
|
11
|
+
|
|
12
|
+
def initialize(start_year)
|
|
13
|
+
@start_year = Integer(start_year)
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.from(value)
|
|
18
|
+
return value if value.is_a?(FiscalYear)
|
|
19
|
+
|
|
20
|
+
new(value)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The fiscal year containing +date+.
|
|
24
|
+
def self.containing(date)
|
|
25
|
+
raise TypeError, "expected a NepDate::Date, got #{date.class}" unless date.is_a?(Date)
|
|
26
|
+
|
|
27
|
+
new((date.month >= 4) ? date.year : date.year - 1)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# 1 Shrawan of the fiscal year.
|
|
31
|
+
def start
|
|
32
|
+
Date.from_bs(start_year, 4, 1)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The last day of Ashad of the following BS year.
|
|
36
|
+
def end
|
|
37
|
+
year = start_year + 1
|
|
38
|
+
Date.from_bs(year, 3, Calendar.month_length(year, 3))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def range
|
|
42
|
+
DateRange.new(start, self.end)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The quarter +quarter+ of this fiscal year, clipped to the fiscal year's own bounds.
|
|
46
|
+
def quarter_range(quarter)
|
|
47
|
+
quarter = Quarter.from(quarter)
|
|
48
|
+
start_month, start_offset, end_month, end_offset =
|
|
49
|
+
case quarter.number
|
|
50
|
+
when 1 then [4, 0, 6, 0]
|
|
51
|
+
when 2 then [7, 0, 9, 0]
|
|
52
|
+
when 3 then [10, 0, 12, 0]
|
|
53
|
+
else [1, 1, 3, 1]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
first = Date.from_bs(start_year + start_offset, start_month, 1)
|
|
57
|
+
last_year = start_year + end_offset
|
|
58
|
+
last = Date.from_bs(last_year, end_month, Calendar.month_length(last_year, end_month))
|
|
59
|
+
DateRange.new(first, last)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def quarter_start(quarter)
|
|
63
|
+
quarter_range(quarter).start
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def quarter_end(quarter)
|
|
67
|
+
quarter_range(quarter).end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The label, "2081/82", with digits in +lang+.
|
|
71
|
+
def label(lang: :en)
|
|
72
|
+
lang = Lang.coerce(lang)
|
|
73
|
+
"#{Names.digits(start_year, lang, width: 4)}/#{Names.digits((start_year + 1) % 100, lang, width: 2)}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def succ
|
|
77
|
+
FiscalYear.new(start_year + 1)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def <=>(other)
|
|
81
|
+
return nil unless other.is_a?(FiscalYear)
|
|
82
|
+
|
|
83
|
+
start_year <=> other.start_year
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def ==(other)
|
|
87
|
+
other.is_a?(FiscalYear) && start_year == other.start_year
|
|
88
|
+
end
|
|
89
|
+
alias_method :eql?, :==
|
|
90
|
+
|
|
91
|
+
def hash
|
|
92
|
+
[FiscalYear, start_year].hash
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def to_s
|
|
96
|
+
label
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def inspect
|
|
100
|
+
"#<NepDate::FiscalYear #{start_year}/#{(start_year + 1) % 100}>"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NepDate
|
|
4
|
+
# Formatting for every public form (FORMATTING.md sections 2 to 5). One pass, no regular
|
|
5
|
+
# expressions in the pattern formatter.
|
|
6
|
+
# @api private
|
|
7
|
+
module Format
|
|
8
|
+
DEFAULT_PATTERNS = ["", "G", "g", "d"].freeze
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def default(year, month, day, lang)
|
|
13
|
+
lang = Lang.coerce(lang)
|
|
14
|
+
if lang == :ne
|
|
15
|
+
"#{Names::YEAR_NE[year - CalendarTable::MIN_YEAR]}/#{Names::TWO_DIGIT_NE[month]}/#{Names::TWO_DIGIT_NE[day]}"
|
|
16
|
+
else
|
|
17
|
+
"#{Names::YEAR_EN[year - CalendarTable::MIN_YEAR]}/#{Names::TWO_DIGIT_EN[month]}/#{Names::TWO_DIGIT_EN[day]}"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def iso8601(year, month, day, lang)
|
|
22
|
+
lang = Lang.coerce(lang)
|
|
23
|
+
if lang == :ne
|
|
24
|
+
"#{Names::YEAR_NE[year - CalendarTable::MIN_YEAR]}-#{Names::TWO_DIGIT_NE[month]}-#{Names::TWO_DIGIT_NE[day]}"
|
|
25
|
+
else
|
|
26
|
+
"#{Names::YEAR_EN[year - CalendarTable::MIN_YEAR]}-#{Names::TWO_DIGIT_EN[month]}-#{Names::TWO_DIGIT_EN[day]}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def short(year, month, day, order:, separator:, pad:, lang:)
|
|
31
|
+
lang = Lang.coerce(lang)
|
|
32
|
+
fields = DateOrder.fields(order)
|
|
33
|
+
separator = Separator.character(separator)
|
|
34
|
+
values = {
|
|
35
|
+
year: Names.digits(year, lang, width: 4),
|
|
36
|
+
month: Names.digits(month, lang, width: pad ? 2 : 0),
|
|
37
|
+
day: Names.digits(day, lang, width: pad ? 2 : 0)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fields.map { |field| values.fetch(field) }.join(separator)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def long(year, month, day, weekday_number:, include_year:, pad:, lang:)
|
|
44
|
+
lang = Lang.coerce(lang)
|
|
45
|
+
if lang == :ne
|
|
46
|
+
day_text = pad ? Names::TWO_DIGIT_NE[day] : Names::NUMBER_NE[day]
|
|
47
|
+
year_text = Names::YEAR_NE[year - CalendarTable::MIN_YEAR]
|
|
48
|
+
month_text = Names::MONTH_NE[month]
|
|
49
|
+
prefix = weekday_number.nil? ? "" : "#{Names::WEEKDAY_NE[weekday_number]}, "
|
|
50
|
+
else
|
|
51
|
+
day_text = pad ? Names::TWO_DIGIT_EN[day] : Names::NUMBER_EN[day]
|
|
52
|
+
year_text = Names::YEAR_EN[year - CalendarTable::MIN_YEAR]
|
|
53
|
+
month_text = Names::MONTH_EN[month]
|
|
54
|
+
prefix = weekday_number.nil? ? "" : "#{Names::WEEKDAY_EN[weekday_number]}, "
|
|
55
|
+
end
|
|
56
|
+
suffix = include_year ? ", #{year_text}" : ""
|
|
57
|
+
|
|
58
|
+
"#{prefix}#{month_text} #{day_text}#{suffix}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def pattern(pattern, year:, month:, day:, lang:)
|
|
62
|
+
raise TypeError, "format pattern must be a String, got #{pattern.class}" unless pattern.is_a?(String)
|
|
63
|
+
|
|
64
|
+
lang = Lang.coerce(lang)
|
|
65
|
+
return default(year, month, day, lang) if DEFAULT_PATTERNS.include?(pattern)
|
|
66
|
+
return long(year, month, day, weekday_number: nil, include_year: true, pad: true, lang: lang) if pattern == "D"
|
|
67
|
+
return iso8601(year, month, day, lang) if pattern == "s"
|
|
68
|
+
|
|
69
|
+
tokens = {
|
|
70
|
+
year: Names.digits(year, lang, width: 4),
|
|
71
|
+
year2: Names.digits(year % 100, lang, width: 2),
|
|
72
|
+
month_name: Names.month_name(month, lang),
|
|
73
|
+
month_short: Names.month_short(month, lang),
|
|
74
|
+
month_pad: Names.digits(month, lang, width: 2),
|
|
75
|
+
month: Names.digits(month, lang),
|
|
76
|
+
day_pad: Names.digits(day, lang, width: 2),
|
|
77
|
+
day: Names.digits(day, lang)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
out = +""
|
|
81
|
+
chars = pattern.chars
|
|
82
|
+
index = 0
|
|
83
|
+
while index < chars.length
|
|
84
|
+
char = chars[index]
|
|
85
|
+
if char == "\\"
|
|
86
|
+
# A trailing backslash has nothing to escape, so it is written literally.
|
|
87
|
+
out << ((index + 1 < chars.length) ? chars[index + 1] : "\\")
|
|
88
|
+
index += 2
|
|
89
|
+
next
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
if char == "'"
|
|
93
|
+
closing = ((index + 1)...chars.length).find { |position| chars[position] == "'" }
|
|
94
|
+
if closing
|
|
95
|
+
out << chars[(index + 1)...closing].join
|
|
96
|
+
index = closing + 1
|
|
97
|
+
else
|
|
98
|
+
out << chars[(index + 1)..].join
|
|
99
|
+
break
|
|
100
|
+
end
|
|
101
|
+
next
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
if char == "y"
|
|
105
|
+
finish = run_end(chars, index, "y")
|
|
106
|
+
out << ((finish - index >= 4) ? tokens[:year] : tokens[:year2])
|
|
107
|
+
index = finish
|
|
108
|
+
next
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
if char == "M"
|
|
112
|
+
finish = run_end(chars, index, "M")
|
|
113
|
+
length = finish - index
|
|
114
|
+
value = if length >= 4
|
|
115
|
+
tokens[:month_name]
|
|
116
|
+
elsif length == 3
|
|
117
|
+
tokens[:month_short]
|
|
118
|
+
elsif length == 2
|
|
119
|
+
tokens[:month_pad]
|
|
120
|
+
else
|
|
121
|
+
tokens[:month]
|
|
122
|
+
end
|
|
123
|
+
out << value
|
|
124
|
+
index = finish
|
|
125
|
+
next
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
if char == "d"
|
|
129
|
+
finish = run_end(chars, index, "d")
|
|
130
|
+
out << ((finish - index == 2) ? tokens[:day_pad] : tokens[:day])
|
|
131
|
+
index = finish
|
|
132
|
+
next
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
out << char
|
|
136
|
+
index += 1
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
out
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def run_end(chars, index, char)
|
|
143
|
+
finish = index + 1
|
|
144
|
+
finish += 1 while finish < chars.length && chars[finish] == char
|
|
145
|
+
finish
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NepDate
|
|
4
|
+
# The field order of short and range formatting (FORMATTING.md section 3). Use the symbols
|
|
5
|
+
# +:ymd+, +:ydm+, +:myd+, +:mdy+, +:dym+ or +:dmy+.
|
|
6
|
+
module DateOrder
|
|
7
|
+
ORDER = {
|
|
8
|
+
ymd: [:year, :month, :day],
|
|
9
|
+
ydm: [:year, :day, :month],
|
|
10
|
+
myd: [:month, :year, :day],
|
|
11
|
+
mdy: [:month, :day, :year],
|
|
12
|
+
dym: [:day, :year, :month],
|
|
13
|
+
dmy: [:day, :month, :year]
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
ALL = ORDER.keys.freeze
|
|
17
|
+
|
|
18
|
+
def self.coerce(value)
|
|
19
|
+
symbol = value.to_s.downcase.to_sym
|
|
20
|
+
return symbol if ORDER.key?(symbol)
|
|
21
|
+
|
|
22
|
+
raise ArgumentError, "date order must be one of #{ALL.join(", ")}, got #{value.inspect}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.fields(value)
|
|
26
|
+
ORDER.fetch(coerce(value))
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The separator of short and range formatting (FORMATTING.md section 3). Use +:slash+,
|
|
31
|
+
# +:backslash+, +:dot+, +:underscore+, +:dash+ or +:space+, or pass the character itself.
|
|
32
|
+
module Separator
|
|
33
|
+
CHARACTERS = {
|
|
34
|
+
slash: "/",
|
|
35
|
+
backslash: "\\",
|
|
36
|
+
dot: ".",
|
|
37
|
+
underscore: "_",
|
|
38
|
+
dash: "-",
|
|
39
|
+
space: " "
|
|
40
|
+
}.freeze
|
|
41
|
+
|
|
42
|
+
ALL = CHARACTERS.keys.freeze
|
|
43
|
+
BY_CHARACTER = CHARACTERS.invert.freeze
|
|
44
|
+
|
|
45
|
+
def self.coerce(value)
|
|
46
|
+
return BY_CHARACTER.fetch(value) if CHARACTERS.value?(value)
|
|
47
|
+
|
|
48
|
+
symbol = value.to_s.downcase.to_sym
|
|
49
|
+
return symbol if CHARACTERS.key?(symbol)
|
|
50
|
+
|
|
51
|
+
raise ArgumentError, "separator must be one of #{ALL.join(", ")}, got #{value.inspect}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.character(value)
|
|
55
|
+
return value if CHARACTERS.value?(value)
|
|
56
|
+
|
|
57
|
+
CHARACTERS.fetch(coerce(value))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.name_for(character)
|
|
61
|
+
BY_CHARACTER.fetch(character)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
data/lib/nepdate/lang.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NepDate
|
|
4
|
+
# The output language: +:en+ for English, +:ne+ for Nepali. Symbols are the Ruby spelling of
|
|
5
|
+
# the C# +Lang+ option used by the other ports.
|
|
6
|
+
module Lang
|
|
7
|
+
EN = :en
|
|
8
|
+
NE = :ne
|
|
9
|
+
|
|
10
|
+
def self.coerce(value)
|
|
11
|
+
return value if value == EN || value == NE
|
|
12
|
+
|
|
13
|
+
case value
|
|
14
|
+
when "en" then EN
|
|
15
|
+
when "ne" then NE
|
|
16
|
+
else
|
|
17
|
+
raise ArgumentError, "language must be :en or :ne, got #{value.inspect}"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|