timeliness 0.2.0 → 0.3.0

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.
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ pkg/*
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/CHANGELOG.rdoc CHANGED
@@ -1,3 +1,9 @@
1
+ = 0.3.0 - 2010-11-27
2
+ * Support for parsed timezone offset or abbreviation being used in creating time value
3
+ * Added timezone abbreviation mapping config option
4
+ * Allow 2nd argument for parse method to be the type, :now value, or options hash.
5
+ * Refactoring
6
+
1
7
  = 0.2.0 - 2010-10-27
2
8
  * Allow a lambda for date_for_time_type which is evaluated on parse
3
9
  * Return the offset or zone in array from _parse
data/README.rdoc CHANGED
@@ -14,9 +14,9 @@ Date/time parser for Ruby with the following features:
14
14
  * I18n support (for months), if I18n gem loaded.
15
15
  * Fewer WTFs than Time/Date parse method.
16
16
  * Has no dependencies.
17
- * Works with Ruby MRI 1.8.*, 1.9.2, Rubinius
17
+ * Works with Ruby MRI 1.8.*, 1.9.2, Rubinius and JRuby.
18
18
 
19
- Extracted from my {validates_timeliness gem}[http://github.com/adzap/validates_timeliness], it has been rewritten cleaner and much faster. It's most suitable for when
19
+ Extracted from the {validates_timeliness gem}[http://github.com/adzap/validates_timeliness], it has been rewritten cleaner and much faster. It's most suitable for when
20
20
  you need to control the parsing behaviour. It's faster than the Time/Date class parse methods, so it
21
21
  has general appeal.
22
22
 
@@ -64,6 +64,9 @@ It can also be specified with :now option:
64
64
 
65
65
  Timeliness.parse('12:13:14', :now => Time.mktime(2010,9,8)) #=> Wed Sep 08 12:13:14 1000 2010
66
66
 
67
+ As well conforming to the Ruby Time class style.
68
+
69
+ Timeliness.parse('12:13:14', Time.mktime(2010,9,8)) #=> Wed Sep 08 12:13:14 1000 2010
67
70
 
68
71
  === Timezone
69
72
 
@@ -95,13 +98,36 @@ To get super finicky, you can restrict the parsing to a single format with the :
95
98
  Timeliness.parse('08/09/2010 12:13:14', :format => 'yyyy-mm-dd hh:nn:ss') #=> nil
96
99
 
97
100
 
101
+ === String with Offset or Zone Abbreviations
102
+
103
+ Sometimes you may want to parse a string with a zone abbreviation (e.g. MST) or the zone offset (e.g. +1000).
104
+ These values are supported by the parser and will be used when creating the time object. The return value
105
+ will be in the default timezone or the zone specified with the :zone option.
106
+
107
+ Timeliness.parse('Wed, 08 Sep 2010 12:13:14 MST') => Thu, 09 Sep 2010 05:13:14 EST 10:00
108
+
109
+ Timeliness.parse('2010-09-08T12:13:14-06:00') => Thu, 09 Sep 2010 05:13:14 EST 10:00
110
+
111
+ To enable zone abbreviations to work you must have loaded ActiveSupport.
112
+
113
+ The zone abbreviations supported are those defined in the TzInfo gem, used by ActiveSupport. If you find some
114
+ that are missing you can add more:
115
+
116
+ Timeliness.timezone_mapping.update(
117
+ 'ZZZ' => 'Sleepy Town'
118
+ )
119
+
120
+ Where 'Sleepy Town' is a valid zone name supported by ActiveSupport/TzInfo.
121
+
122
+
98
123
  === Raw Parsed Values
99
124
 
100
125
  If you would like to get the raw array of values before the time object is created, you can with
101
126
 
102
- Timeliness._parse('2010-09-08 12:13:14') # => [2010, 9, 8, 12, 13, 14, nil, nil]
127
+ Timeliness._parse('2010-09-08 12:13:14.123456 MST') # => [2010, 9, 8, 12, 13, 14, 123456, 'MST']
103
128
 
104
- The last two nils are for the empty value of microseconds, and timezone or offset.
129
+ The last two value are the microseconds, and zone abbreviation or offset.
130
+ Note: The format for this value is not defined. You can add it yourself, easily.
105
131
 
106
132
 
107
133
  == Formats
data/Rakefile CHANGED
@@ -1,28 +1,9 @@
1
1
  require 'rubygems'
2
2
  require 'rake/rdoctask'
3
- require 'rake/gempackagetask'
4
3
  require 'rubygems/specification'
5
4
  require 'rspec/core/rake_task'
6
- require 'lib/timeliness/version'
7
5
 
8
6
  GEM_NAME = "timeliness"
9
- GEM_VERSION = Timeliness::VERSION
10
-
11
- spec = Gem::Specification.new do |s|
12
- s.name = GEM_NAME
13
- s.version = GEM_VERSION
14
- s.platform = Gem::Platform::RUBY
15
- s.rubyforge_project = "timeliness"
16
- s.has_rdoc = true
17
- s.extra_rdoc_files = ["README.rdoc", "CHANGELOG.rdoc"]
18
- s.summary = %q{Control time (parsing), quickly.}
19
- s.description = %q{Fast date/time parser with customisable formats and I18n support.}
20
- s.author = "Adam Meehan"
21
- s.email = "adam.meehan@gmail.com"
22
- s.homepage = "http://github.com/adzap/timeliness"
23
- s.require_path = 'lib'
24
- s.files = %w(timeliness.gemspec LICENSE CHANGELOG.rdoc README.rdoc Rakefile) + Dir.glob("{lib,spec}/**/*")
25
- end
26
7
 
