timeliness 0.1.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/CHANGELOG.rdoc ADDED
File without changes
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Adam Meehan
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,242 @@
1
+ = Timeliness
2
+
3
+ * Source: http://github.com/adzap/timeliness
4
+ * Bugs: http://github.com/adzap/timeliness/issues
5
+
6
+ == Description
7
+
8
+ Date/time parser for Ruby with the following features:
9
+
10
+ * Extensible with custom formats and tokens.
11
+ * It's pretty fast. Up to 60% faster than Time/Date parse method.
12
+ * Control the parser strictness.
13
+ * Control behaviour of ambiguous date formats (US vs European e.g. mm/dd/yy, dd/mm/yy).
14
+ * I18n support (for months).
15
+ * Fewer WTFs than Time/Date parse method.
16
+ * Has no dependencies.
17
+
18
+ Extracted from the validates_timeliness gem, it has been rewritten cleaner and much faster. It's most suitable for when
19
+ you need to control the parsing behaviour. It's faster than the Time/Date class parse methods, so it has general appeal.
20
+
21
+
22
+ == Usage
23
+
24
+ The simplest example is just a straight forward string parse:
25
+
26
+ Timeliness.parse('2010-09-08 12:13:14') #=> Wed Sep 08 12:13:14 1000 2010
27
+ Timeliness.parse('2010-09-08') #=> Wed Sep 08 00:00:00 1000 2010
28
+ Timeliness.parse('12:13:14') #=> Sat Jan 01 12:13:14 1100 2000
29
+
30
+ Notice a time only string will return with a date value. The date value can be configured globally with
31
+ this setting:
32
+
33
+ Timeliness.date_for_time_type = [2010, 1, 1]
34
+
35
+ or specified with :now option:
36
+
37
+ Timeliness.parse('12:13:14', :now => Time.mktime(2010,9,8)) #=> Wed Sep 08 12:13:14 1000 2010
38
+
39
+ You can also provide a type which will tell the parser that you are only interested in the values for that type.
40
+
41
+ Timeliness.parse('2010-09-08 12:13:14', :date) #=> Wed Sep 08 00:00:00 1000 2010
42
+ Timeliness.parse('2010-09-08 12:13:14', :time) #=> Sat Jan 01 12:13:14 1100 2000
43
+ Timeliness.parse('2010-09-08 12:13:14', :datetime) #=> Wed Sep 08 12:13:14 1000 2010 i.e. the whole string is used
44
+
45
+ Now let's get strict. Pass the :strict option with true and things get finicky
46
+
47
+ Timeliness.parse('2010-09-08 12:13:14', :date, :strict => true) #=> nil
48
+ Timeliness.parse('2010-09-08 12:13:14', :time, :strict => true) #=> nil
49
+ Timeliness.parse('2010-09-08 12:13:14', :datetime, :strict => true) #=> Wed Sep 08 12:13:14 1000 2010 i.e. the whole string is used
50
+
51
+ The strict option without a type is ignored.
52
+
53
+ To control what zone the time object is returned in, you have two options. Firstly you can set the default zone. Below
54
+ is the list of options with their effective time creation method call
55
+
56
+ Timeliness.default_timezone = :local # Time.local(...)
57
+ Timeliness.default_timezone = :utc # Time.utc(...)
58
+ Timeliness.default_timezone = :current # Time.zone.local(...)
59
+ Timeliness.default_timezone = 'Melbourne' # Time.use_zone('Melbourne') { Time.zone.local(...) }
60
+
61
+ The last two options require that you have ActiveSupport timezone extension loaded.
62
+
63
+ You can also use the :zone option to control it for a single parse call:
64
+
65
+ Timeliness.parse('2010-09-08 12:13:14', :zone => :utc) #=> Wed Sep 08 12:13:14 UTC 2010
66
+ Timeliness.parse('2010-09-08 12:13:14', :zone => :local) #=> Wed Sep 08 12:13:14 1000 2010
67
+ Timeliness.parse('2010-09-08 12:13:14', :zone => :current) #=> Wed Sep 08 12:13:14 1000 2010, with Time.zone = 'Melbourne'
68
+ Timeliness.parse('2010-09-08 12:13:14', :zone => 'Melbourne') #=> Wed Sep 08 12:13:14 1000 2010
69
+
70
+ Remember, you must have ActiveSupport timezone extension loaded to use the last two examples.
71
+
72
+ To get super finicky, you can restrict the parsing to a single format with the :format option
73
+
74
+ Timeliness.parse('2010-09-08 12:13:14', :format => 'yyyy-mm-dd hh:nn:ss') #=> Wed Sep 08 12:13:14 UTC 2010
75
+ Timeliness.parse('08/09/2010 12:13:14', :format => 'yyyy-mm-dd hh:nn:ss') #=> nil
76
+
77
+ If you would like to get the raw array of values before the time object is created, you can with
78
+
79
+ Timeliness._parse('2010-09-08 12:13:14') # => [2010, 9, 8, 12, 13, 14, nil]
80
+
81
+ The last nil is for the empty value of microseconds.
82
+
83
+
84
+ == Formats
85
+
86
+ The gem has default formats included which can be easily added to using the format syntax. Also formats can be easily
87
+ removed so that they are no longer considered valid.
88
+
89
+ Below are the default formats. If you think they are easy to read then you will be happy to know that is exactly the same
90
+ format syntax you can use to define your own. No complex regular expressions are needed.
91
+
92
+
93
+ === Datetime formats
94
+
95
+ m/d/yy h:nn:ss OR d/m/yy hh:nn:ss
96
+ m/d/yy h:nn OR d/m/yy h:nn
97
+ m/d/yy h:nn_ampm OR d/m/yy h:nn_ampm
98
+ yyyy-mm-dd hh:nn:ss
99
+ yyyy-mm-dd h:nn
100
+ ddd mmm d hh:nn:ss zo yyyy # Ruby time string
101
+ yyyy-mm-ddThh:nn:ssZ # ISO 8601 without zone offset
102
+ yyyy-mm-ddThh:nn:sszo # ISO 8601 with zone offset
103
+
104
+ NOTE: To use non-US date formats see US/Euro Formats section
105
+
106
+
107
+ === Date formats
108
+
109
+ yyyy/mm/dd
110
+ yyyy-mm-dd
111
+ yyyy.mm.dd
112
+ m/d/yy OR d/m/yy
113
+ m\d\yy OR d\m\yy
114
+ d-m-yy
115
+ dd-mm-yyyy
116
+ d.m.yy
117
+ d mmm yy
118
+
119
+ NOTE: To use non-US date formats see US/Euro Formats section
120
+
121
+
122
+ === Time formats
123
+
124
+ hh:nn:ss
125
+ hh-nn-ss
126
+ h:nn
127
+ h.nn
128
+ h nn
129
+ h-nn
130
+ h:nn_ampm
131
+ h.nn_ampm
132
+ h nn_ampm
133
+ h-nn_ampm
134
+ h_ampm
135
+
136
+ NOTE: Any time format without a meridian token (the 'ampm' token) is considered in 24 hour time.
137
+
138
+
139
+ === Format Tokens
140
+
141
+ Here is what each format token means:
142
+
143
+ Format tokens:
144
+ y = year
145
+ m = month
146
+ d = day
147
+ h = hour
148
+ n = minute
149
+ s = second
150
+ u = micro-seconds
151
+ ampm = meridian (am or pm) with or without dots (e.g. am, a.m, or a.m.)
152
+ _ = optional space
153
+ tz = Timezone abbreviation (e.g. UTC, GMT, PST, EST)
154
+ zo = Timezone offset (e.g. +10:00, -08:00, +1000)
155
+
156
+ Repeating tokens:
157
+ x = 1 or 2 digits for unit (e.g. 'h' means an hour can be '9' or '09')
158
+ xx = 2 digits exactly for unit (e.g. 'hh' means an hour can only be '09')
159
+
160
+ Special Cases:
161
+ yy = 2 or 4 digit year
162
+ yyyy = exactly 4 digit year
163
+ mmm = month long name (e.g. 'Jul' or 'July')
164
+ ddd = Day name of 3 to 9 letters (e.g. Wed or Wednesday)
165
+ u = microseconds matches 1 to 3 digits
166
+
167
+ All other characters are considered literal. For the technically minded, these formats are compiled into a single regular expression
168
+
169
+ To see all defined formats look at the {parser source code}[http://github.com/adzap/timeliness/tree/master/lib/timeliness/parser.rb].
170
+
171
+
172
+ == Settings
173
+
174
+ === US/Euro Formats
175
+
176
+ The perennial problem for non-US developers or applications not primarily for the
177
+ US, is the US date format of m/d/yy. This is ambiguous with the European format
178
+ of d/m/yy. By default the gem uses the US formats as this is the Ruby default
179
+ when it does date interpretation.
180
+
181
+ To switch to using the European (or Rest of The World) formats use this setting
182
+
183
+ Timeliness.use_euro_formats
184
+
185
+ Now '01/02/2000' will be parsed as 1st February 2000, instead of 2nd January 2000.
186
+
187
+ You can switch back to US formats with
188
+
189
+ Timeliness.use_us_formats
190
+
191
+
192
+ === Customising Formats
193
+
194
+ Sometimes you may not want certain formats to be valid. You can remove formats for each type and
195
+ the parser will then not consider that a valid format. To remove a format
196
+
197
+ Timeliness.remove_formats(:date, 'm\d\yy')
198
+
199
+ Adding new formats is also simple
200
+
201
+ Timeliness.add_formats(:time, "d o'clock")
202
+
203
+ Now "10 o'clock" will be a valid value.
204
+
205
+ You can embed regular expressions in the format but no guarantees that it will
206
+ remain intact. If you avoid the use of any token characters, and regexp dots or
207
+ backslashes as special characters in the regexp, it may work as expected.
208
+ For special characters use POSIX character classes for safety. See the ISO 8601
209
+ datetime for an example of an embedded regular expression.
210
+
211
+ Because formats are evaluated in order, adding a format which may be ambiguous
212
+ with an existing format, will mean your format is ignored. If you need to make
213
+ your new format higher precedence than an existing format, you can include the
214
+ before option like so
215
+
216
+ Timeliness.add_formats(:time, 'ss:nn:hh', :before => 'hh:nn:ss')
217
+
218
+ Now a time of '59:30:23' will be interpreted as 11:30:59 pm. This option saves
219
+ you adding a new one and deleting an old one to get it to work.
220
+
221
+
222
+ === Ambiguous Year
223
+
224
+ When dealing with 2 digit year values, by default a year is interpreted as being
225
+ in the last century when at or above 30. You can customize this however
226
+
227
+ Timeliness.ambiguous_year_threshold = 20
228
+
229
+ Now you get:
230
+
231
+ year of 19 is considered 2019
232
+ year of 20 is considered 1920
233
+
234
+
235
+ == Credits
236
+
237
+ * Adam Meehan (adam.meehan@gmail.com, http://github.com/adzap)
238
+
239
+
240
+ == License
241
+
242
+ Copyright (c) 2010 Adam Meehan, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,64 @@
1
+ require 'rubygems'
2
+ require 'rake/rdoctask'
3
+ require 'rake/gempackagetask'
4
+ require 'rubygems/specification'
5
+ require 'rspec/core/rake_task'
6
+ require 'lib/timeliness/version'
7
+
8
+ 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
+
27
+ desc 'Default: run specs.'
28
+ task :default => :spec
29
+
30
+ desc "Run specs"
31
+ RSpec::Core::RakeTask.new do |t|
32
+ t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default.
33
+ end
34
+
35
+ desc "Generate code coverage"
36
+ RSpec::Core::RakeTask.new(:coverage) do |t|
37
+ t.rcov = true
38
+ t.rcov_opts = ['--exclude', 'spec']
39
+ end
40
+
41
+ desc 'Generate documentation for plugin.'
42
+ Rake::RDocTask.new(:rdoc) do |rdoc|
43
+ rdoc.rdoc_dir = 'rdoc'
44
+ rdoc.title = 'Timeliness'
45
+ rdoc.options << '--line-numbers' << '--inline-source'
46
+ rdoc.rdoc_files.include('README')
47
+ rdoc.rdoc_files.include('lib/**/*.rb')
48
+ end
49
+
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
+ 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
64
+ end
data/lib/timeliness.rb ADDED
@@ -0,0 +1,35 @@
1
+ require 'date'
2
+ require 'forwardable'
3
+
4
+ require 'timeliness/helpers'
5
+ require 'timeliness/formats'
6
+ require 'timeliness/format_set'
7
+ require 'timeliness/parser'
8
+ require 'timeliness/version'
9
+
10
+ module Timeliness
11
+ class << self
12
+ extend Forwardable
13
+ def_delegators Parser, :parse, :_parse
14
+ def_delegators Formats, :add_formats, :remove_formats, :use_us_formats, :use_euro_formats
15
+ attr_accessor :default_timezone, :date_for_time_type, :ambiguous_year_threshold
16
+ end
17
+
18
+ # Default timezone (:local or :utc)
19
+ @default_timezone = :local
20
+
21
+ # Set the default date part for a time type values.
22
+ @date_for_time_type = [ 2000, 1, 1 ]
23
+
24
+ # Set the threshold value for a two digit year to be considered last century
25
+ #
26
+ # Default: 30
27
+ #
28
+ # Example:
29
+ # year = '29' is considered 2029
30
+ # year = '30' is considered 1930
31
+ #
32
+ @ambiguous_year_threshold = 30
33
+ end
34
+
35
+ Timeliness::Formats.compile_formats
@@ -0,0 +1,95 @@
1
+ module Timeliness
2
+ class FormatSet
3
+ include Helpers
4
+
5
+ attr_reader :formats, :regexp
6
+
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] * 7
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
+
63
+ end
64
+
65
+ def initialize(formats)
66
+ @formats = formats
67
+ end
68
+
69
+ def compile!
70
+ regexp_string = ''
71
+ @format_regexps = {}
72
+ @match_indexes = {}
73
+ @formats.inject(0) { |index, format|
74
+ format_regexp, token_count = self.class.compile_format(format)
75
+ @format_regexps[format] = Regexp.new("^(#{format_regexp})$")
76
+ @match_indexes[index] = format
77
+ regexp_string = "#{regexp_string}(#{format_regexp})|"
78
+ index + token_count + 1 # add one for wrapper capture
79
+ }
80
+ @regexp = Regexp.new("^(?:#{regexp_string.chop})$")
81
+ end
82
+
83
+ def match(string, format=nil)
84
+ match_regexp = format ? @format_regexps[format] : @regexp
85
+ if match_data = match_regexp.match(string)
86
+ index = match_data.captures.index(string)
87
+ start = index + 1
88
+ values = match_data.captures[start..(start+7)].compact
89
+ format ||= @match_indexes[index]
90
+ send(:"format_#{format}", *values)
91
+ end
92
+ end
93
+
94
+ end
95
+ end