timeliness 0.5.1 → 0.5.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8a3c28fd75e1bedf4e7679a8fd71b9484a5daadbf7dceea15d7f54fb7f5304c3
4
- data.tar.gz: 3bc44bf2be43bba84c55ca5eb37f2ba97c8fa4051d10bd30477a307aab1605e9
3
+ metadata.gz: ed9d4a7bedb3b511ea9a907ccbf31b40903dd29fdd4d4c9aa3763e6827d02d2e
4
+ data.tar.gz: 6de9d02d5dfb2e96fb9899e190cf5dfbf75e29fffbe51a7c6b7c41e00fa5190e
5
5
  SHA512:
6
- metadata.gz: 79b6996ca7dce5c043b0dfecf11c2d9cd1016ce5784d134576123f23de9ac8bdc2a4ce0905eda58949d31f03c79e89c37622bdd0e926e70a6e189af265eb5b24
7
- data.tar.gz: 321feb85cf0177e55ed47e5a35026b85610ba8559ae03daca287f5d6bbcc5f1e4d287ae8212267e22049907adef252415b528def599efbaafecdd34bb7d8ad86
6
+ metadata.gz: a06066317124ddf6be084e604fb398824f5ffc4febbc33863ca42110c5cda782ccd5da8f0de11482f3a9aef6e16d33f29272777aa150a77f8eaa432db924124e
7
+ data.tar.gz: b2aba3bd75fe034df0cc867fa0d8e7aa69a895a74891ea2ce9f157ad691e437504553d195fd3a0aaa1c369c52548514f2d8360e62c2683125564ed4a93366374
data/CHANGELOG.rdoc CHANGED
@@ -1,3 +1,9 @@
1
+ = 0.5.2 - 2025-01-31
2
+ * Reduce allocations through on the parse hot path
3
+
4
+ = 0.5.1 - 2025-01-07
5
+ * Make frozen string compatible
6
+
1
7
  = 0.5.0 - 2024-12-02
2
8
  * Reduce allocations through some internal parsing changes
3
9
  * Changed parse method arg handling to simple using keyword args
@@ -13,12 +19,10 @@
13
19
 
14
20
  = 0.4.3 - 2019-06-16
15
21
  * Fixed `Timeliness.ambiguous_date_format` being used in new threads if custom value set
16
- * Moved all config from Timeliness to new Configuration class. Delegated all
17
- old config methods to Timeliness.configuration instance.
22
+ * Moved all config from Timeliness to new Configuration class. Delegated all old config methods to Timeliness.configuration instance.
18
23
 
19
24
  = 0.4.2 - 2019-06-15
20
- * Fixed thread safe issue that forced you to use one of the date format methods e.g. `use_euro_formats`
21
- to initialize the format sets in each new thread. Now a new thread will default to the global default (main thread).
25
+ * Fixed thread safe issue that forced you to use one of the date format methods e.g. `use_euro_formats` to initialize the format sets in each new thread. Now a new thread will default to the global default (main thread).
22
26
  * Add `Timeliness.ambiguous_date_format` config setting (:us or :euro) to control global default for date format sets.
23
27
 
24
28
  = 0.4.1 - 2019-06-11
@@ -27,8 +31,7 @@
27
31
  * Add 'zt' format token to support 'Z' (Zulu time) zone offset i.e. +00:00 or UTC
28
32
 
29
33
  = 0.4.0 - 2019-02-09
30
- * Add threadsafety for use_euro_formats & use_us_formats to allow runtime
31
- switching (andruby, timdiggins)
34
+ * Add threadsafety for use_euro_formats & use_us_formats to allow runtime switching (andruby, timdiggins)
32
35
 
33
36
  = 0.3.10 - 2019-02-06
34
37
  * Fixed file permissions in gem build
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Timeliness
2
4
  class Format
3
5
  include Helpers
@@ -11,7 +13,6 @@ module Timeliness
11
13
  end
12
14
 
13
15
  def compile!
14
- @token_count = 0
15
16
  found_tokens, token_order = [], []
16
17
 
17
18
  format = format_string.dup
@@ -19,20 +20,16 @@ module Timeliness
19
20
 
20
21
  # Substitute tokens with numbered placeholder
21
22
  Definitions.sorted_token_keys.each do |token|
22
- count = 0
23
23
  format.gsub!(token) do
24
24
  token_regexp_str, arg_key = Definitions.format_tokens[token]
25
- token_index = found_tokens.size
26
25
 
27
- if arg_key
26
+ if arg_key && found_tokens.rassoc(arg_key)
28
27
  raise CompilationFailed, "Token '#{token}' was found more than once in format '#{format_string}'. This has unexpected effects should be removed." if count > 0
