timeliness 0.1.0

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