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,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NepDate
|
|
4
|
+
# The deterministic human-input parser of PARSING.md section 2. It tries the strict grammar
|
|
5
|
+
# first, then a closed token grammar with a fixed month-name list. It never guesses.
|
|
6
|
+
# @api private
|
|
7
|
+
module Lenient
|
|
8
|
+
SEPARATORS = ["-", "/", "\\", "_", " ", "|", "।", ","].freeze
|
|
9
|
+
MAX_DIGITS = 4
|
|
10
|
+
KELVIN = "\u212A"
|
|
11
|
+
|
|
12
|
+
DEVA_DIGITS = {
|
|
13
|
+
"०" => "0", "१" => "1", "२" => "2", "३" => "3", "४" => "4",
|
|
14
|
+
"५" => "5", "६" => "6", "७" => "7", "८" => "8", "९" => "9"
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
ERA_MARKERS = ["bs", "b.s.", "b.s", "vs", "v.s.", "v.s", "बि.सं.", "वि.सं.", "बि.सं", "वि.सं"].freeze
|
|
18
|
+
IGNORED_WORDS = ["गते", "मिति"].freeze
|
|
19
|
+
|
|
20
|
+
MONTH_WORDS = {
|
|
21
|
+
"baishakh" => 1, "baisakh" => 1, "baishak" => 1, "vaishakh" => 1, "vaisakh" => 1, "bai" => 1,
|
|
22
|
+
"बैशाख" => 1, "वैशाख" => 1, "बैसाख" => 1,
|
|
23
|
+
"jestha" => 2, "jeth" => 2, "jyestha" => 2, "jes" => 2,
|
|
24
|
+
"जेठ" => 2, "जेष्ठ" => 2, "ज्येष्ठ" => 2,
|
|
25
|
+
"ashad" => 3, "ashadh" => 3, "asar" => 3, "asadh" => 3, "asa" => 3,
|
|
26
|
+
"असार" => 3, "आषाढ" => 3, "असाढ" => 3,
|
|
27
|
+
"shrawan" => 4, "shravan" => 4, "srawan" => 4, "sawan" => 4, "saun" => 4, "shr" => 4,
|
|
28
|
+
"साउन" => 4, "श्रावण" => 4, "सावन" => 4,
|
|
29
|
+
"bhadra" => 5, "bhadau" => 5, "bha" => 5,
|
|
30
|
+
"भदौ" => 5, "भाद्र" => 5, "भाद्रपद" => 5,
|
|
31
|
+
"ashoj" => 6, "asoj" => 6, "ashwin" => 6, "aso" => 6,
|
|
32
|
+
"असोज" => 6, "आश्विन" => 6, "असौज" => 6,
|
|
33
|
+
"kartik" => 7, "kattik" => 7, "kar" => 7,
|
|
34
|
+
"कार्तिक" => 7, "कात्तिक" => 7,
|
|
35
|
+
"mangsir" => 8, "mansir" => 8, "margashirsha" => 8, "man" => 8,
|
|
36
|
+
"मंसिर" => 8, "मङ्सिर" => 8, "मार्गशीर्ष" => 8,
|
|
37
|
+
"poush" => 9, "paush" => 9, "push" => 9, "pus" => 9, "pou" => 9,
|
|
38
|
+
"पुष" => 9, "पौष" => 9, "पुस" => 9,
|
|
39
|
+
"magh" => 10, "mag" => 10, "माघ" => 10,
|
|
40
|
+
"falgun" => 11, "phalgun" => 11, "fagun" => 11, "fal" => 11,
|
|
41
|
+
"फागुन" => 11, "फाल्गुन" => 11,
|
|
42
|
+
"chaitra" => 12, "chait" => 12, "cha" => 12,
|
|
43
|
+
"चैत" => 12, "चैत्र" => 12
|
|
44
|
+
}.freeze
|
|
45
|
+
|
|
46
|
+
module_function
|
|
47
|
+
|
|
48
|
+
def parse(text)
|
|
49
|
+
begin
|
|
50
|
+
strict = Parse.strict(text)
|
|
51
|
+
Calendar.validate(*strict)
|
|
52
|
+
return strict
|
|
53
|
+
rescue Error
|
|
54
|
+
# The strict grammar or the date it reads is unusable: try the token grammar.
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
normalized = text.each_char.map { |char| DEVA_DIGITS.fetch(char, char) }.join
|
|
58
|
+
numbers, digit_counts, months, unknown = tokens(normalized)
|
|
59
|
+
|
|
60
|
+
if unknown
|
|
61
|
+
raise ParseError.new(ErrorKind::UNRECOGNIZED, "unknown word")
|
|
62
|
+
end
|
|
63
|
+
if months.length >= 2
|
|
64
|
+
raise ParseError.new(ErrorKind::UNRECOGNIZED, "more than one month name")
|
|
65
|
+
end
|
|
66
|
+
if months.length == 1 && numbers.length == 2
|
|
67
|
+
return month_form(months.first, numbers, digit_counts)
|
|
68
|
+
end
|
|
69
|
+
if months.empty? && numbers.length == 3
|
|
70
|
+
return numeric_form(numbers, digit_counts)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
raise ParseError.new(ErrorKind::UNRECOGNIZED, "unrecognized token count")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# [numbers, digit counts, month numbers, saw an unknown word]
|
|
77
|
+
def tokens(text)
|
|
78
|
+
numbers = []
|
|
79
|
+
digit_counts = []
|
|
80
|
+
months = []
|
|
81
|
+
unknown = false
|
|
82
|
+
|
|
83
|
+
index = 0
|
|
84
|
+
chars = text.chars
|
|
85
|
+
while index < chars.length
|
|
86
|
+
char = chars[index]
|
|
87
|
+
if SEPARATORS.include?(char)
|
|
88
|
+
index += 1
|
|
89
|
+
next
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
chunk = +""
|
|
93
|
+
while index < chars.length
|
|
94
|
+
char = chars[index]
|
|
95
|
+
break if SEPARATORS.include?(char)
|
|
96
|
+
|
|
97
|
+
chunk << char
|
|
98
|
+
index += 1
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
if numeric?(chunk)
|
|
102
|
+
chunk.split(".").each do |part|
|
|
103
|
+
next if part.empty?
|
|
104
|
+
if part.length > MAX_DIGITS
|
|
105
|
+
raise ParseError.new(ErrorKind::NUMBER_TOO_LONG, "a number has more than 4 digits")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
numbers << part.to_i
|
|
109
|
+
digit_counts << part.length
|
|
110
|
+
end
|
|
111
|
+
next
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
value = word_value(chunk)
|
|
115
|
+
if value.positive?
|
|
116
|
+
months << value
|
|
117
|
+
elsif value.negative?
|
|
118
|
+
unknown = true
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
[numbers, digit_counts, months, unknown]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def numeric?(chunk)
|
|
126
|
+
chunk.each_char.all? { |char| char.between?("0", "9") || char == "." }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# A month number, 0 for a dropped marker, or -1 for an unknown word.
|
|
130
|
+
def word_value(token)
|
|
131
|
+
folded = fold(token)
|
|
132
|
+
month = MONTH_WORDS[folded]
|
|
133
|
+
return month if month
|
|
134
|
+
return 0 if ERA_MARKERS.include?(folded) || IGNORED_WORDS.include?(folded)
|
|
135
|
+
|
|
136
|
+
-1
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Exactly ASCII A-Z plus the Kelvin sign fold; no other character changes (PARSING.md).
|
|
140
|
+
def fold(token)
|
|
141
|
+
token.each_char.map { |char|
|
|
142
|
+
if char.between?("A", "Z")
|
|
143
|
+
(char.ord + 32).chr(Encoding::UTF_8)
|
|
144
|
+
elsif char == KELVIN
|
|
145
|
+
"k"
|
|
146
|
+
else
|
|
147
|
+
char
|
|
148
|
+
end
|
|
149
|
+
}.join
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def month_form(month, numbers, digit_counts)
|
|
153
|
+
four = []
|
|
154
|
+
digit_counts.each_with_index { |count, index| four << index if count == MAX_DIGITS }
|
|
155
|
+
unless four.length == 1
|
|
156
|
+
raise ParseError.new(ErrorKind::AMBIGUOUS, "no single four-digit year")
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
year_index = four.first
|
|
160
|
+
other_index = 1 - year_index
|
|
161
|
+
if digit_counts[other_index] > 2
|
|
162
|
+
raise ParseError.new(ErrorKind::AMBIGUOUS, "the day has more than two digits")
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
[numbers[year_index], month, numbers[other_index]]
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def numeric_form(numbers, digit_counts)
|
|
169
|
+
return [numbers[0], numbers[1], numbers[2]] if digit_counts[0] == MAX_DIGITS
|
|
170
|
+
return [numbers[2], numbers[1], numbers[0]] if digit_counts[2] == MAX_DIGITS
|
|
171
|
+
|
|
172
|
+
raise ParseError.new(ErrorKind::AMBIGUOUS, "no four-digit year first or last")
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NepDate
|
|
4
|
+
# A Bikram Sambat month, 1 = Baishakh through 12 = Chaitra. Ruby has no enum, so this is a
|
|
5
|
+
# small immutable value with the twelve members as constants.
|
|
6
|
+
class Month
|
|
7
|
+
include Comparable
|
|
8
|
+
|
|
9
|
+
NAMES_EN = Names::MONTH_EN
|
|
10
|
+
NAMES_NE = Names::MONTH_NE
|
|
11
|
+
SHORT_EN = Names::MONTH_SHORT_EN
|
|
12
|
+
|
|
13
|
+
attr_reader :number
|
|
14
|
+
|
|
15
|
+
def initialize(number)
|
|
16
|
+
number = Integer(number)
|
|
17
|
+
unless (1..12).cover?(number)
|
|
18
|
+
raise InvalidMonthError, "month #{number} is outside 1..12"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
@number = number
|
|
22
|
+
freeze
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.from(value)
|
|
26
|
+
return value if value.is_a?(Month)
|
|
27
|
+
|
|
28
|
+
new(value)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
BAISHAKH = new(1)
|
|
32
|
+
JESTHA = new(2)
|
|
33
|
+
ASHAD = new(3)
|
|
34
|
+
SHRAWAN = new(4)
|
|
35
|
+
BHADRA = new(5)
|
|
36
|
+
ASHOJ = new(6)
|
|
37
|
+
KARTIK = new(7)
|
|
38
|
+
MANGSIR = new(8)
|
|
39
|
+
POUSH = new(9)
|
|
40
|
+
MAGH = new(10)
|
|
41
|
+
FALGUN = new(11)
|
|
42
|
+
CHAITRA = new(12)
|
|
43
|
+
|
|
44
|
+
ALL = [
|
|
45
|
+
BAISHAKH, JESTHA, ASHAD, SHRAWAN, BHADRA, ASHOJ,
|
|
46
|
+
KARTIK, MANGSIR, POUSH, MAGH, FALGUN, CHAITRA
|
|
47
|
+
].freeze
|
|
48
|
+
|
|
49
|
+
def label(lang: :en)
|
|
50
|
+
Names.month_name(number, Lang.coerce(lang))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def short_label(lang: :en)
|
|
54
|
+
Names.month_short(number, Lang.coerce(lang))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def to_i
|
|
58
|
+
number
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def to_s
|
|
62
|
+
label
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def <=>(other)
|
|
66
|
+
return nil unless other.is_a?(Month)
|
|
67
|
+
|
|
68
|
+
number <=> other.number
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def ==(other)
|
|
72
|
+
other.is_a?(Month) && number == other.number
|
|
73
|
+
end
|
|
74
|
+
alias_method :eql?, :==
|
|
75
|
+
|
|
76
|
+
def hash
|
|
77
|
+
[Month, number].hash
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def inspect
|
|
81
|
+
"#<NepDate::Month #{number} #{label}>"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NepDate
|
|
4
|
+
# What whole-month arithmetic does when the day does not exist in the target month (D-02).
|
|
5
|
+
# +:clamp+ keeps the last day of the target month; +:spill+ moves the excess into the next
|
|
6
|
+
# month. There is no fractional month arithmetic.
|
|
7
|
+
module MonthOverflow
|
|
8
|
+
CLAMP = :clamp
|
|
9
|
+
SPILL = :spill
|
|
10
|
+
ALL = [CLAMP, SPILL].freeze
|
|
11
|
+
|
|
12
|
+
def self.coerce(value)
|
|
13
|
+
return value if value == CLAMP || value == SPILL
|
|
14
|
+
|
|
15
|
+
symbol = value.to_s.downcase.to_sym
|
|
16
|
+
return symbol if ALL.include?(symbol)
|
|
17
|
+
|
|
18
|
+
raise ArgumentError, "overflow must be :clamp or :spill, got #{value.inspect}"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NepDate
|
|
4
|
+
# The month, weekday and digit names of FORMATTING.md section 1.
|
|
5
|
+
# @api private
|
|
6
|
+
module Names
|
|
7
|
+
MONTH_EN = [
|
|
8
|
+
"", "Baishakh", "Jestha", "Ashad", "Shrawan", "Bhadra", "Ashoj",
|
|
9
|
+
"Kartik", "Mangsir", "Poush", "Magh", "Falgun", "Chaitra"
|
|
10
|
+
].freeze
|
|
11
|
+
|
|
12
|
+
MONTH_NE = [
|
|
13
|
+
"", "बैशाख", "जेठ", "असार", "साउन", "भदौ", "असोज",
|
|
14
|
+
"कार्तिक", "मंसिर", "पुष", "माघ", "फागुन", "चैत"
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
# D-04: Ashad and Ashoj are Asa and Aso, not the C# "Ash" shared by both.
|
|
18
|
+
MONTH_SHORT_EN = [
|
|
19
|
+
"", "Bai", "Jes", "Asa", "Shr", "Bha", "Aso",
|
|
20
|
+
"Kar", "Man", "Pou", "Mag", "Fal", "Cha"
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
WEEKDAY_EN = %w[Sunday Monday Tuesday Wednesday Thursday Friday Saturday].freeze
|
|
24
|
+
WEEKDAY_NE = %w[आइतवार सोमवार मङ्गलवार बुधवार बिहिवार शुक्रवार शनिवार].freeze
|
|
25
|
+
WEEKDAY_SHORT_EN = %w[Sun Mon Tue Wed Thu Fri Sat].freeze
|
|
26
|
+
WEEKDAY_SHORT_NE = %w[आइत सोम मङ्गल बुध बिहि शुक्र शनि].freeze
|
|
27
|
+
|
|
28
|
+
DEVANAGARI_DIGITS = %w[० १ २ ३ ४ ५ ६ ७ ८ ९].freeze
|
|
29
|
+
ASCII_DIGITS = %w[0 1 2 3 4 5 6 7 8 9].freeze
|
|
30
|
+
|
|
31
|
+
def self.month_name(number, lang = :en)
|
|
32
|
+
(lang == :ne) ? MONTH_NE.fetch(number) : MONTH_EN.fetch(number)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.month_short(number, lang = :en)
|
|
36
|
+
return MONTH_NE.fetch(number) if lang == :ne
|
|
37
|
+
|
|
38
|
+
MONTH_SHORT_EN.fetch(number)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.weekday_name(number, lang = :en)
|
|
42
|
+
(lang == :ne) ? WEEKDAY_NE.fetch(number) : WEEKDAY_EN.fetch(number)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.weekday_short(number, lang = :en)
|
|
46
|
+
(lang == :ne) ? WEEKDAY_SHORT_NE.fetch(number) : WEEKDAY_SHORT_EN.fetch(number)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Replaces every ASCII digit with its Devanagari counterpart and leaves every other character
|
|
50
|
+
# alone (FORMATTING.md section 1).
|
|
51
|
+
def self.nepali_digits(text)
|
|
52
|
+
text.tr("0123456789", "०१२३४५६७८९")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.digits(number, lang = :en, width: 0)
|
|
56
|
+
if width == 4 && number >= CalendarTable::MIN_YEAR && number <= CalendarTable::MAX_YEAR
|
|
57
|
+
return (lang == :ne) ? YEAR_NE[number - CalendarTable::MIN_YEAR] : YEAR_EN[number - CalendarTable::MIN_YEAR]
|
|
58
|
+
end
|
|
59
|
+
if width == 2 && number >= 0 && number <= 99
|
|
60
|
+
return (lang == :ne) ? TWO_DIGIT_NE[number] : TWO_DIGIT_EN[number]
|
|
61
|
+
end
|
|
62
|
+
if width.zero? && number >= 0 && number <= 99
|
|
63
|
+
return (lang == :ne) ? NUMBER_NE[number] : NUMBER_EN[number]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
text = number.to_s
|
|
67
|
+
text = text.rjust(width, "0") if width.positive?
|
|
68
|
+
(lang == :ne) ? nepali_digits(text) : text
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# The short and long forms of every digit string the formatters need, built once at load so a
|
|
72
|
+
# formatting call only reads a String. Years are 1901 through 2199; the two-digit and plain
|
|
73
|
+
# tables cover dates, months and the two-digit year.
|
|
74
|
+
NUMBER_EN = (0..99).map(&:to_s).freeze
|
|
75
|
+
NUMBER_NE = NUMBER_EN.map { |text| nepali_digits(text) }.freeze
|
|
76
|
+
TWO_DIGIT_EN = (0..99).map { |number| number.to_s.rjust(2, "0") }.freeze
|
|
77
|
+
TWO_DIGIT_NE = TWO_DIGIT_EN.map { |text| nepali_digits(text) }.freeze
|
|
78
|
+
YEAR_EN = (CalendarTable::MIN_YEAR..CalendarTable::MAX_YEAR).map(&:to_s).freeze
|
|
79
|
+
YEAR_NE = YEAR_EN.map { |text| nepali_digits(text) }.freeze
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NepDate
|
|
4
|
+
# The strict parser of PARSING.md section 1: year, month, day, with ASCII and Devanagari
|
|
5
|
+
# digits mixed freely. The scanner walks UTF-8 bytes; the only multi-byte characters it accepts
|
|
6
|
+
# are the ten Devanagari digits and the danda separator.
|
|
7
|
+
# @api private
|
|
8
|
+
module Parse
|
|
9
|
+
GROUP_COUNT = 3
|
|
10
|
+
MAX_DIGITS = 4
|
|
11
|
+
SEPARATOR_BYTES = {
|
|
12
|
+
0x2D => true, 0x2F => true, 0x2E => true, 0x5F => true,
|
|
13
|
+
0x5C => true, 0x20 => true, 0x7C => true
|
|
14
|
+
}.freeze
|
|
15
|
+
DANDA = "।"
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def strict(text)
|
|
20
|
+
raise TypeError, "date text must be a String, got #{text.class}" unless text.is_a?(String)
|
|
21
|
+
|
|
22
|
+
fast = fast_ascii(text)
|
|
23
|
+
return fast if fast
|
|
24
|
+
|
|
25
|
+
groups = [0, 0, 0]
|
|
26
|
+
count = 0
|
|
27
|
+
in_group = false
|
|
28
|
+
digits = 0
|
|
29
|
+
index = 0
|
|
30
|
+
length = text.bytesize
|
|
31
|
+
|
|
32
|
+
while index < length
|
|
33
|
+
byte = text.getbyte(index)
|
|
34
|
+
digit = nil
|
|
35
|
+
width = 1
|
|
36
|
+
|
|
37
|
+
if byte.between?(0x30, 0x39)
|
|
38
|
+
digit = byte - 0x30
|
|
39
|
+
elsif byte < 0x80
|
|
40
|
+
invalid_character(text, index) unless SEPARATOR_BYTES.key?(byte)
|
|
41
|
+
elsif byte == 0xE0
|
|
42
|
+
second = text.getbyte(index + 1)
|
|
43
|
+
third = text.getbyte(index + 2)
|
|
44
|
+
if second == 0xA5 && third
|
|
45
|
+
if third.between?(0xA6, 0xAF)
|
|
46
|
+
digit = third - 0xA6
|
|
47
|
+
width = 3
|
|
48
|
+
elsif third == 0xA4
|
|
49
|
+
width = 3
|
|
50
|
+
else
|
|
51
|
+
invalid_character(text, index)
|
|
52
|
+
end
|
|
53
|
+
else
|
|
54
|
+
invalid_character(text, index)
|
|
55
|
+
end
|
|
56
|
+
else
|
|
57
|
+
invalid_character(text, index)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
if digit
|
|
61
|
+
if !in_group
|
|
62
|
+
if count == GROUP_COUNT
|
|
63
|
+
raise ParseError.new(ErrorKind::WRONG_GROUP_COUNT, "more than three groups")
|
|
64
|
+
end
|
|
65
|
+
count += 1
|
|
66
|
+
in_group = true
|
|
67
|
+
digits = 0
|
|
68
|
+
end
|
|
69
|
+
digits += 1
|
|
70
|
+
if digits > MAX_DIGITS
|
|
71
|
+
raise ParseError.new(ErrorKind::NUMBER_TOO_LONG, "a group has more than 4 digits")
|
|
72
|
+
end
|
|
73
|
+
groups[count - 1] = groups[count - 1] * 10 + digit
|
|
74
|
+
index += width
|
|
75
|
+
next
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
in_group = false
|
|
79
|
+
index += width
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
if count < GROUP_COUNT
|
|
83
|
+
raise ParseError.new(ErrorKind::WRONG_GROUP_COUNT, "fewer than three groups")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
groups
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The common YYYY/MM/DD form (and every other one-byte separator) without the byte scanner.
|
|
90
|
+
# Returns nil when the text is not exactly four, two and two ASCII digits, so the scanner can
|
|
91
|
+
# report the precise error or accept Devanagari digits.
|
|
92
|
+
def fast_ascii(text)
|
|
93
|
+
return nil unless text.bytesize == 10
|
|
94
|
+
|
|
95
|
+
first = text.getbyte(0)
|
|
96
|
+
second = text.getbyte(1)
|
|
97
|
+
third = text.getbyte(2)
|
|
98
|
+
fourth = text.getbyte(3)
|
|
99
|
+
fifth = text.getbyte(5)
|
|
100
|
+
sixth = text.getbyte(6)
|
|
101
|
+
eighth = text.getbyte(8)
|
|
102
|
+
ninth = text.getbyte(9)
|
|
103
|
+
return nil unless first.between?(0x30, 0x39) &&
|
|
104
|
+
second.between?(0x30, 0x39) &&
|
|
105
|
+
third.between?(0x30, 0x39) &&
|
|
106
|
+
fourth.between?(0x30, 0x39) &&
|
|
107
|
+
fifth.between?(0x30, 0x39) &&
|
|
108
|
+
sixth.between?(0x30, 0x39) &&
|
|
109
|
+
eighth.between?(0x30, 0x39) &&
|
|
110
|
+
ninth.between?(0x30, 0x39)
|
|
111
|
+
return nil unless SEPARATOR_BYTES.key?(text.getbyte(4)) && SEPARATOR_BYTES.key?(text.getbyte(7))
|
|
112
|
+
|
|
113
|
+
[
|
|
114
|
+
(first - 0x30) * 1000 + (second - 0x30) * 100 + (third - 0x30) * 10 + (fourth - 0x30),
|
|
115
|
+
(fifth - 0x30) * 10 + (sixth - 0x30),
|
|
116
|
+
(eighth - 0x30) * 10 + (ninth - 0x30)
|
|
117
|
+
]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def invalid_character(text, index)
|
|
121
|
+
char = text.byteslice(index, 4).to_s.each_char.first
|
|
122
|
+
raise ParseError.new(ErrorKind::INVALID_CHARACTER, "invalid character #{char.inspect}")
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NepDate
|
|
4
|
+
# A fiscal quarter: Q1 Shrawan-Ashoj through Q4 Baishakh-Ashad of the following year.
|
|
5
|
+
class Quarter
|
|
6
|
+
include Comparable
|
|
7
|
+
|
|
8
|
+
NAMES = {1 => :q1, 2 => :q2, 3 => :q3, 4 => :q4}.freeze
|
|
9
|
+
LABELS = {1 => "Q1", 2 => "Q2", 3 => "Q3", 4 => "Q4"}.freeze
|
|
10
|
+
|
|
11
|
+
attr_reader :number
|
|
12
|
+
|
|
13
|
+
def initialize(number)
|
|
14
|
+
number = Integer(number)
|
|
15
|
+
unless (1..4).cover?(number)
|
|
16
|
+
raise ArgumentError, "quarter #{number} is outside 1..4"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
@number = number
|
|
20
|
+
freeze
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.from(value)
|
|
24
|
+
return value if value.is_a?(Quarter)
|
|
25
|
+
|
|
26
|
+
case value
|
|
27
|
+
when :q1, "q1", 1 then Q1
|
|
28
|
+
when :q2, "q2", 2 then Q2
|
|
29
|
+
when :q3, "q3", 3 then Q3
|
|
30
|
+
when :q4, "q4", 4 then Q4
|
|
31
|
+
else
|
|
32
|
+
raise ArgumentError, "quarter must be 1..4 or :q1..:q4, got #{value.inspect}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
Q1 = new(1)
|
|
37
|
+
Q2 = new(2)
|
|
38
|
+
Q3 = new(3)
|
|
39
|
+
Q4 = new(4)
|
|
40
|
+
|
|
41
|
+
ALL = [Q1, Q2, Q3, Q4].freeze
|
|
42
|
+
|
|
43
|
+
def name
|
|
44
|
+
NAMES.fetch(number)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def label
|
|
48
|
+
LABELS.fetch(number)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def to_i
|
|
52
|
+
number
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def to_s
|
|
56
|
+
label
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def <=>(other)
|
|
60
|
+
return nil unless other.is_a?(Quarter)
|
|
61
|
+
|
|
62
|
+
number <=> other.number
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def ==(other)
|
|
66
|
+
other.is_a?(Quarter) && number == other.number
|
|
67
|
+
end
|
|
68
|
+
alias_method :eql?, :==
|
|
69
|
+
|
|
70
|
+
def hash
|
|
71
|
+
[Quarter, number].hash
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def inspect
|
|
75
|
+
"#<NepDate::Quarter #{label}>"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NepDate
|
|
4
|
+
# A weekday with Sunday = 0, the numbering of ALGORITHM.md section 7 and the shared vectors.
|
|
5
|
+
class Weekday
|
|
6
|
+
include Comparable
|
|
7
|
+
|
|
8
|
+
attr_reader :number
|
|
9
|
+
|
|
10
|
+
def initialize(number)
|
|
11
|
+
number = Integer(number)
|
|
12
|
+
unless (0..6).cover?(number)
|
|
13
|
+
raise ArgumentError, "weekday #{number} is outside 0..6"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
@number = number
|
|
17
|
+
freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.from(value)
|
|
21
|
+
return value if value.is_a?(Weekday)
|
|
22
|
+
|
|
23
|
+
new(value)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
SUNDAY = new(0)
|
|
27
|
+
MONDAY = new(1)
|
|
28
|
+
TUESDAY = new(2)
|
|
29
|
+
WEDNESDAY = new(3)
|
|
30
|
+
THURSDAY = new(4)
|
|
31
|
+
FRIDAY = new(5)
|
|
32
|
+
SATURDAY = new(6)
|
|
33
|
+
|
|
34
|
+
ALL = [SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY].freeze
|
|
35
|
+
|
|
36
|
+
def label(lang: :en)
|
|
37
|
+
Names.weekday_name(number, Lang.coerce(lang))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def short_label(lang: :en)
|
|
41
|
+
Names.weekday_short(number, Lang.coerce(lang))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def sunday? = number.zero?
|
|
45
|
+
|
|
46
|
+
def monday? = number == 1
|
|
47
|
+
|
|
48
|
+
def tuesday? = number == 2
|
|
49
|
+
|
|
50
|
+
def wednesday? = number == 3
|
|
51
|
+
|
|
52
|
+
def thursday? = number == 4
|
|
53
|
+
|
|
54
|
+
def friday? = number == 5
|
|
55
|
+
|
|
56
|
+
def saturday? = number == 6
|
|
57
|
+
|
|
58
|
+
def to_i
|
|
59
|
+
number
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def to_s
|
|
63
|
+
label
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def <=>(other)
|
|
67
|
+
return nil unless other.is_a?(Weekday)
|
|
68
|
+
|
|
69
|
+
number <=> other.number
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def ==(other)
|
|
73
|
+
other.is_a?(Weekday) && number == other.number
|
|
74
|
+
end
|
|
75
|
+
alias_method :eql?, :==
|
|
76
|
+
|
|
77
|
+
def hash
|
|
78
|
+
[Weekday, number].hash
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def inspect
|
|
82
|
+
"#<NepDate::Weekday #{number} #{label}>"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
data/lib/nepdate.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "nepdate/version"
|
|
4
|
+
require_relative "nepdate/errors"
|
|
5
|
+
require_relative "nepdate/calendar_table"
|
|
6
|
+
require_relative "nepdate/calendar"
|
|
7
|
+
require_relative "nepdate/names"
|
|
8
|
+
require_relative "nepdate/lang"
|
|
9
|
+
require_relative "nepdate/month"
|
|
10
|
+
require_relative "nepdate/weekday"
|
|
11
|
+
require_relative "nepdate/quarter"
|
|
12
|
+
require_relative "nepdate/format_options"
|
|
13
|
+
require_relative "nepdate/month_overflow"
|
|
14
|
+
require_relative "nepdate/date_diff"
|
|
15
|
+
require_relative "nepdate/parse"
|
|
16
|
+
require_relative "nepdate/lenient"
|
|
17
|
+
require_relative "nepdate/format"
|
|
18
|
+
require_relative "nepdate/date"
|
|
19
|
+
require_relative "nepdate/date_range"
|
|
20
|
+
require_relative "nepdate/fiscal_year"
|
|
21
|
+
|
|
22
|
+
# Bikram Sambat dates for Ruby. The canonical entry point is +NepDate::Date+;
|
|
23
|
+
# +NepDate::Date.parse+ reads a strict date and +NepDate::Date.today+ reads the clock through an
|
|
24
|
+
# explicit zone and an optional injected clock.
|
|
25
|
+
module NepDate
|
|
26
|
+
class << self
|
|
27
|
+
# The date +text+ names, with the strict grammar.
|
|
28
|
+
def parse(text)
|
|
29
|
+
Date.parse(text)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Today in +zone+, with an optional clock callable.
|
|
33
|
+
def today(zone: Date::DEFAULT_ZONE, clock: nil)
|
|
34
|
+
Date.today(zone: zone, clock: clock)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|