29
- count += 1
30
-
31
- token_regexp_str = "(#{token_regexp_str})"
32
- @token_count += 1
33
28
  end
29
+
34
30
  found_tokens << [ token_regexp_str, arg_key ]
35
31
 
32
+ token_index = found_tokens.size - 1
36
33
  "%<#{token_index}>"
37
34
  end
38
35
  end
@@ -40,11 +37,18 @@ module Timeliness
40
37
  # Replace placeholders with token regexps
41
38
  format.gsub!(/%<(\d+)>/) do
42
39
  token_regexp_str, arg_key = found_tokens[$1.to_i]
43
- token_order << arg_key
44
- token_regexp_str
40
+
41
+ if arg_key
42
+ token_order << arg_key
43
+ "(#{token_regexp_str})"
44
+ else
45
+ token_regexp_str
46
+ end
45
47
  end
46
48
 
47
- define_process_method(token_order.compact)
49
+ @token_count = token_order.size
50
+
51
+ define_process_method(token_order)
48
52
  @regexp_string = format
49
53
  @regexp = Regexp.new("^(?>#{format})$")
50
54
  self
@@ -31,12 +31,12 @@ module Timeliness
31
31
 
32
32
  def match(string, format_string=nil)
33
33
  format = single_format(format_string) if format_string
34
- match_regexp = format && format.regexp || @regexp
34
+ match_regexp = format ? format.regexp : @regexp
35
35
 
36
- match_regexp.match(string) do |match_data|
37
- captures = match_data.captures # For a multi-format regexp there are lots of nils
38
- index = captures.find_index { |e| !e.nil? } # Find the start of captures for matched format
39
- values = captures.values_at(index..(index+7))
36
+ if (match_data = match_regexp.match(string))
37
+ captures = match_data.captures # For a multi-format regexp there are lots of nils
38
+ index = captures.index { |e| !e.nil? } # Find the start of captures for matched format
39
+ values = captures[index, 8]
40
40
  format ||= @match_indexes[index]
41
41
  format.process(*values)
42
42
  end
@@ -1,10 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Timeliness
2
4
  module Helpers
5
+ # Helper methods used in format component processing. See Definitions.
3
6
 
4
7
  def full_hour(hour, meridian)
5
8
  hour = hour.to_i
6
9
  return hour if meridian.nil?
7
- if meridian.delete('.').downcase == 'am'
10
+
11
+ meridian.delete!('.')
12
+ meridian.downcase!
13
+
14
+ if meridian == 'am'
8
15
  raise(ArgumentError) if hour == 0 || hour > 12
9
16
  hour == 12 ? 0 : hour
10
17
  else
@@ -20,9 +27,9 @@ module Timeliness
20
27
  end
21
28
  year.to_i
22
29
  end
23
-
30
+
24
31
  def month_index(month)
25
- return month.to_i if month.to_i > 0 || /0+/ =~ month
32
+ return month.to_i if month.match?(/\d/)
26
33
  (month.length > 3 ? month_names : abbr_month_names).index { |str| month.casecmp?(str) }
27
34
  end
28
35
 
@@ -39,10 +46,8 @@ module Timeliness
39
46
  end
40
47
 
41
48
  def offset_in_seconds(offset)
42
- sign = offset =~ /^-/ ? -1 : 1
43
- parts = offset.scan(/\d\d/).map {|p| p.to_f }
44
- parts[1] = parts[1].to_f / 60
45
- (parts[0] + parts[1]) * sign * 3600
49
+ offset =~ /^([-+])?(\d{2}):?(\d{2})/
50
+ ($1 == '-' ? -1 : 1) * ($2.to_f * 3600 + $3.to_f * 60)
46
51
  end
47
52
 
48
53
  def i18n_loaded?
@@ -18,19 +18,20 @@ module Timeliness
18
18
 
19
19
  default_values_by_type(time_array, type, options) unless type == :datetime
20
20
 
21
- make_time(time_array[0..7], options[:zone])
21
+ make_time(time_array, options[:zone])
22
22
  rescue NoMethodError => ex