27
8
  desc 'Default: run specs.'
28
9
  task :default => :spec
@@ -47,18 +28,7 @@ Rake::RDocTask.new(:rdoc) do |rdoc|
47
28
  rdoc.rdoc_files.include('lib/**/*.rb')
48
29
  end
49
30
 
50
- Rake::GemPackageTask.new(spec) do |pkg|
51
- pkg.gem_spec = spec
52
- end
53
-
54
- desc "Install the gem locally"
55
- task :install => [:package] do
56
- sh %{gem install pkg/#{GEM_NAME}-#{GEM_VERSION}}
57
- end
58
-
59
31
  desc "Create a gemspec file"
60
- task :make_spec do
61
- File.open("#{GEM_NAME}.gemspec", "w") do |file|
62
- file.puts spec.to_ruby
63
- end
32
+ task :build do
33
+ `gem build #{GEM_NAME}.gemspec`
64
34
  end
data/benchmark.rb ADDED
@@ -0,0 +1,161 @@
1
+ $:.unshift(File.expand_path('lib'))
2
+
3
+ require 'benchmark'
4
+ require 'time'
5
+ require 'parsedate'
6
+ require 'timeliness'
7
+
8
+ if defined?(JRUBY_VERSION)
9
+ # Warm up JRuby
10
+ 20000.times do
11
+ Time.parse("2000-01-04 12:12:12")
12
+ Timeliness::Parser.parse("2000-01-04 12:12:12", :datetime)
13
+ end
14
+ end
15
+
16
+ n = 10000
17
+ Benchmark.bm do |x|
18
+ x.report('timeliness - datetime') {
19
+ n.times do
20
+ Timeliness::Parser.parse("2000-01-04 12:12:12", :datetime)
21
+ end
22
+ }
23
+
24
+ x.report('timeliness - datetime with :format') {
25
+ n.times do
26
+ Timeliness::Parser.parse("2000-01-04 12:12:12", :datetime, :format => 'yyyy-mm-dd hh:nn:ss')
27
+ end
28
+ }
29
+
30
+ x.report('timeliness - date') {
31
+ n.times do
32
+ Timeliness::Parser.parse("2000-01-04", :date)
33
+ end
34
+ }
35
+
36
+ x.report('timeliness - date as datetime') {
37
+ n.times do
38
+ Timeliness::Parser.parse("2000-01-04", :datetime)
39
+ end
40
+ }
41
+
42
+ x.report('timeliness - time') {
43
+ n.times do
44
+ Timeliness::Parser.parse("12:01:02", :time)
45
+ end
46
+ }
47
+
48
+ x.report('timeliness - no type with datetime value') {
49
+ n.times do
50
+ Timeliness::Parser.parse("2000-01-04 12:12:12")
51
+ end
52
+ }
53
+
54
+ x.report('timeliness - no type with date value') {
55
+ n.times do
56
+ Timeliness::Parser.parse("2000-01-04")
57
+ end
58
+ }
59
+
60
+ x.report('timeliness - no type with time value') {
61
+ n.times do
62
+ Timeliness::Parser.parse("12:01:02")
63
+ end
64
+ }
65
+
66
+ x.report('timeliness - invalid format datetime') {
67
+ n.times do
68
+ Timeliness::Parser.parse("20xx-01-04 12:12:12", :datetime)
69
+ end
70
+ }
71
+
72
+ x.report('timeliness - invalid format date') {
73
+ n.times do
74
+ Timeliness::Parser.parse("20xx-01-04", :date)
75
+ end
76
+ }
77
+
78
+ x.report('timeliness - invalid format time') {
79
+ n.times do
80
+ Timeliness::Parser.parse("12:xx:02", :time)
81
+ end
82
+ }
83
+
84
+ x.report('timeliness - invalid value datetime') {
85
+ n.times do
86
+ Timeliness::Parser.parse("2000-01-32 12:12:12", :datetime)
87
+ end
88
+ }
89
+
90
+ x.report('timeliness - invalid value date') {
91
+ n.times do
92
+ Timeliness::Parser.parse("2000-01-32", :date)
93
+ end
94
+ }
95
+
96
+ x.report('timeliness - invalid value time') {
97
+ n.times do
98
+ Timeliness::Parser.parse("12:61:02", :time)
99
+ end
100
+ }
101
+
102
+ x.report('ISO regexp for datetime') {
103
+ n.times do
104
+ "2000-01-04 12:12:12" =~ /\A(\d{4})-(\d{2})-(\d{2}) (\d{2})[\. :](\d{2})([\. :](\d{2}))?\Z/
105
+ microsec = ($7.to_f * 1_000_000).to_i
106
+ Time.mktime($1.to_i, $2.to_i, $3.to_i, $3.to_i, $5.to_i, $6.to_i, microsec)
107
+ end
108
+ }
109
+
110
+ x.report('Time.parse - valid') {
111
+ n.times do
112
+ Time.parse("2000-01-04 12:12:12")
113
+ end
114
+ }
115
+
116
+ x.report('Time.parse - invalid ') {
117
+ n.times do
118
+ Time.parse("2000-01-32 12:12:12") rescue nil
119
+ end
120
+ }
121
+
122
+ x.report('Date._parse - valid') {
123
+ n.times do
124
+ hash = Date._parse("2000-01-04 12:12:12")
125
+ Time.mktime(hash[:year], hash[:mon], hash[:mday], hash[:hour], hash[:min], hash[:sec])
126
+ end
127
+ }
128
+
129
+ x.report('Date._parse - invalid ') {
130
+ n.times do
131
+ hash = Date._parse("2000-01-32 12:12:12")
132
+ Time.mktime(hash[:year], hash[:mon], hash[:mday], hash[:hour], hash[:min], hash[:sex]) rescue nil
133
+ end
134
+ }
135
+
136
+ x.report('parsedate - valid') {
137
+ n.times do
138
+ arr = ParseDate.parsedate("2000-01-04 12:12:12")
139
+ Date.new(*arr[0..2])
140
+ Time.mktime(*arr)
141
+ end
142
+ }
143
+
144
+ x.report('parsedate - invalid ') {
145
+ n.times do
146
+ arr = ParseDate.parsedate("2000-00-04 12:12:12")
147
+ end
148
+ }
149
+
150
+ x.report('strptime - valid') {
151
+ n.times do
152
+ DateTime.strptime("2000-01-04 12:12:12", '%Y-%m-%d %H:%M:%s')
153
+ end
154
+ }
155
+
156
+ x.report('strptime - invalid') {
157
+ n.times do
158
+ DateTime.strptime("2000-00-04 12:12:12", '%Y-%m-%d %H:%M:%s') rescue nil
159
+ end
160
+ }
161
+ end
data/lib/timeliness.rb CHANGED
@@ -2,7 +2,8 @@ require 'date'
2
2
  require 'forwardable'
3
3
 
4
4
  require 'timeliness/helpers'
5
- require 'timeliness/formats'
5
+ require 'timeliness/definitions'
6
+ require 'timeliness/format'
6
7
  require 'timeliness/format_set'
7
8
  require 'timeliness/parser'
8
9
  require 'timeliness/version'
@@ -11,7 +12,7 @@ module Timeliness
11
12
  class << self
12
13
  extend Forwardable
13
14
  def_delegators Parser, :parse, :_parse
14
- def_delegators Formats, :add_formats, :remove_formats, :use_us_formats, :use_euro_formats
15
+ def_delegators Definitions, :add_formats, :remove_formats, :use_us_formats, :use_euro_formats
15
16
  attr_accessor :default_timezone, :date_for_time_type, :ambiguous_year_threshold
16
17
  end
17
18
 
@@ -26,7 +27,7 @@ module Timeliness
26
27
  @default_timezone = :local
27
28
 
28
29
  # Set the default date part for a time type values.
29
- @date_for_time_type = [ 2000, 1, 1 ]
30
+ @date_for_time_type = lambda { Time.now }
30
31
 
31
32
  def self.date_for_time_type
32
33
  case @date_for_time_type
@@ -49,4 +50,4 @@ module Timeliness
49
50
  @ambiguous_year_threshold = 30
50
51
  end
51
52
 
52
- Timeliness::Formats.compile_formats
53
+ Timeliness::Definitions.compile_formats
@@ -1,5 +1,5 @@
1
1
  module Timeliness
2
- module Formats
2
+ module Definitions
3
3
 
4
4
  # Format tokens:
5
5
  # y = year
@@ -100,7 +100,7 @@ module Timeliness
100
100
  'u' => [ '\d{1,6}', :usec ],
101
101
  'ampm' => [ '[aApP]\.?[mM]\.?', :meridian ],
102
102
  'zo' => [ '[+-]\d{2}:?\d{2}', :offset ],
103
- 'tz' => [ '[A-Z]{1,4}', :zone ],
103
+ 'tz' => [ '[A-Z]{1,5}', :zone ],
104
104
  '_' => [ '\s?' ]
105
105
  }
