timeliness 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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