23
23
  raise ex unless ex.message =~ /undefined method `(zone|use_zone|current)' for Time:Class/
24
24
  raise MissingTimezoneSupport, "ActiveSupport timezone support must be loaded to use timezones other than :utc and :local."
25
25
  end
26
26
 
27
27
  def make_time(time_array, zone_option=nil)
28
- return nil unless fast_date_valid_with_fallback(*time_array[0..2])
28
+ return nil unless fast_date_valid_with_fallback(time_array[0], time_array[1], time_array[2])
29
29
 
30
30
  zone, offset = zone_and_offset(time_array[7]) if time_array[7]
31
31
 
32
32
  value = create_time_in_zone(time_array[0..6].compact, zone || zone_option)
33
33
  value = shift_time_to_zone(value, zone_option) if zone
34
+
34
35
  return nil unless value
35
36
 
36
37
  offset ? value + (value.utc_offset - offset) : value
@@ -74,9 +75,12 @@ module Timeliness
74
75
  def default_values_by_type(values, type, options)
75
76
  case type
76
77
  when :date
77
- values[3..7] = nil
78
+ values.fill(nil, 3..7)
78
79
  when :time
79
- values[0..2] = current_date(options)
80
+ current_date = current_date(options)
81
+ values[0] = current_date[0]
82
+ values[1] = current_date[1]
83
+ values[2] = current_date[2]
80
84
  when nil
81
85
  dummy_date = current_date(options)
82
86
  values[0] ||= dummy_date[0]
@@ -1,3 +1,3 @@
1
1
  module Timeliness
2
- VERSION = '0.5.1'.freeze
2
+ VERSION = '0.5.3'.freeze
3
3
  end
@@ -0,0 +1,116 @@
1
+ describe Timeliness::Helpers do
2
+ include Timeliness::Helpers
3
+
4
+ describe "#full_hour" do
5
+ it "should convert a 12-hour clock AM time to 24-hour format correctly" do
6
+ expect(full_hour(12, 'am')).to eq 0
7
+ expect(full_hour(1, 'am')).to eq 1
8
+ expect(full_hour(10, 'am')).to eq 10
9
+ end
10
+
11
+ it "should convert a 12-hour clock PM time to 24-hour format correctly" do
12
+ expect(full_hour(12, 'pm')).to eq 12
13
+ expect(full_hour(1, 'pm')).to eq 13
14
+ expect(full_hour(10, 'pm')).to eq 22
15
+ end
16
+
17
+ it "should raise ArgumentError when given an hour of 0 with AM meridian" do
18
+ expect { full_hour(0, 'am') }.to raise_error(ArgumentError)
19
+ end
20
+
21
+ it "should raise ArgumentError when given an hour greater than 12 with AM meridian" do
22
+ expect { full_hour(13, 'am') }.to raise_error(ArgumentError)
23
+ end
24
+
25
+ it "should handle meridian strings with periods" do
26
+ expect(full_hour(10, 'A.M.')).to eq 10
27
+ expect(full_hour(10, 'P.M.')).to eq 22
28
+ expect(full_hour(12, 'A.M.')).to eq 0
29
+ expect(full_hour(12, 'P.M.')).to eq 12
30
+ end
31
+ end
32
+
33
+ describe "#unambiguous_year" do
34
+ before do
35
+ @original_threshold = Timeliness.configuration.ambiguous_year_threshold
36
+ Timeliness.configuration.ambiguous_year_threshold = 30
37
+ Timecop.freeze(Time.new(2023, 1, 1))
38
+ end
39
+
40
+ after do
41
+ Timeliness.configuration.ambiguous_year_threshold = @original_threshold
42
+ Timecop.return
43
+ end
44
+
45
+ it "should convert 2-digit years to 4-digit years based on the current century and ambiguous year threshold" do
46
+ # Current century (21st) for years below threshold
47
+ expect(unambiguous_year('29')).to eq 2029
48
+
49
+ # Previous century (20th) for years above or equal to threshold
50
+ expect(unambiguous_year('30')).to eq 1930
51
+ expect(unambiguous_year('99')).to eq 1999
52
+
53
+ # Should not modify years that are already 4-digits
54
+ expect(unambiguous_year('2023')).to eq 2023
55
+
56
+ # Should handle single digit years with padding
57
+ expect(unambiguous_year('7')).to eq 2007
58
+
59
+ # Should handle years at century boundaries
60
+ expect(unambiguous_year('00')).to eq 2000
61
+ end
62
+ end
63
+
64
+ describe "#month_index" do
65
+ before do
66
+ allow(self).to receive(:i18n_loaded?).and_return(false)
67
+ end
68
+
69
+ it "should correctly parse month names as month indices regardless of case" do
70
+ # Testing with full month names
71
+ expect(month_index("january")).to eq 1
72
+ expect(month_index("MARCH")).to eq 3
73
+ expect(month_index("DeCeMbEr")).to eq 12
74
+
75
+ # Testing with abbreviated month names
76
+ expect(month_index("jan")).to eq 1
77
+ expect(month_index("MAR")).to eq 3
78
+ expect(month_index("deC")).to eq 12
79
+
80
+ # Testing with numeric month
81
+ expect(month_index("7")).to eq 7
82
+ end
83
+ end
84
+
85
+ describe "#microseconds" do
86
+ it "should convert microsecond strings to integer microsecond values" do
87
+ expect(microseconds('0')).to eq 0
88
+ expect(microseconds('1')).to eq 100000
89
+ expect(microseconds('01')).to eq 10000
90
+ expect(microseconds('001')).to eq 1000
91
+ expect(microseconds('9')).to eq 900000
92
+ expect(microseconds('99')).to eq 990000
93
+ expect(microseconds('999')).to eq 999000
94
+ expect(microseconds('999999')).to eq 999999
95
+ end
96
+ end
97
+
98
+ describe "#offset_in_seconds" do
99
+ it "should calculate offset in seconds from timezone string formats" do
100
+ # Standard format with colon
101
+ expect(offset_in_seconds('+10:00')).to eq 36000
102
+ expect(offset_in_seconds('-05:30')).to eq -19800
103
+
104
+ # Format without colon
105
+ expect(offset_in_seconds('+1030')).to eq 37800
106
+ expect(offset_in_seconds('-0530')).to eq -19800
107
+
108
+ # Default positive sign when omitted
109
+ expect(offset_in_seconds('08:00')).to eq 28800
110
+
111
+ # Zero offset
112
+ expect(offset_in_seconds('+00:00')).to eq 0
113
+ expect(offset_in_seconds('-00:00')).to eq 0
114
+ end
115
+ end
116
+ end
@@ -2,7 +2,7 @@ describe Timeliness::Parser do
2
2
  def self.timezone_settings(zone: nil, output: nil)
3
3
  before do
4
4
  Time.zone = zone if zone
5
- Timeliness.configuration.default_timezone = output if output
5
+ Timeliness.configuration.default_timezone = output if output
6
6
  end
7
7
  end
8
8
 
@@ -82,23 +82,23 @@ describe Timeliness::Parser do
82
82
  end
83
83
 
84
84
  context "string with zone offset value" do
85
- context "when current timezone is earler than string zone" do
85
+ context "when current timezone is later than string zone" do
86
86
  timezone_settings zone: 'Australia/Melbourne', output: :current
87
87
 
88
- it 'should return value shifted by positive offset in default timezone' do
88
+ it 'should return value shifted by positive string offset in default timezone' do
89
89
  value = parse("2000-06-01T12:00:00+02:00")
90
90
  expect(value).to eq Time.zone.local(2000,6,1,20,0,0)
91
91
  expect(value.utc_offset).to eq 10.hours
92
92
  end
93
93
 
94
- it 'should return value shifted by negative offset in default timezone' do
94
+ it 'should return value shifted by negative string offset in default timezone' do
95
95
  value = parse("2000-06-01T12:00:00-01:00")
96
96
  expect(value).to eq Time.zone.local(2000,6,1,23,0,0)
97
97
  expect(value.utc_offset).to eq 10.hours
98
98
  end
99
99
  end
100
100
 
101
- context "when current timezone is later than string zone" do
101
+ context "when current timezone is earlier than string zone" do
102
102
  timezone_settings zone: 'America/Phoenix', output: :current
103
103
 
104
104
  it 'should return value shifted by positive offset in default timezone' do
@@ -146,7 +146,7 @@ describe Timeliness::Parser do
146
146
 
147
147
  context "string with zulu time abbreviation 'Z'" do
148
148
  timezone_settings zone: 'Australia/Melbourne'
149
-
149
+
150
150
  it 'should return value using string zone adjusted to default :current timezone' do
151
151
  Timeliness.configuration.default_timezone = :current
152
152
 
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: timeliness
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.5.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Meehan
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-01-07 00:00:00.000000000 Z
10
+ date: 2025-05-13 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activesupport
@@ -164,6 +163,7 @@ files:
164
163
  - spec/timeliness/definitions_spec.rb
165
164
  - spec/timeliness/format_set_spec.rb
166
165
  - spec/timeliness/format_spec.rb
166
+ - spec/timeliness/helpers_spec.rb
167
167
  - spec/timeliness/parser_spec.rb
168
168
  - spec/timeliness_helper.rb
169
169
  - timeliness.gemspec
@@ -176,7 +176,6 @@ metadata:
176
176
  changelog_uri: https://github.com/adzap/timeliness/blob/master/CHANGELOG.rdoc
177
177
  source_code_uri: https://github.com/adzap/timeliness
178
178
  wiki_uri: https://github.com/adzap/timeliness/wiki
179
- post_install_message:
180
179
  rdoc_options: []
181
180
  require_paths:
182
181
  - lib
@@ -191,8 +190,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
191
190
  - !ruby/object:Gem::Version
192
191
  version: '0'
193
192
  requirements: []
194
- rubygems_version: 3.4.19
195
- signing_key:
193
+ rubygems_version: 3.6.3
196
194
  specification_version: 4
197
195
  summary: Date/time parsing for the control freak.
198
196
  test_files: []