106
106
 
@@ -126,10 +126,25 @@ module Timeliness
126
126
  :meridian => [ nil ]
127
127
  }
128
128
 
129
+ # Mapping some common timezone abbreviations which are not mapped or
130
+ # mapped inconsistenly in ActiveSupport (TzInfo).
131
+ @timezone_mapping = {
132
+ 'AEST' => 'Australia/Sydney',
133
+ 'AEDT' => 'Australia/Sydney',
134
+ 'ACST' => 'Australia/Adelaide',
135
+ 'ACDT' => 'Australia/Adelaide',
136
+ 'PST' => 'PST8PDT',
137
+ 'PDT' => 'PST8PDT',
138
+ 'CST' => 'CST6CDT',
139
+ 'CDT' => 'CST6CDT',
140
+ 'EDT' => 'EST5EDT',
141
+ 'MDT' => 'MST7MDT'
142
+ }
143
+
129
144
  US_FORMAT_REGEXP = /\Am{1,2}[^m]/
130
145
 
131
146
  class << self
132
- attr_accessor :time_formats, :date_formats, :datetime_formats, :format_tokens, :format_components
147
+ attr_accessor :time_formats, :date_formats, :datetime_formats, :format_tokens, :format_components, :timezone_mapping
133
148
  attr_reader :date_format_set, :time_format_set, :datetime_format_set
134
149
 
135
150
  # Adds new formats. Must specify format type and can specify a :before
@@ -191,7 +206,7 @@ module Timeliness
191
206
 
192
207
  # Returns format for type and other possible matching format set based on type
193
208
  # and value length. Gives minor speed-up by checking string length.
194
- def format_set(type, string)
209
+ def format_sets(type, string)
195
210
  case type
196
211
  when :date
197
212
  [ @date_format_set, @datetime_format_set ]
@@ -217,6 +232,5 @@ module Timeliness
217
232
  end
218
233
 
219
234
  end
220
-
221
235
  end
222
236
  end
@@ -0,0 +1,64 @@
1
+ module Timeliness
2
+ class Format
3
+ include Helpers
4
+
5
+ attr_reader :format_string, :regexp, :regexp_string, :token_count
6
+
7
+ def initialize(format_string)
8
+ @format_string = format_string
9
+ end
10
+
11
+ def compile!
12
+ @token_count = 0
13
+ format = format_string.dup
14
+ format.gsub!(/([\.\\])/, '\\\\\1') # escapes dots and backslashes
15
+ found_tokens, token_order = [], []
16
+
17
+ # Substitute tokens with numbered placeholder
18
+ Definitions.sorted_token_keys.each do |token|
19
+ token_regexp_str, arg_key = Definitions.format_tokens[token]
20
+ if format.gsub!(/#{token}/, "%<#{found_tokens.size}>")
21
+ if arg_key
22
+ token_regexp_str = "(#{token_regexp_str})"
23
+ @token_count += 1
24
+ end
25
+ found_tokens << [token_regexp_str, arg_key]
26
+ end
27
+ end
28
+
29
+ # Replace placeholders with token regexps
30
+ format.scan(/%<(\d)>/).each {|token_index|
31
+ token_index = token_index.first
32
+ token_regexp_str, arg_key = found_tokens[token_index.to_i]
33
+ format.gsub!("%<#{token_index}>", token_regexp_str)
34
+ token_order << arg_key
35
+ }
36
+
37
+ define_process_method(token_order.compact)
38
+ @regexp_string = format
39
+ @regexp = Regexp.new("^(#{format})$")
40
+ self
41
+ rescue
42
+ raise "The format '#{format_string}' failed to compile using regexp string #{format}."
43
+ end
44
+
45
+ # Redefined on compile
46
+ def process(*args); end
47
+
48
+ private
49
+
50
+ def define_process_method(components)
51
+ values = [nil] * 8
52
+ components.each do |component|
53
+ position, code = Definitions.format_components[component]
54
+ values[position] = code || "#{component}.to_i" if position
55
+ end
56
+ instance_eval <<-DEF
57
+ def process(#{components.join(',')})
58
+ [#{values.map {|i| i || 'nil' }.join(',')}]
59
+ end
60
+ DEF
61
+ end
62
+
63
+ end
64
+ end
@@ -1,97 +1,42 @@
1
1
  module Timeliness
2
2
  class FormatSet
3
- include Helpers
4
-
5
3
  attr_reader :formats, :regexp
6
4
 
7
- class << self
8
-
9
- def compile(formats)
10
- set = new(formats)
11
- set.compile!
12
- set
13
- end
14
-
15
- def compile_format(string_format)
16
- format = string_format.dup
17
- format.gsub!(/([\.\\])/, '\\\\\1') # escapes dots and backslashes
18
- found_tokens, token_order, value_token_count = [], [], 0
19
-
20
- # Substitute tokens with numbered placeholder
21
- Formats.sorted_token_keys.each do |token|
22
- regexp_str, arg_key = *Formats.format_tokens[token]
23
- if format.gsub!(/#{token}/, "%<#{found_tokens.size}>")
24
- if arg_key
25
- regexp_str = "(#{regexp_str})"
26
- value_token_count += 1
27
- end
28
- found_tokens << [regexp_str, arg_key]
29
- end
30
- end
31
-
32
- # Replace placeholders with token regexps
33
- format.scan(/%<(\d)>/).each {|token_index|
34
- token_index = token_index.first
35
- regexp_str, arg_key = found_tokens[token_index.to_i]
36
- format.gsub!("%<#{token_index}>", regexp_str)
37
- token_order << arg_key
38
- }
39
-
40
- define_format_method(string_format, token_order.compact)
41
- return format, value_token_count
42
- rescue
43
- raise "The following format regular expression failed to compile: #{format}\n from format #{string_format}."
44
- end
45
-
46
- # Compiles a format method which maps the regexp capture groups to method
47
- # arguments based on order captured. A time array is built using the argument
48
- # values placed in the position defined by the component.
49
- #
50
- def define_format_method(name, components)
51
- values = [nil] * 8
52
- components.each do |component|
53
- position, code = *Formats.format_components[component]
54
- values[position] = code || "#{component}.to_i" if position
55
- end
56
- class_eval <<-DEF
57
- define_method(:"format_#{name}") do |#{components.join(',')}|
58
- [#{values.map {|i| i || 'nil' }.join(',')}]
59
- end
60
- DEF
61
- end
62
-
5
+ def self.compile(formats)
6
+ new(formats).compile!
63
7
  end
64
8
 
65
9
  def initialize(formats)
66
- @formats = formats
10
+ @formats = formats
11
+ @formats_hash = {}
12
+ @match_indexes = {}
67
13
  end
68
14
 
69
15
  # Compiles the formats into one big regexp. Stores the index of where
70
- # each format's capture values begin in the match data. Each individual
71
- # format regpexp is also stored for use with the parse :format option.
72
- #
16
+ # each format's capture values begin in the matchdata.
73
17
  def compile!
74
- regexp_string = ''
75
- @format_regexps = {}
76
- @match_indexes = {}
77
- @formats.inject(0) { |index, format|
78
- format_regexp, token_count = self.class.compile_format(format)
79
- @format_regexps[format] = Regexp.new("^(#{format_regexp})$")
80
- @match_indexes[index] = format
81
- regexp_string = "#{regexp_string}(#{format_regexp})|"
82
- index + token_count + 1 # add one for wrapper capture
18
+ regexp_string = ''
19
+ @formats.inject(0) { |index, format_string|
20
+ format = Format.new(format_string).compile!
21
+ @formats_hash[format_string] = format
22
+ @match_indexes[index] = format
23
+ regexp_string = "#{regexp_string}(#{format.regexp_string})|"
24
+ index + format.token_count + 1 # add one for wrapper capture
83
25
  }
84
26
  @regexp = Regexp.new("^(?:#{regexp_string.chop})$")
27
+ self
85
28
  end
86
29
 
87
- def match(string, format=nil)
88
- match_regexp = format ? @format_regexps[format] : @regexp
30
+ def match(string, format_string=nil)
31
+ format = @formats_hash[format_string] if format_string
32
+ match_regexp = format && format.regexp || @regexp
33
+
89
34
  if match_data = match_regexp.match(string)
90
35
  index = match_data.captures.index(string)
91
36
  start = index + 1
92
37
  values = match_data.captures[start..(start+7)].compact
93
38
  format ||= @match_indexes[index]
94
- send(:"format_#{format}", *values)
39
+ format.process(*values)
95
40
  end
96
41
  end
97
42