pax_global_header00006660000000000000000000000064134264102070014511gustar00rootroot0000000000000052 comment=23dd2ad90aa3968728339470d5196931b3cd21a8 timeliness-0.3.10/000077500000000000000000000000001342641020700137465ustar00rootroot00000000000000timeliness-0.3.10/.gitignore000066400000000000000000000000721342641020700157350ustar00rootroot00000000000000pkg/* .bundle/ vendor/bundle Gemfile.lock .byebug_history timeliness-0.3.10/.rspec000066400000000000000000000001131342641020700150560ustar00rootroot00000000000000--color --require byebug --require spec_helper --require timeliness_helper timeliness-0.3.10/.travis.yml000066400000000000000000000001501342641020700160530ustar00rootroot00000000000000language: ruby rvm: - 1.9.3 - ruby-head - jruby-19mode - rbx-19mode script: 'bundle exec rspec' timeliness-0.3.10/CHANGELOG.rdoc000066400000000000000000000030761342641020700161140ustar00rootroot00000000000000= 0.3.7 - 2012-10-03 * Change to a hot switch between US and Euro formats without a compile. * Fix date parsing with bad month name defaulting to 1 if year and day present. * Fix date parsing with nil month. = 0.3.6 - 2012-03-29 * Fix bug with month_index using Integer method and leading zeroes treated as octal. = 0.3.5 - 2012-03-29 * Correctly handle month value of 0. Fixes issue#4. = 0.3.4 - 2011-05-26 * Compact time array when creating time in zone so that invalid time handling works properly. Fixes issue#3. = 0.3.3 - 2011-01-02 * Add String core extension for to_time, to_date and to_datetime methods, like ActiveSupport * Allow arbitrary format string as :format option and it will be compiled, if not found. = 0.3.2 - 2010-11-26 * Catch all errors for ActiveSupport not being loaded for more helpful error = 0.3.1 - 2010-11-27 * Fix issue with 2nd argument options being overidden = 0.3.0 - 2010-11-27 * Support for parsed timezone offset or abbreviation being used in creating time value * Added timezone abbreviation mapping config option * Allow 2nd argument for parse method to be the type, :now value, or options hash. * Refactoring = 0.2.0 - 2010-10-27 * Allow a lambda for date_for_time_type which is evaluated on parse * Return the offset or zone in array from _parse * Give a nicer error message if use a zone and ActiveSupport is not loaded. * Removed some aliases used in validates_timeliness and are no longer needed. * Some minor spec fixes = 0.1.1 - 2010-10-14 * Alias for validates_timeliness compatibility * Tiny cleanup = 0.1.0 - 2010-10-14 * Initial release timeliness-0.3.10/Gemfile000066400000000000000000000001241342641020700152360ustar00rootroot00000000000000source 'http://rubygems.org' gemspec gem 'activesupport', '~> 4.2.0' gem 'byebug' timeliness-0.3.10/LICENSE000066400000000000000000000020371342641020700147550ustar00rootroot00000000000000Copyright (c) 2010 Adam Meehan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. timeliness-0.3.10/README.rdoc000066400000000000000000000243531342641020700155630ustar00rootroot00000000000000= Timeliness * Source: http://github.com/adzap/timeliness * Bugs: http://github.com/adzap/timeliness/issues == Description Date/time parser for Ruby with the following features: * Extensible with custom formats and tokens. * It's pretty fast. Up to 60% faster than Time/Date parse method. * Control the parser strictness. * Control behaviour of ambiguous date formats (US vs European e.g. mm/dd/yy, dd/mm/yy). * I18n support (for months), if I18n gem loaded. * Fewer WTFs than Time/Date parse method. * Has no dependencies. * Works with Ruby MRI 1.8.*, 1.9.2, Rubinius and JRuby. 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 you need to control the parsing behaviour. It's faster than the Time/Date class parse methods, so it has general appeal. == Usage The simplest example is just a straight forward string parse: Timeliness.parse('2010-09-08 12:13:14') #=> Wed Sep 08 12:13:14 1000 2010 Timeliness.parse('2010-09-08') #=> Wed Sep 08 00:00:00 1000 2010 Timeliness.parse('12:13:14') #=> Sat Jan 01 12:13:14 1100 2000 === Specify a Type You can provide a type which will tell the parser that you are only interested in the part of the value for that type. Timeliness.parse('2010-09-08 12:13:14', :date) #=> Wed Sep 08 00:00:00 1000 2010 Timeliness.parse('2010-09-08 12:13:14', :time) #=> Sat Jan 01 12:13:14 1100 2000 Timeliness.parse('2010-09-08 12:13:14', :datetime) #=> Wed Sep 08 12:13:14 1000 2010 i.e. the whole string is used Now let's get strict. Pass the :strict option with true and things get finicky Timeliness.parse('2010-09-08 12:13:14', :date, :strict => true) #=> nil Timeliness.parse('2010-09-08 12:13:14', :time, :strict => true) #=> nil 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 The date and time strings are not accepted for a datetime type. The strict option without a type is ignored. === Specify the Current Date Notice a time only string will return with a date value. The date value can be configured globally with this setting: Timeliness.date_for_time_type = [2010, 1, 1] or using a lambda thats evaluated when parsed Timeliness.date_for_time_type = lambda { Time.now } It can also be specified with :now option: Timeliness.parse('12:13:14', :now => Time.mktime(2010,9,8)) #=> Wed Sep 08 12:13:14 1000 2010 As well conforming to the Ruby Time class style. Timeliness.parse('12:13:14', Time.mktime(2010,9,8)) #=> Wed Sep 08 12:13:14 1000 2010 === Timezone To control what zone the time object is returned in, you have two options. Firstly you can set the default zone. Below is the list of options with their effective time creation method call Timeliness.default_timezone = :local # Time.local(...) Timeliness.default_timezone = :utc # Time.utc(...) Timeliness.default_timezone = :current # Time.zone.local(...). Use current zone. Timeliness.default_timezone = 'Melbourne' # Time.use_zone('Melbourne') { Time.zone.local(...) }. Doesn't change Time.zone. The last two options require that you have ActiveSupport timezone extension loaded. You can also use the :zone option to control it for a single parse call: Timeliness.parse('2010-09-08 12:13:14', :zone => :utc) #=> Wed Sep 08 12:13:14 UTC 2010 Timeliness.parse('2010-09-08 12:13:14', :zone => :local) #=> Wed Sep 08 12:13:14 1000 2010 Timeliness.parse('2010-09-08 12:13:14', :zone => :current) #=> Wed Sep 08 12:13:14 1000 2010, with Time.zone = 'Melbourne' Timeliness.parse('2010-09-08 12:13:14', :zone => 'Melbourne') #=> Wed Sep 08 12:13:14 1000 2010 Remember, you must have ActiveSupport timezone extension loaded to use the last two examples. === Restrict to Format To get super finicky, you can restrict the parsing to a single format with the :format option Timeliness.parse('2010-09-08 12:13:14', :format => 'yyyy-mm-dd hh:nn:ss') #=> Wed Sep 08 12:13:14 UTC 2010 Timeliness.parse('08/09/2010 12:13:14', :format => 'yyyy-mm-dd hh:nn:ss') #=> nil === String with Offset or Zone Abbreviations Sometimes you may want to parse a string with a zone abbreviation (e.g. MST) or the zone offset (e.g. +1000). These values are supported by the parser and will be used when creating the time object. The return value will be in the default timezone or the zone specified with the :zone option. Timeliness.parse('Wed, 08 Sep 2010 12:13:14 MST') => Thu, 09 Sep 2010 05:13:14 EST 10:00 Timeliness.parse('2010-09-08T12:13:14-06:00') => Thu, 09 Sep 2010 05:13:14 EST 10:00 To enable zone abbreviations to work you must have loaded ActiveSupport. The zone abbreviations supported are those defined in the TzInfo gem, used by ActiveSupport. If you find some that are missing you can add more: Timeliness.timezone_mapping.update( 'ZZZ' => 'Sleepy Town' ) Where 'Sleepy Town' is a valid zone name supported by ActiveSupport/TzInfo. === Raw Parsed Values If you would like to get the raw array of values before the time object is created, you can with Timeliness._parse('2010-09-08 12:13:14.123456 MST') # => [2010, 9, 8, 12, 13, 14, 123456, 'MST'] The last two value are the microseconds, and zone abbreviation or offset. Note: The format for this value is not defined. You can add it yourself, easily. === ActiveSupport Core Extensions To make it easier to use the parser in Rails or an app using ActiveSupport, you can add/override the methods for to_time, to_date and to_datetime on a string value. These methods will then use the Timeliness parser for converting a string, instead of the default. You just need to add this line to an initializer or other application file: require 'timeliness/core_ext' == Formats The gem has default formats included which can be easily added to using the format syntax. Also formats can be easily removed so that they are no longer considered valid. 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 format syntax you can use to define your own. No complex regular expressions are needed. === Datetime formats m/d/yy h:nn:ss OR d/m/yy hh:nn:ss m/d/yy h:nn OR d/m/yy h:nn m/d/yy h:nn_ampm OR d/m/yy h:nn_ampm yyyy-mm-dd hh:nn:ss yyyy-mm-dd h:nn ddd mmm d hh:nn:ss zo yyyy # Ruby time string yyyy-mm-ddThh:nn:ssZ # ISO 8601 without zone offset yyyy-mm-ddThh:nn:sszo # ISO 8601 with zone offset NOTE: To use non-US date formats see US/Euro Formats section === Date formats yyyy/mm/dd yyyy-mm-dd yyyy.mm.dd m/d/yy OR d/m/yy m\d\yy OR d\m\yy d-m-yy dd-mm-yyyy d.m.yy d mmm yy NOTE: To use non-US date formats see US/Euro Formats section === Time formats hh:nn:ss hh-nn-ss h:nn h.nn h nn h-nn h:nn_ampm h.nn_ampm h nn_ampm h-nn_ampm h_ampm NOTE: Any time format without a meridian token (the 'ampm' token) is considered in 24 hour time. === Format Tokens Here is what each format token means: Format tokens: y = year m = month d = day h = hour n = minute s = second u = micro-seconds ampm = meridian (am or pm) with or without dots (e.g. am, a.m, or a.m.) _ = optional space tz = Timezone abbreviation (e.g. UTC, GMT, PST, EST) zo = Timezone offset (e.g. +10:00, -08:00, +1000) Repeating tokens: x = 1 or 2 digits for unit (e.g. 'h' means an hour can be '9' or '09') xx = 2 digits exactly for unit (e.g. 'hh' means an hour can only be '09') Special Cases: yy = 2 or 4 digit year yyyy = exactly 4 digit year mmm = month long name (e.g. 'Jul' or 'July') ddd = Day name of 3 to 9 letters (e.g. Wed or Wednesday) u = microseconds matches 1 to 3 digits All other characters are considered literal. For the technically minded, these formats are compiled into a single regular expression To see all defined formats look at the {source code}[http://github.com/adzap/timeliness/tree/master/lib/timeliness/formats.rb]. == Settings === US/Euro Formats The perennial problem for non-US developers or applications not primarily for the US, is the US date format of m/d/yy. This is ambiguous with the European format of d/m/yy. By default the gem uses the US formats as this is the Ruby default when it does date interpretation. To switch to using the European (or Rest of The World) formats use this setting Timeliness.use_euro_formats Now '01/02/2000' will be parsed as 1st February 2000, instead of 2nd January 2000. You can switch back to US formats with Timeliness.use_us_formats === Customising Formats Sometimes you may not want certain formats to be valid. You can remove formats for each type and the parser will then not consider that a valid format. To remove a format Timeliness.remove_formats(:date, 'm\d\yy') Adding new formats is also simple Timeliness.add_formats(:time, "h o'clock") Now "10 o'clock" will be a valid value. You can embed regular expressions in the format but no guarantees that it will remain intact. If you avoid the use of any token characters, and regexp dots or backslashes as special characters in the regexp, it may work as expected. For special characters use POSIX character classes for safety. See the ISO 8601 datetime for an example of an embedded regular expression. Because formats are evaluated in order, adding a format which may be ambiguous with an existing format, will mean your format is ignored. If you need to make your new format higher precedence than an existing format, you can include the before option like so Timeliness.add_formats(:time, 'ss:nn:hh', :before => 'hh:nn:ss') Now a time of '59:30:23' will be interpreted as 11:30:59 pm. This option saves you adding a new one and deleting an old one to get it to work. === Ambiguous Year When dealing with 2 digit year values, by default a year is interpreted as being in the last century when at or above 30. You can customize this however Timeliness.ambiguous_year_threshold = 20 Now you get: year of 19 is considered 2019 year of 20 is considered 1920 == Credits * Adam Meehan (adam.meehan@gmail.com, http://github.com/adzap) == License Copyright (c) 2010 Adam Meehan, released under the MIT license timeliness-0.3.10/Rakefile000066400000000000000000000011421342641020700154110ustar00rootroot00000000000000require 'bundler' Bundler::GemHelper.install_tasks require 'rdoc/task' require 'rspec/core/rake_task' desc "Run specs" RSpec::Core::RakeTask.new(:spec) desc "Generate code coverage" RSpec::Core::RakeTask.new(:coverage) do |t| t.rcov = true t.rcov_opts = ['--exclude', 'spec'] end desc 'Generate documentation for plugin.' Rake::RDocTask.new(:rdoc) do |rdoc| rdoc.rdoc_dir = 'rdoc' rdoc.title = 'Timeliness' rdoc.options << '--line-numbers' << '--inline-source' rdoc.rdoc_files.include('README') rdoc.rdoc_files.include('lib/**/*.rb') end desc 'Default: run specs.' task :default => :spec timeliness-0.3.10/benchmark.rb000066400000000000000000000071731342641020700162350ustar00rootroot00000000000000$:.unshift(File.expand_path('lib')) require 'benchmark' require 'time' require 'parsedate' unless RUBY_VERSION =~ /^1\.9\./ require 'timeliness' if defined?(JRUBY_VERSION) # Warm up JRuby 20_000.times do Time.parse("2000-01-04 12:12:12") Timeliness::Parser.parse("2000-01-04 12:12:12", :datetime) end end n = 10_000 Benchmark.bm do |x| x.report('timeliness - datetime') { n.times do Timeliness::Parser.parse("2000-01-04 12:12:12", :datetime) end } x.report('timeliness - datetime with :format') { n.times do Timeliness::Parser.parse("2000-01-04 12:12:12", :datetime, :format => 'yyyy-mm-dd hh:nn:ss') end } x.report('timeliness - date') { n.times do Timeliness::Parser.parse("2000-01-04", :date) end } x.report('timeliness - date as datetime') { n.times do Timeliness::Parser.parse("2000-01-04", :datetime) end } x.report('timeliness - time') { n.times do Timeliness::Parser.parse("12:01:02", :time) end } x.report('timeliness - no type with datetime value') { n.times do Timeliness::Parser.parse("2000-01-04 12:12:12") end } x.report('timeliness - no type with date value') { n.times do Timeliness::Parser.parse("2000-01-04") end } x.report('timeliness - no type with time value') { n.times do Timeliness::Parser.parse("12:01:02") end } x.report('timeliness - invalid format datetime') { n.times do Timeliness::Parser.parse("20xx-01-04 12:12:12", :datetime) end } x.report('timeliness - invalid format date') { n.times do Timeliness::Parser.parse("20xx-01-04", :date) end } x.report('timeliness - invalid format time') { n.times do Timeliness::Parser.parse("12:xx:02", :time) end } x.report('timeliness - invalid value datetime') { n.times do Timeliness::Parser.parse("2000-01-32 12:12:12", :datetime) end } x.report('timeliness - invalid value date') { n.times do Timeliness::Parser.parse("2000-01-32", :date) end } x.report('timeliness - invalid value time') { n.times do Timeliness::Parser.parse("12:61:02", :time) end } x.report('ISO regexp for datetime') { n.times do "2000-01-04 12:12:12" =~ /\A(\d{4})-(\d{2})-(\d{2}) (\d{2})[\. :](\d{2})([\. :](\d{2}))?\Z/ Time.mktime($1.to_i, $2.to_i, $3.to_i, $3.to_i, $5.to_i, $6.to_i) end } x.report('Time.parse - valid') { n.times do Time.parse("2000-01-04 12:12:12") end } x.report('Time.parse - invalid ') { n.times do Time.parse("2000-01-32 12:12:12") rescue nil end } x.report('Date._parse - valid') { n.times do hash = Date._parse("2000-01-04 12:12:12") Time.mktime(hash[:year], hash[:mon], hash[:mday], hash[:hour], hash[:min], hash[:sec]) end } x.report('Date._parse - invalid ') { n.times do hash = Date._parse("2000-01-32 12:12:12") Time.mktime(hash[:year], hash[:mon], hash[:mday], hash[:hour], hash[:min], hash[:sex]) rescue nil end } if defined?(ParseDate) x.report('parsedate - valid') { n.times do arr = ParseDate.parsedate("2000-01-04 12:12:12") Date.new(*arr[0..2]) Time.mktime(*arr) end } x.report('parsedate - invalid ') { n.times do arr = ParseDate.parsedate("2000-00-04 12:12:12") end } end x.report('strptime - valid') { n.times do DateTime.strptime("2000-01-04 12:12:12", '%Y-%m-%d %H:%M:%s') end } x.report('strptime - invalid') { n.times do DateTime.strptime("2000-00-04 12:12:12", '%Y-%m-%d %H:%M:%s') rescue nil end } end timeliness-0.3.10/lib/000077500000000000000000000000001342641020700145145ustar00rootroot00000000000000timeliness-0.3.10/lib/timeliness.rb000066400000000000000000000020731342641020700172170ustar00rootroot00000000000000require 'date' require 'forwardable' require 'timeliness/helpers' require 'timeliness/definitions' require 'timeliness/format' require 'timeliness/format_set' require 'timeliness/parser' require 'timeliness/version' module Timeliness class << self extend Forwardable def_delegators Parser, :parse, :_parse def_delegators Definitions, :add_formats, :remove_formats, :use_us_formats, :use_euro_formats attr_accessor :default_timezone, :date_for_time_type, :ambiguous_year_threshold end # Default timezone. Options: # - :local (default) # - :utc # # If ActiveSupport loaded, also # - :current # - 'Zone name' # self.default_timezone = :local # Set the default date part for a time type values. # self.date_for_time_type = lambda { Time.now } # Set the threshold value for a two digit year to be considered last century # # Default: 30 # # Example: # year = '29' is considered 2029 # year = '30' is considered 1930 # self.ambiguous_year_threshold = 30 end Timeliness::Definitions.compile_formats timeliness-0.3.10/lib/timeliness/000077500000000000000000000000001342641020700166705ustar00rootroot00000000000000timeliness-0.3.10/lib/timeliness/core_ext.rb000066400000000000000000000001231342641020700210210ustar00rootroot00000000000000require 'timeliness/core_ext/string' module Timeliness module CoreExt end end timeliness-0.3.10/lib/timeliness/core_ext/000077500000000000000000000000001342641020700205005ustar00rootroot00000000000000timeliness-0.3.10/lib/timeliness/core_ext/string.rb000066400000000000000000000011611342641020700223320ustar00rootroot00000000000000class String # Form can be either :utc (default) or :local. def to_time(form = :utc) return nil if self.blank? Timeliness::Parser.parse(self, :datetime, :zone => form) end def to_date return nil if self.blank? values = Timeliness::Parser._parse(self, :date).map { |arg| arg || 0 } ::Date.new(*values[0..2]) end def to_datetime return nil if self.blank? values = Timeliness::Parser._parse(self, :datetime).map { |arg| arg || 0 } values[7] = values[7]/24.hours.to_f if values[7] != 0 values[5] += Rational(values.delete_at(6), 1000000) ::DateTime.civil(*values) end end timeliness-0.3.10/lib/timeliness/definitions.rb000066400000000000000000000206761342641020700215430ustar00rootroot00000000000000module Timeliness module Definitions # Format tokens: # y = year # m = month # d = day # h = hour # n = minute # s = second # u = micro-seconds # ampm = meridian (am or pm) with or without dots (e.g. am, a.m, or a.m.) # _ = optional space # tz = Timezone abbreviation (e.g. UTC, GMT, PST, EST) # zo = Timezone offset (e.g. +10:00, -08:00, +1000) # # All other characters are considered literal. You can embed regexp in the # format but no guarantees that it will remain intact. If you don't use capture # groups, dots or backslashes in the regexp, it may well work as expected. # For special characters, use POSIX character classes for safety. # # Repeating tokens: # x = 1 or 2 digits for unit (e.g. 'h' means an hour can be '9' or '09') # xx = 2 digits exactly for unit (e.g. 'hh' means an hour can only be '09') # # Special Cases: # yy = 2 or 4 digit year # yyyy = exactly 4 digit year # mmm = month long name (e.g. 'Jul' or 'July') # ddd = Day name of 3 to 9 letters (e.g. Wed or Wednesday) # u = microseconds matches 1 to 6 digits @time_formats = [ 'hh:nn:ss', 'hh-nn-ss', 'h:nn', 'h.nn', 'h nn', 'h-nn', 'h:nn_ampm', 'h.nn_ampm', 'h nn_ampm', 'h-nn_ampm', 'h_ampm' ] @date_formats = [ 'yyyy-mm-dd', 'yyyy/mm/dd', 'yyyy.mm.dd', 'm/d/yy', 'd/m/yy', 'm\d\yy', 'd\m\yy', 'd-m-yy', 'dd-mm-yyyy', 'd.m.yy', 'd mmm yy' ] @datetime_formats = [ 'yyyy-mm-dd hh:nn:ss.u', 'yyyy-mm-dd hh:nn:ss', 'yyyy-mm-dd h:nn', 'yyyy-mm-dd h:nn_ampm', 'm/d/yy h:nn:ss', 'm/d/yy h:nn_ampm', 'm/d/yy h:nn', 'd/m/yy hh:nn:ss', 'd/m/yy h:nn_ampm', 'd/m/yy h:nn', 'dd-mm-yyyy hh:nn:ss', 'dd-mm-yyyy h:nn_ampm', 'dd-mm-yyyy h:nn', 'ddd, dd mmm yyyy hh:nn:ss tz', # RFC 822 'ddd, dd mmm yyyy hh:nn:ss zo', # RFC 822 'ddd mmm d hh:nn:ss zo yyyy', # Ruby time string 'yyyy-mm-ddThh:nn:ssZ', # ISO 8601 without zone offset 'yyyy-mm-ddThh:nn:sszo', # ISO 8601 with zone offset 'yyyy-mm-ddThh:nn:ss.u', # ISO 8601 with usec 'yyyy-mm-ddThh:nn:ss.uzo', # ISO 8601 with usec and offset 'yyyy-mm-dd hh:nn:ss zo', # Ruby time string in later versions 'yyyy-mm-dd hh:nn:ss tz', # Ruby time string for UTC in later versions ] # All tokens available for format construction. The token array is made of # regexp and key for format component mapping, if any. # @format_tokens = { 'ddd' => [ '\w{3,9}' ], 'dd' => [ '\d{2}', :day ], 'd' => [ '\d{1,2}', :day ], 'mmm' => [ '\w{3,9}', :month ], 'mm' => [ '\d{2}', :month ], 'm' => [ '\d{1,2}', :month ], 'yyyy' => [ '\d{4}', :year ], 'yy' => [ '\d{4}|\d{2}', :year ], 'hh' => [ '\d{2}', :hour ], 'h' => [ '\d{1,2}', :hour ], 'nn' => [ '\d{2}', :min ], 'n' => [ '\d{1,2}', :min ], 'ss' => [ '\d{2}', :sec ], 's' => [ '\d{1,2}', :sec ], 'u' => [ '\d{1,6}', :usec ], 'ampm' => [ '[aApP]\.?[mM]\.?', :meridian ], 'zo' => [ '[+-]\d{2}:?\d{2}', :offset ], 'tz' => [ '[A-Z]{1,5}', :zone ], '_' => [ '\s?' ] } # Component argument values will be passed to the format method if matched in # the time string. The key should match the key defined in the format tokens. # # The array consists of the position the value should be inserted in # the time array, and the code to place in the time array. # # If the position is nil, then the value won't be put in the time array. If the # code is nil, then just the raw value is used. # @format_components = { :year => [ 0, 'unambiguous_year(year)'], :month => [ 1, 'month_index(month)'], :day => [ 2 ], :hour => [ 3, 'full_hour(hour, meridian ||= nil)'], :min => [ 4 ], :sec => [ 5 ], :usec => [ 6, 'microseconds(usec)'], :offset => [ 7, 'offset_in_seconds(offset)'], :zone => [ 7, 'zone'], :meridian => [ nil ] } # Mapping some common timezone abbreviations which are not mapped or # mapped inconsistenly in ActiveSupport (TzInfo). # @timezone_mapping = { 'AEST' => 'Australia/Sydney', 'AEDT' => 'Australia/Sydney', 'ACST' => 'Australia/Adelaide', 'ACDT' => 'Australia/Adelaide', 'PST' => 'PST8PDT', 'PDT' => 'PST8PDT', 'CST' => 'CST6CDT', 'CDT' => 'CST6CDT', 'EDT' => 'EST5EDT', 'MDT' => 'MST7MDT' } US_FORMAT_REGEXP = /\Am{1,2}[^m]/ FormatNotFound = Class.new(StandardError) DuplicateFormat = Class.new(StandardError) class << self attr_accessor :time_formats, :date_formats, :datetime_formats, :format_tokens, :format_components, :timezone_mapping attr_reader :date_format_set, :time_format_set, :datetime_format_set # Adds new formats. Must specify format type and can specify a :before # option to nominate which format the new formats should be inserted in # front on to take higher precedence. # # Error is raised if format already exists or if :before format is not found. # def add_formats(type, *add_formats) formats = send("#{type}_formats") options = add_formats.last.is_a?(Hash) ? add_formats.pop : {} before = options[:before] raise FormatNotFound, "Format for :before option #{before.inspect} was not found." if before && !formats.include?(before) add_formats.each do |format| raise DuplicateFormat, "Format #{format.inspect} is already included in #{type.inspect} formats" if formats.include?(format) index = before ? formats.index(before) : -1 formats.insert(index, format) end compile_formats end # Delete formats of specified type. Error raised if format not found. # def remove_formats(type, *remove_formats) remove_formats.each do |format| unless send("#{type}_formats").delete(format) raise FormatNotFound, "Format #{format.inspect} not found in #{type.inspect} formats" end end compile_formats end # Removes US date formats so that ambiguous dates are parsed as European format # def use_euro_formats @date_format_set = @euro_date_format_set @datetime_format_set = @euro_datetime_format_set end # Restores default to parse ambiguous dates as US format # def use_us_formats @date_format_set = @us_date_format_set @datetime_format_set = @us_datetime_format_set end def compile_formats @sorted_token_keys = nil @time_format_set = FormatSet.compile(time_formats) @us_date_format_set = FormatSet.compile(date_formats) @us_datetime_format_set = FormatSet.compile(datetime_formats) @euro_date_format_set = FormatSet.compile(date_formats.select { |format| US_FORMAT_REGEXP !~ format }) @euro_datetime_format_set = FormatSet.compile(datetime_formats.select { |format| US_FORMAT_REGEXP !~ format }) @date_format_set = @us_date_format_set @datetime_format_set = @us_datetime_format_set end def sorted_token_keys @sorted_token_keys ||= format_tokens.keys.sort {|a,b| a.size <=> b.size }.reverse end # Returns format for type and other possible matching format set based on type # and value length. Gives minor speed-up by checking string length. # def format_sets(type, string) case type when :date [ @date_format_set, @datetime_format_set ] when :datetime if string.length < 11 [ @date_format_set, @datetime_format_set ] else [ @datetime_format_set, @date_format_set ] end when :time if string.length < 11 [ @time_format_set ] else [ @datetime_format_set, @time_format_set ] end else if string.length < 11 [ @date_format_set, @time_format_set, @datetime_format_set ] else [ @datetime_format_set, @date_format_set, @time_format_set ] end end end end end end timeliness-0.3.10/lib/timeliness/format.rb000066400000000000000000000036531342641020700205140ustar00rootroot00000000000000module Timeliness class Format include Helpers CompilationFailed = Class.new(StandardError) attr_reader :format_string, :regexp, :regexp_string, :token_count def initialize(format_string) @format_string = format_string end def compile! @token_count = 0 format = format_string.dup format.gsub!(/([\.\\])/, '\\\\\1') # escapes dots and backslashes found_tokens, token_order = [], [] # Substitute tokens with numbered placeholder Definitions.sorted_token_keys.each do |token| token_regexp_str, arg_key = Definitions.format_tokens[token] if format.gsub!(/#{token}/, "%<#{found_tokens.size}>") if arg_key token_regexp_str = "(#{token_regexp_str})" @token_count += 1 end found_tokens << [token_regexp_str, arg_key] end end # Replace placeholders with token regexps format.scan(/%<(\d)>/).each {|token_index| token_index = token_index.first token_regexp_str, arg_key = found_tokens[token_index.to_i] format.gsub!("%<#{token_index}>", token_regexp_str) token_order << arg_key } define_process_method(token_order.compact) @regexp_string = format @regexp = Regexp.new("^(#{format})$") self rescue => ex raise CompilationFailed, "The format '#{format_string}' failed to compile using regexp string #{format}. Error message: #{ex.inspect}" end # Redefined on compile def process(*args); end private def define_process_method(components) values = [nil] * 8 components.each do |component| position, code = Definitions.format_components[component] values[position] = code || "#{component}.to_i" if position end instance_eval <<-DEF def process(#{components.join(',')}) [#{values.map {|i| i || 'nil' }.join(',')}] end DEF end end end timeliness-0.3.10/lib/timeliness/format_set.rb000066400000000000000000000026131342641020700213620ustar00rootroot00000000000000module Timeliness class FormatSet attr_reader :formats, :regexp def self.compile(formats) new(formats).compile! end def initialize(formats) @formats = formats @formats_hash = {} @match_indexes = {} end # Compiles the formats into one big regexp. Stores the index of where # each format's capture values begin in the matchdata. def compile! regexp_string = '' @formats.inject(0) { |index, format_string| format = Format.new(format_string).compile! @formats_hash[format_string] = format @match_indexes[index] = format regexp_string = "#{regexp_string}(#{format.regexp_string})|" index + format.token_count + 1 # add one for wrapper capture } @regexp = %r[\A(?:#{regexp_string.chop})\z] self end def match(string, format_string=nil) format = single_format(format_string) if format_string match_regexp = format && format.regexp || @regexp if match_data = match_regexp.match(string) index = match_data.captures.index(string) start = index + 1 values = match_data.captures[start..(start+7)].compact format ||= @match_indexes[index] format.process(*values) end end def single_format(format_string) @formats_hash.fetch(format_string) { Format.new(format_string).compile! } end end end timeliness-0.3.10/lib/timeliness/helpers.rb000066400000000000000000000025031342641020700206570ustar00rootroot00000000000000module Timeliness module Helpers def full_hour(hour, meridian) hour = hour.to_i return hour if meridian.nil? if meridian.delete('.').downcase == 'am' raise(ArgumentError) if hour == 0 || hour > 12 hour == 12 ? 0 : hour else hour == 12 ? hour : hour + 12 end end def unambiguous_year(year) if year.length <= 2 century = Time.now.year.to_s[0..1].to_i century -= 1 if year.to_i >= Timeliness.ambiguous_year_threshold year = "#{century}#{year.rjust(2,'0')}" end year.to_i end def month_index(month) return month.to_i if month.to_i > 0 || /0+/ =~ month month.length > 3 ? month_names.index(month.capitalize) : abbr_month_names.index(month.capitalize) end def month_names i18n_loaded? ? I18n.t('date.month_names') : Date::MONTHNAMES end def abbr_month_names i18n_loaded? ? I18n.t('date.abbr_month_names') : Date::ABBR_MONTHNAMES end def microseconds(usec) (".#{usec}".to_f * 1_000_000).to_i end def offset_in_seconds(offset) sign = offset =~ /^-/ ? -1 : 1 parts = offset.scan(/\d\d/).map {|p| p.to_f } parts[1] = parts[1].to_f / 60 (parts[0] + parts[1]) * sign * 3600 end def i18n_loaded? defined?(I18n) end end end timeliness-0.3.10/lib/timeliness/parser.rb000066400000000000000000000120521342641020700205110ustar00rootroot00000000000000module Timeliness module Parser class MissingTimezoneSupport < StandardError; end class << self def parse(value, *args) return value if acts_like_temporal?(value) return nil unless parseable?(value) type, options = type_and_options_from_args(args) time_array = _parse(value, type, options) return nil if time_array.nil? default_values_by_type(time_array, type, options) unless type == :datetime make_time(time_array[0..7], options[:zone]) rescue NoMethodError => ex raise ex unless ex.message =~ /undefined method `(zone|use_zone|current)' for Time:Class/ raise MissingTimezoneSupport, "ActiveSupport timezone support must be loaded to use timezones other than :utc and :local." end def make_time(time_array, zone_option=nil) return nil unless fast_date_valid_with_fallback(*time_array[0..2]) zone, offset = zone_and_offset(time_array[7]) if time_array[7] value = create_time_in_zone(time_array[0..6].compact, zone || zone_option) value = shift_time_to_zone(value, zone_option) if zone return nil unless value offset ? value + (value.utc_offset - offset) : value rescue ArgumentError, TypeError nil end def _parse(string, type=nil, options={}) if options[:strict] && type Definitions.send("#{type}_format_set").match(string, options[:format]) else values = nil Definitions.format_sets(type, string).find {|set| values = set.match(string, options[:format]) } values end rescue nil end private def parseable?(value) value.is_a?(String) end def acts_like_temporal?(value) value.is_a?(Time) || value.is_a?(Date) || value.respond_to?(:acts_like_date?) || value.respond_to?(:acts_like_time?) end def type_and_options_from_args(args) options = args.last.is_a?(Hash) ? args.pop : {} type_or_now = args.first if type_or_now.is_a?(Symbol) type = type_or_now elsif type_or_now options[:now] = type_or_now end return type, options end def default_values_by_type(values, type, options) case type when :date values[3..7] = nil when :time values[0..2] = current_date(options) when nil dummy_date = current_date(options) values[0] ||= dummy_date[0] values[1] ||= dummy_date[1] unless values.values_at(0,2).all? values[2] ||= dummy_date[2] end end def current_date(options) now = if options[:now] options[:now] elsif options[:zone] current_time_in_zone(options[:zone]) else evaluate_date_for_time_type end now.is_a?(Array) ? now[0..2] : [now.year, now.month, now.day] end def current_time_in_zone(zone) case zone when :utc, :local Time.now.send("get#{zone}") when :current Time.current else Time.use_zone(zone) { Time.current } end end def shift_time_to_zone(time, zone=nil) zone ||= Timeliness.default_timezone case zone when :utc, :local time.send("get#{zone}") when :current time.in_time_zone else Time.use_zone(zone) { time.in_time_zone } end end def create_time_in_zone(time_array, zone=nil) zone ||= Timeliness.default_timezone case zone when :utc, :local time_with_datetime_fallback(zone, *time_array) when :current Time.zone.local(*time_array) else Time.use_zone(zone) { Time.zone.local(*time_array) } end end def zone_and_offset(parsed_value) if parsed_value.is_a?(String) zone = Definitions.timezone_mapping[parsed_value] || parsed_value else offset = parsed_value end return zone, offset end # Taken from ActiveSupport and simplified def time_with_datetime_fallback(utc_or_local, year, month=1, day=1, hour=0, min=0, sec=0, usec=0) return nil if hour > 23 || min > 59 || sec > 59 ::Time.send(utc_or_local, year, month, day, hour, min, sec, usec) rescue offset = utc_or_local == :local ? (::Time.local(2007).utc_offset.to_r/86400) : 0 ::DateTime.civil(year, month, day, hour, min, sec, offset) end # Enforce strict date part validity which the Time class does not. # Only does full date check if month and day are possibly invalid. def fast_date_valid_with_fallback(year, month, day) month && month < 13 && (day < 29 || Date.valid_civil?(year, month, day)) end def evaluate_date_for_time_type case Timeliness.date_for_time_type when Array Timeliness.date_for_time_type when Proc v = Timeliness.date_for_time_type.call [v.year, v.month, v.day] end end end end end timeliness-0.3.10/lib/timeliness/version.rb000066400000000000000000000000531342641020700207000ustar00rootroot00000000000000module Timeliness VERSION = '0.3.10' end timeliness-0.3.10/spec/000077500000000000000000000000001342641020700147005ustar00rootroot00000000000000timeliness-0.3.10/spec/spec_helper.rb000066400000000000000000000105751342641020700175260ustar00rootroot00000000000000# This file was generated by the `rspec --init` command. Conventionally, all # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. # The generated `.rspec` file contains `--require spec_helper` which will cause # this file to always be loaded, without a need to explicitly require it in any # files. # # Given that it is always loaded, you are encouraged to keep this file as # light-weight as possible. Requiring heavyweight dependencies from this file # will add to the boot time of your test suite on EVERY test run, even for an # individual file that may not need all of that loaded. Instead, consider making # a separate helper file that requires the additional dependencies and performs # the additional setup, and require it from the spec files that actually need # it. # # The `.rspec` file also contains a few flags that are not defaults but that # users commonly want. # # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration RSpec.configure do |config| # rspec-expectations config goes here. You can use an alternate # assertion/expectation library such as wrong or the stdlib/minitest # assertions if you prefer. config.expect_with :rspec do |expectations| # This option will default to `true` in RSpec 4. It makes the `description` # and `failure_message` of custom matchers include text for helper methods # defined using `chain`, e.g.: # be_bigger_than(2).and_smaller_than(4).description # # => "be bigger than 2 and smaller than 4" # ...rather than: # # => "be bigger than 2" expectations.include_chain_clauses_in_custom_matcher_descriptions = true end # rspec-mocks config goes here. You can use an alternate test double # library (such as bogus or mocha) by changing the `mock_with` option here. config.mock_with :rspec do |mocks| # Prevents you from mocking or stubbing a method that does not exist on # a real object. This is generally recommended, and will default to # `true` in RSpec 4. mocks.verify_partial_doubles = true end # The settings below are suggested to provide a good initial experience # with RSpec, but feel free to customize to your heart's content. =begin # These two settings work together to allow you to limit a spec run # to individual examples or groups you care about by tagging them with # `:focus` metadata. When nothing is tagged with `:focus`, all examples # get run. config.filter_run :focus config.run_all_when_everything_filtered = true # Allows RSpec to persist some state between runs in order to support # the `--only-failures` and `--next-failure` CLI options. We recommend # you configure your source control system to ignore this file. config.example_status_persistence_file_path = "spec/examples.txt" # Limits the available syntax to the non-monkey patched syntax that is # recommended. For more details, see: # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode config.disable_monkey_patching! # This setting enables warnings. It's recommended, but in some cases may # be too noisy due to issues in dependencies. config.warnings = true # Many RSpec users commonly either run the entire suite or an individual # file, and it's useful to allow more verbose output when running an # individual spec file. if config.files_to_run.one? # Use the documentation formatter for detailed output, # unless a formatter has already been configured # (e.g. via a command-line flag). config.default_formatter = 'doc' end # Print the 10 slowest examples and example groups at the # end of the spec run, to help surface which specs are running # particularly slow. config.profile_examples = 10 # Run specs in random order to surface order dependencies. If you find an # order dependency and want to debug it, you can fix the order by providing # the seed, which is printed after each run. # --seed 1234 config.order = :random # Seed global randomization in this process using the `--seed` CLI option. # Setting this allows you to use `--seed` to deterministically reproduce # test failures related to randomization by passing the same `--seed` value # as the one that triggered the failure. Kernel.srand config.seed =end end timeliness-0.3.10/spec/timeliness/000077500000000000000000000000001342641020700170545ustar00rootroot00000000000000timeliness-0.3.10/spec/timeliness/core_ext/000077500000000000000000000000001342641020700206645ustar00rootroot00000000000000timeliness-0.3.10/spec/timeliness/core_ext/string_spec.rb000066400000000000000000000044601342641020700235350ustar00rootroot00000000000000describe Timeliness::CoreExt, 'String' do # Test values taken from ActiveSupport unit tests for compatibility describe "#to_time" do it 'should convert valid string to Time object in default zone' do expect("2005-02-27 23:50".to_time).to eq Time.utc(2005, 2, 27, 23, 50) end it 'should convert ISO 8601 string to Time object' do expect("2005-02-27T23:50:19.275038".to_time).to eq Time.utc(2005, 2, 27, 23, 50, 19, 275038) end context "with :local" do it 'should convert valid string to local time' do expect("2005-02-27 23:50".to_time(:local)).to eq Time.local(2005, 2, 27, 23, 50) end it 'should convert ISO 8601 string to local time' do expect("2005-02-27T23:50:19.275038".to_time(:local)).to eq Time.local(2005, 2, 27, 23, 50, 19, 275038) end end it 'should convert valid future string to Time object' do expect("2039-02-27 23:50".to_time(:local)).to eq Time.local(2039, 2, 27, 23, 50) end it 'should convert valid future string to Time object' do expect("2039-02-27 23:50".to_time).to eq DateTime.civil(2039, 2, 27, 23, 50) end it 'should convert empty string to nil' do expect(''.to_time).to be_nil end end describe "#to_datetime" do it 'should convert valid string to DateTime object' do expect("2039-02-27 23:50".to_datetime).to eq DateTime.civil(2039, 2, 27, 23, 50) end it 'should convert to DateTime object with UTC offset' do expect("2039-02-27 23:50".to_datetime.offset).to eq 0 end it 'should convert ISO 8601 string to DateTime object' do datetime = DateTime.civil(2039, 2, 27, 23, 50, 19 + Rational(275038, 1000000), "-04:00") expect("2039-02-27T23:50:19.275038-04:00".to_datetime).to eq datetime end it 'should use Rubys default start value' do # Taken from ActiveSupport unit tests. Not sure on the implication. expect("2039-02-27 23:50".to_datetime.start).to eq ::Date::ITALY end it 'should convert empty string to nil' do expect(''.to_datetime).to be_nil end end describe "#to_date" do it 'should convert string to Date object' do expect("2005-02-27".to_date).to eq Date.new(2005, 2, 27) end it 'should convert empty string to nil' do expect(''.to_date).to be_nil end end end timeliness-0.3.10/spec/timeliness/definitions_spec.rb000066400000000000000000000067011342641020700227320ustar00rootroot00000000000000describe Timeliness::Definitions do context "add_formats" do before do @default_formats = definitions.time_formats.dup end it "should add format to format array" do definitions.add_formats(:time, "h o'clock") expect(definitions.time_formats).to include("h o'clock") end it "should parse new format after its added" do should_not_parse("12 o'clock", :time) definitions.add_formats(:time, "h o'clock") should_parse("12 o'clock", :time) end it "should raise error if format exists" do expect { definitions.add_formats(:time, "hh:nn:ss") }.to raise_error(Timeliness::Definitions::DuplicateFormat) end context "with :before option" do it "should add new format with higher precedence" do definitions.add_formats(:time, "ss:hh:nn", :before => 'hh:nn:ss') time_array = parser._parse('59:23:58', :time) expect(time_array).to eq [nil,nil,nil,23,58,59,nil,nil] end it "should raise error if :before format does not exist" do expect { definitions.add_formats(:time, "ss:hh:nn", :before => 'nn:hh:ss') }.to raise_error(Timeliness::Definitions::FormatNotFound) end end after do definitions.time_formats = @default_formats definitions.compile_formats end end context "remove_formats" do before do @default_formats = definitions.time_formats.dup end it "should remove a single format from the formats array for type" do definitions.remove_formats(:time, 'h.nn_ampm') expect(definitions.time_formats).not_to include('h.nn_ampm') end it "should remove multiple formats from formats array for type" do definitions.remove_formats(:time, 'h:nn', 'h.nn_ampm') expect(definitions.time_formats).not_to include('h:nn') expect(definitions.time_formats).not_to include('h.nn_ampm') end it "should prevent parsing of removed format" do should_parse('2.12am', :time) definitions.remove_formats(:time, 'h.nn_ampm') should_not_parse('2.12am', :time) end it "should raise error if format does not exist" do expect { definitions.remove_formats(:time, "ss:hh:nn") }.to raise_error(Timeliness::Definitions::FormatNotFound) end after do definitions.time_formats = @default_formats definitions.compile_formats end end context "use_euro_formats" do it "should allow ambiguous date to be parsed as European format" do expect(parser._parse('01/02/2000', :date)).to eq [2000,1,2,nil,nil,nil,nil,nil] definitions.use_euro_formats expect(parser._parse('01/02/2000', :date)).to eq [2000,2,1,nil,nil,nil,nil,nil] end it "should not parse formats on switch to euro after initial compile" do definitions.compile_formats expect(Timeliness::FormatSet).not_to receive(:compile) definitions.use_euro_formats end end context "use_us_formats" do before do definitions.use_euro_formats end it "should allow ambiguous date to be parsed as European format" do expect(parser._parse('01/02/2000', :date)).to eq [2000,2,1,nil,nil,nil,nil,nil] definitions.use_us_formats expect(parser._parse('01/02/2000', :date)).to eq [2000,1,2,nil,nil,nil,nil,nil] end it "should not parse formats on switch to euro after initial compile" do definitions.compile_formats expect(Timeliness::FormatSet).not_to receive(:compile) definitions.use_us_formats end end end timeliness-0.3.10/spec/timeliness/format_set_spec.rb000066400000000000000000000112331342641020700225560ustar00rootroot00000000000000describe Timeliness::FormatSet do context "#compile!" do let(:set) { Timeliness::FormatSet.new(['yyyy-mm-dd', 'dd/mm/yyyy']) } it 'should set the regexp for the set' do set.compile! expect(set.regexp).not_to be_nil end end context "compiled regexp" do context "for time formats" do format_tests = { 'hh:nn:ss' => {:pass => ['12:12:12', '01:01:01'], :fail => ['1:12:12', '12:1:12', '12:12:1', '12-12-12']}, 'hh-nn-ss' => {:pass => ['12-12-12', '01-01-01'], :fail => ['1-12-12', '12-1-12', '12-12-1', '12:12:12']}, 'h:nn' => {:pass => ['12:12', '1:01'], :fail => ['12:2', '12-12']}, 'h.nn' => {:pass => ['2.12', '12.12'], :fail => ['2.1', '12:12']}, 'h nn' => {:pass => ['2 12', '12 12'], :fail => ['2 1', '2.12', '12:12']}, 'h-nn' => {:pass => ['2-12', '12-12'], :fail => ['2-1', '2.12', '12:12']}, 'h:nn_ampm' => {:pass => ['2:12am', '2:12 pm', '2:12 AM', '2:12PM'], :fail => ['1:2am', '1:12 pm', '2.12am']}, 'h.nn_ampm' => {:pass => ['2.12am', '2.12 pm'], :fail => ['1:2am', '1:12 pm', '2:12am']}, 'h nn_ampm' => {:pass => ['2 12am', '2 12 pm'], :fail => ['1 2am', '1 12 pm', '2:12am']}, 'h-nn_ampm' => {:pass => ['2-12am', '2-12 pm'], :fail => ['1-2am', '1-12 pm', '2:12am']}, 'h_ampm' => {:pass => ['2am', '2 am', '12 pm'], :fail => ['1.am', '12 pm', '2:12am']}, } format_tests.each do |format, values| it "should correctly match times in format '#{format}'" do regexp = compile_regexp(format) values[:pass].each {|value| expect(value).to match(regexp)} values[:fail].each {|value| expect(value).not_to match(regexp)} end end end context "for date formats" do format_tests = { 'yyyy/mm/dd' => {:pass => ['2000/02/01'], :fail => ['2000\02\01', '2000/2/1', '00/02/01']}, 'yyyy-mm-dd' => {:pass => ['2000-02-01'], :fail => ['2000\02\01', '2000-2-1', '00-02-01']}, 'yyyy.mm.dd' => {:pass => ['2000.02.01'], :fail => ['2000\02\01', '2000.2.1', '00.02.01']}, 'm/d/yy' => {:pass => ['2/1/01', '02/01/00', '02/01/2000'], :fail => ['2/1/0', '2.1.01']}, 'd/m/yy' => {:pass => ['1/2/01', '01/02/00', '01/02/2000'], :fail => ['1/2/0', '1.2.01']}, 'm\d\yy' => {:pass => ['2\1\01', '2\01\00', '02\01\2000'], :fail => ['2\1\0', '2/1/01']}, 'd\m\yy' => {:pass => ['1\2\01', '1\02\00', '01\02\2000'], :fail => ['1\2\0', '1/2/01']}, 'd-m-yy' => {:pass => ['1-2-01', '1-02-00', '01-02-2000'], :fail => ['1-2-0', '1/2/01']}, 'd.m.yy' => {:pass => ['1.2.01', '1.02.00', '01.02.2000'], :fail => ['1.2.0', '1/2/01']}, 'd mmm yy' => {:pass => ['1 Feb 00', '1 Feb 2000', '1 February 00', '01 February 2000'], :fail => ['1 Fe 00', 'Feb 1 2000', '1 Feb 0']} } format_tests.each do |format, values| it "should correctly match dates in format '#{format}'" do regexp = compile_regexp(format) values[:pass].each {|value| expect(value).to match(regexp)} values[:fail].each {|value| expect(value).not_to match(regexp)} end end end context "for datetime formats" do format_tests = { 'ddd mmm d hh:nn:ss zo yyyy' => {:pass => ['Sat Jul 19 12:00:00 +1000 2008'], :fail => []}, 'yyyy-mm-ddThh:nn:ss(?:Z|zo)' => {:pass => ['2008-07-19T12:00:00+10:00', '2008-07-19T12:00:00Z'], :fail => ['2008-07-19T12:00:00Z+10:00']}, } format_tests.each do |format, values| it "should correctly match datetimes in format '#{format}'" do regexp = compile_regexp(format) values[:pass].each {|value| expect(value).to match(regexp)} values[:fail].each {|value| expect(value).not_to match(regexp)} end end end end context "#match" do let(:set) { Timeliness::FormatSet.compile(['yyyy-mm-dd', 'dd/mm/yyyy']) } it 'should return array if string matches a format in set' do expect(set.match('2000-01-02')).to be_kind_of(Array) end it 'should return nil if string does not matches a format in set' do expect(set.match('2nd Feb 2000')).to be_nil end it 'should only use specific format string for match if provided' do expect(set.match('2000-01-02', 'yyyy-mm-dd')).to be_kind_of(Array) expect(set.match('2000-01-02', 'dd/mm/yyyy')).to be_nil end it 'should compile unknown format for one off match' do expect(set.match('20001011')).to be_nil expect(set.match('20001011', 'yyyymmdd')).to be_kind_of(Array) end end def compile_regexp(format) Timeliness::FormatSet.compile([format]).regexp end end timeliness-0.3.10/spec/timeliness/format_spec.rb000066400000000000000000000100041342641020700216760ustar00rootroot00000000000000describe Timeliness::Format do describe "#compile!" do it 'should compile valid string format' do expect { Timeliness::Format.new('yyyy-mm-dd hh:nn:ss.u zo').compile! }.to_not raise_error end it 'should return self' do format = Timeliness::Format.new('yyyy-mm-dd hh:nn:ss.u zo') expect(format.compile!).to eq format end it 'should raise compilation error for bad format' do expect { Timeliness::Format.new('|--[)').compile! }.to raise_error(Timeliness::Format::CompilationFailed) end end describe "#process" do it "should define method which outputs date array with values in correct order" do expect(format_for('yyyy-mm-dd').process('2000', '1', '2')).to eq [2000,1,2,nil,nil,nil,nil,nil] end it "should define method which outputs date array from format with different order" do expect(format_for('dd/mm/yyyy').process('2', '1', '2000')).to eq [2000,1,2,nil,nil,nil,nil,nil] end it "should define method which outputs date array with zeros when month and day are '0'" do expect(format_for('m/d/yy').process('0', '0', '0000')).to eq [0,0,0,nil,nil,nil,nil,nil] end it "should define method which outputs date array with zeros when month and day are '00'" do expect(format_for('m/d/yy').process('00', '00', '0000')).to eq [0,0,0,nil,nil,nil,nil,nil] end it "should define method which outputs time array" do expect(format_for('hh:nn:ss').process('01', '02', '03')).to eq [nil,nil,nil,1,2,3,nil,nil] end it "should define method which outputs time array with meridian 'pm' adjusted hour" do expect(format_for('hh:nn:ss ampm').process('01', '02', '03', 'pm')).to eq [nil,nil,nil,13,2,3,nil,nil] end it "should define method which outputs time array with meridian 'am' unadjusted hour" do expect(format_for('hh:nn:ss ampm').process('01', '02', '03', 'am')).to eq [nil,nil,nil,1,2,3,nil,nil] end it "should define method which outputs time array with microseconds" do expect(format_for('hh:nn:ss.u').process('01', '02', '03', '99')).to eq [nil,nil,nil,1,2,3,990000,nil] end it "should define method which outputs datetime array with zone offset" do expect(format_for('yyyy-mm-dd hh:nn:ss.u zo').process('2001', '02', '03', '04', '05', '06', '99', '+10:00')).to eq [2001,2,3,4,5,6,990000,36000] end it "should define method which outputs datetime array with timezone string" do expect(format_for('yyyy-mm-dd hh:nn:ss.u tz').process('2001', '02', '03', '04', '05', '06', '99', 'EST')).to eq [2001,2,3,4,5,6,990000,'EST'] end context "with long month" do let(:format) { format_for('dd mmm yyyy') } context "with I18n loaded" do before(:all) do I18n.locale = :es I18n.backend.store_translations :es, :date => { :month_names => %w{ ~ Enero Febrero Marzo } } I18n.backend.store_translations :es, :date => { :abbr_month_names => %w{ ~ Ene Feb Mar } } end it 'should parse abbreviated month for current locale to correct value' do expect(format.process('2', 'Ene', '2000')).to eq [2000,1,2,nil,nil,nil,nil,nil] end it 'should parse full month for current locale to correct value' do expect(format.process('2', 'Enero', '2000')).to eq [2000,1,2,nil,nil,nil,nil,nil] end after(:all) do I18n.locale = :en end end context "without I18n loaded" do before do allow(format).to receive(:i18n_loaded?).and_return(false) expect(I18n).not_to receive(:t) end it 'should parse abbreviated month to correct value' do expect(format.process('2', 'Jan', '2000')).to eq [2000,1,2,nil,nil,nil,nil,nil] end it 'should parse full month to correct value' do expect(format.process('2', 'January', '2000')).to eq [2000,1,2,nil,nil,nil,nil,nil] end end end end def format_for(format) Timeliness::Format.new(format).compile! end end timeliness-0.3.10/spec/timeliness/parser_spec.rb000066400000000000000000000453231342641020700217160ustar00rootroot00000000000000describe Timeliness::Parser do before(:all) do Timecop.freeze(2010,1,1,0,0,0) end describe "parse" do it "should return Time object for valid datetime string" do expect(parse("2000-01-01 12:13:14")).to be_kind_of(Time) end it "should return nil for empty string" do expect(parse("")).to be_nil end it "should return nil for nil value" do expect(parse(nil)).to be_nil end it "should return same value if value is a Time, Date, or DateTime" do [Time.now, Date.new, DateTime.new].each do |value| expect(parse(value)).to eq value end end it "should return nil for non-string non-temporal values" do [ {}, [], Class.new ].each do |value| expect(parse(value)).to eq nil end end it "should return time object for valid date string" do expect(parse("2000-01-01")).to be_kind_of(Time) end it "should return nil for invalid date string" do should_not_parse("2000-02-30") end it "should return nil for invalid date string where month is '0'" do should_not_parse("0/01/2000") end it "should return nil for invalid date string where month is '00'" do should_not_parse("00/01/2000") end it "should return nil for invalid date month string" do should_not_parse("1 Foo 2000") end it "should return time object for valid time string" do expect(parse("12:13:14")).to be_kind_of(Time) end it "should return nil for invalid time string" do should_not_parse("25:00:00") end it "should return nil for datetime string with invalid date part" do should_not_parse("2000-02-30 12:13:14") end it "should return nil for datetime string with invalid time part" do should_not_parse("2000-02-01 25:13:14") end it 'should return nil for ISO 8601 string with invalid time part' do should_not_parse("2000-02-01T25:13:14+02:00") end context "string with zone offset value" do context "when current timezone is earler than string zone" do before(:all) do Timeliness.default_timezone = :current Time.zone = 'Australia/Melbourne' end it 'should return value shifted by positive offset in default timezone' do value = parse("2000-06-01T12:00:00+02:00") expect(value).to eq Time.zone.local(2000,6,1,20,0,0) expect(value.utc_offset).to eq 10.hours end it 'should return value shifted by negative offset in default timezone' do value = parse("2000-06-01T12:00:00-01:00") expect(value).to eq Time.zone.local(2000,6,1,23,0,0) expect(value.utc_offset).to eq 10.hours end after(:all) do Time.zone = nil Timeliness.default_timezone = :local end end context "when current timezone is later than string zone" do before(:all) do Timeliness.default_timezone = :current Time.zone = 'America/Phoenix' end it 'should return value shifted by positive offset in default timezone' do value = parse("2000-06-01T12:00:00+02:00") expect(value).to eq Time.zone.local(2000,6,1,3,0,0) expect(value.utc_offset).to eq -7.hours end it 'should return value shifted by negative offset in default timezone' do value = parse("2000-06-01T12:00:00-01:00") expect(value).to eq Time.zone.local(2000,6,1,6,0,0) expect(value.utc_offset).to eq -7.hours end after(:all) do Time.zone = nil Timeliness.default_timezone = :local end end end context "string with zone abbreviation" do before(:all) do Time.zone = 'Australia/Melbourne' end it 'should return value using string zone adjusted to default :local timezone' do Timeliness.default_timezone = :local value = parse("Thu, 01 Jun 2000 03:00:00 MST") expect(value).to eq Time.utc(2000,6,1,10,0,0).getlocal expect(value.utc_offset).to eq Time.mktime(2000, 6, 1, 10, 0, 0).utc_offset end it 'should return value using string zone adjusted to default :current timezone' do Timeliness.default_timezone = :current Time.zone = 'Adelaide' value = parse("Thu, 01 Jun 2000 03:00:00 MST") expect(value).to eq Time.zone.local(2000,6,1,19,30,0) expect(value.utc_offset).to eq 9.5.hours end it 'should return value using string zone adjusted to :zone option string timezone' do Timeliness.default_timezone = :local value = parse("Thu, 01 Jun 2000 03:00:00 MST", :zone => 'Perth') expect(value).to eq Time.use_zone('Perth') { Time.zone.local(2000,6,1,18,0,0) } expect(value.utc_offset).to eq 8.hours end after(:all) do Time.zone = nil end end context "with :datetime type" do it "should return time object for valid datetime string" do expect(parse("2000-01-01 12:13:14", :datetime)).to eq Time.local(2000,1,1,12,13,14) end it "should return nil for invalid date string" do expect(parse("0/01/2000", :datetime)).to be_nil end end context "with :date type" do it "should return time object for valid date string" do expect(parse("2000-01-01", :date)).to eq Time.local(2000,1,1) end it "should ignore time in datetime string" do expect(parse('2000-02-01 12:13', :date)).to eq Time.local(2000,2,1) end it "should return nil for invalid date string" do expect(parse("0/01/2000", :date)).to be_nil end end context "with :time type" do it "should return time object with a dummy date values" do expect(parse('12:13', :time)).to eq Time.local(2010,1,1,12,13) end it "should ignore date in datetime string" do expect(parse('2010-02-01 12:13', :time)).to eq Time.local(2010,1,1,12,13) end it "should raise error if time hour is out of range for AM meridian" do expect(parse('13:14 am', :time)).to be_nil end end context "with :now option" do it 'should use date parts if string does not specify' do time = parse("12:13:14", :now => Time.local(2010,1,1)) expect(time).to eq Time.local(2010,1,1,12,13,14) end end context "with time value argument" do it 'should use argument as :now option value' do time = parse("12:13:14", Time.local(2010,1,1)) expect(time).to eq Time.local(2010,1,1,12,13,14) end end context "with :zone option" do context ":utc" do it "should return time object in utc timezone" do time = parse("2000-06-01 12:13:14", :datetime, :zone => :utc) expect(time.utc_offset).to eq 0 end it 'should return nil for partial invalid time component' do expect(parse("2000-06-01 12:60", :datetime, :zone => :utc)).to be_nil end end context ":local" do it "should return time object in local system timezone" do time = parse("2000-06-01 12:13:14", :datetime, :zone => :local) expect(time.utc_offset).to eq Time.mktime(2000, 6, 1, 12, 13, 14).utc_offset end it 'should return nil for partial invalid time component' do expect(parse("2000-06-01 12:60", :datetime, :zone => :local)).to be_nil end end context ":current" do it "should return time object in current timezone" do Time.zone = 'Adelaide' time = parse("2000-06-01 12:13:14", :datetime, :zone => :current) expect(time.utc_offset).to eq 9.5.hours end it 'should return nil for partial invalid time component' do expect(parse("2000-06-01 12:60", :datetime, :zone => :current)).to be_nil end end context "named zone" do it "should return time object in the timezone" do time = parse("2000-06-01 12:13:14", :datetime, :zone => 'London') expect(time.utc_offset).to eq 1.hour end it 'should return nil for partial invalid time component' do expect(parse("2000-06-01 12:60", :datetime, :zone => 'London')).to be_nil end end context "without ActiveSupport loaded" do it 'should output message' do expect { expect(Time).to receive(:zone).and_raise(NoMethodError.new("undefined method `zone' for Time:Class")) parse("2000-06-01 12:13:14", :zone => :current) }.to raise_error(Timeliness::Parser::MissingTimezoneSupport) expect { expect(Time).to receive(:current).and_raise(NoMethodError.new("undefined method `current' for Time:Class")) parse("12:13:14", :zone => :current) }.to raise_error(Timeliness::Parser::MissingTimezoneSupport) expect { expect(Time).to receive(:use_zone).and_raise(NoMethodError.new("undefined method `use_zone' for Time:Class")) parse("2000-06-01 12:13:14", :zone => 'London') }.to raise_error(Timeliness::Parser::MissingTimezoneSupport) end end end context "for time type" do context "with date from date_for_time_type" do before do @original = Timeliness.date_for_time_type end it 'should return date array' do Timeliness.date_for_time_type = [2010,1,1] expect(parse('12:13:14', :time)).to eq Time.local(2010,1,1,12,13,14) end it 'should return date array evaluated lambda' do Timeliness.date_for_time_type = lambda { Time.local(2010,2,1) } expect(parse('12:13:14', :time)).to eq Time.local(2010,2,1,12,13,14) end after do Timeliness.date_for_time_type = @original end end context "with :now option" do it 'should use date from :now' do expect(parse('12:13:14', :time, :now => Time.local(2010, 6, 1))).to eq Time.local(2010,6,1,12,13,14) end end context "with :zone option" do before(:all) do Timecop.return @current_tz = ENV['TZ'] ENV['TZ'] = 'Australia/Melbourne' Timecop.freeze(2010,1,1,0,0,0) end it "should use date from the specified zone" do time = parse("12:13:14", :time, :zone => :utc) expect(time.year).to eq 2009 expect(time.month).to eq 12 expect(time.day).to eq 31 end after(:all) do Timecop.return ENV['TZ'] = @current_tz Timecop.freeze(2010,1,1,0,0,0) end end end end describe "_parse" do context "with no type" do it "should return date array from date string" do time_array = parser._parse('2000-02-01') expect(time_array).to eq [2000,2,1,nil,nil,nil,nil,nil] end it "should return time array from time string" do time_array = parser._parse('12:13:14', :time) expect(time_array).to eq [nil,nil,nil,12,13,14,nil,nil] end it "should return datetime array from datetime string" do time_array = parser._parse('2000-02-01 12:13:14') expect(time_array).to eq [2000,2,1,12,13,14,nil,nil] end end context "with type" do it "should return date array from date string" do time_array = parser._parse('2000-02-01', :date) expect(time_array).to eq [2000,2,1,nil,nil,nil,nil,nil] end it "should not return time array from time string for :date type" do time_array = parser._parse('12:13:14', :date) expect(time_array).to eq nil end it "should return time array from time string" do time_array = parser._parse('12:13:14', :time) expect(time_array).to eq [nil,nil,nil,12,13,14,nil,nil] end it "should not return date array from date string for :time type" do time_array = parser._parse('2000-02-01', :time) expect(time_array).to eq nil end it "should return datetime array from datetime string when type is date" do time_array = parser._parse('2000-02-01 12:13:14', :date) expect(time_array).to eq [2000,2,1,12,13,14,nil,nil] end it "should return date array from date string when type is datetime" do time_array = parser._parse('2000-02-01', :datetime) expect(time_array).to eq [2000,2,1,nil,nil,nil,nil,nil] end it "should not return time array from time string when type is datetime" do time_array = parser._parse('12:13:14', :datetime) expect(time_array).to eq nil end end context "with :strict => true" do it "should return nil from date string when type is datetime" do time_array = parser._parse('2000-02-01', :datetime, :strict => true) expect(time_array).to be_nil end it "should return nil from datetime string when type is date" do time_array = parser._parse('2000-02-01 12:13:14', :date, :strict => true) expect(time_array).to be_nil end it "should return nil from datetime string when type is time" do time_array = parser._parse('2000-02-01 12:13:14', :time, :strict => true) expect(time_array).to be_nil end it "should parse date string when type is date" do time_array = parser._parse('2000-02-01', :date, :strict => true) expect(time_array).not_to be_nil end it "should parse time string when type is time" do time_array = parser._parse('12:13:14', :time, :strict => true) expect(time_array).not_to be_nil end it "should parse datetime string when type is datetime" do time_array = parser._parse('2000-02-01 12:13:14', :datetime, :strict => true) expect(time_array).not_to be_nil end it "should ignore strict parsing if no type specified" do time_array = parser._parse('2000-02-01', :strict => true) expect(time_array).not_to be_nil end end context "with :format option" do it "should return values if string matches specified format" do time_array = parser._parse('2000-02-01 12:13:14', :datetime, :format => 'yyyy-mm-dd hh:nn:ss') expect(time_array).to eq [2000,2,1,12,13,14,nil,nil] end it "should return nil if string does not match specified format" do time_array = parser._parse('2000-02-01 12:13', :datetime, :format => 'yyyy-mm-dd hh:nn:ss') expect(time_array).to be_nil end end context "date with ambiguous year" do it "should return year in current century if year below threshold" do time_array = parser._parse('01-02-29', :date) expect(time_array).to eq [2029,2,1,nil,nil,nil,nil,nil] end it "should return year in last century if year at or above threshold" do time_array = parser._parse('01-02-30', :date) expect(time_array).to eq [1930,2,1,nil,nil,nil,nil,nil] end it "should allow custom threshold" do default = Timeliness.ambiguous_year_threshold Timeliness.ambiguous_year_threshold = 40 time_array = parser._parse('01-02-39', :date) expect(time_array).to eq [2039,2,1,nil,nil,nil,nil,nil] time_array = parser._parse('01-02-40', :date) expect(time_array).to eq [1940,2,1,nil,nil,nil,nil,nil] Timeliness.ambiguous_year_threshold = default end end end describe "make_time" do it "should return time object for valid time array" do time = parser.make_time([2010,9,8,12,13,14]) expect(time).to eq Time.local(2010,9,8,12,13,14) end it "should return nil for invalid date in array" do time = parser.make_time([2010,13,8,12,13,14]) expect(time).to be_nil end it "should return nil for invalid time in array" do time = parser.make_time([2010,9,8,25,13,14]) expect(time).to be_nil end it "should return nil for invalid time in array with timezone" do time = parser.make_time([2010,9,8,25,13,14,0,1]) expect(time).to be_nil end context "default timezone" do before do @default_timezone = Timeliness.default_timezone end it "should be used if no zone value" do Timeliness.default_timezone = :utc time = parser.make_time([2000,6,1,12,0,0]) expect(time.utc_offset).to eq 0 end after do Timeliness.default_timezone = @default_timezone end end context "with zone value" do context ":utc" do it "should return time object in utc timezone" do time = parser.make_time([2000,6,1,12,0,0], :utc) expect(time.utc_offset).to eq 0 end end context ":local" do it "should return time object in local system timezone" do time = parser.make_time([2000,6,1,12,0,0], :local) expect(time.utc_offset).to eq Time.mktime(2000,6,1,12,0,0).utc_offset end end context ":current" do it "should return time object in current timezone" do Time.zone = 'Adelaide' time = parser.make_time([2000,6,1,12,0,0], :current) expect(time.utc_offset).to eq 9.5.hours end end context "named zone" do it "should return time object in the timezone" do time = parser.make_time([2000,6,1,12,0,0], 'London') expect(time.utc_offset).to eq 1.hour end end end end describe "current_date" do context "with no options" do it 'should return date_for_time_type values with no options' do dummy_date = Timeliness.date_for_time_type.call expect(current_date).to eq [ dummy_date.year, dummy_date.month, dummy_date.day ] end end context "with :now option" do it 'should return date array from Time value' do time = Time.now date_array = [time.year, time.month, time.day] expect(current_date(:now => time)).to eq date_array end end context "with :zone option" do it 'should return date array for utc zone' do time = Time.now.getutc date_array = [time.year, time.month, time.day] expect(current_date(:zone => :utc)).to eq date_array end it 'should return date array for local zone' do time = Time.now date_array = [time.year, time.month, time.day] expect(current_date(:zone => :local)).to eq date_array end it 'should return date array for current zone' do Time.zone = 'London' time = Time.current date_array = [time.year, time.month, time.day] expect(current_date(:zone => :current)).to eq date_array end it 'should return date array for named zone' do time = Time.use_zone('London') { Time.current } date_array = [time.year, time.month, time.day] expect(current_date(:zone => 'London')).to eq date_array end end end after(:all) do Timecop.return end end timeliness-0.3.10/spec/timeliness_helper.rb000066400000000000000000000012431342641020700207400ustar00rootroot00000000000000require 'active_support/time' require 'timecop' require 'timeliness' require 'timeliness/core_ext' module TimelinessHelpers def parser Timeliness::Parser end def definitions Timeliness::Definitions end def parse(*args) Timeliness::Parser.parse(*args) end def current_date(options={}) Timeliness::Parser.send(:current_date, options) end def should_parse(*args) expect(Timeliness::Parser.parse(*args)).not_to be_nil end def should_not_parse(*args) expect(Timeliness::Parser.parse(*args)).to be_nil end end I18n.available_locales = ['en', 'es'] RSpec.configure do |c| c.mock_with :rspec c.include TimelinessHelpers endtimeliness-0.3.10/timeliness.gemspec000066400000000000000000000020651342641020700174720ustar00rootroot00000000000000# -*- encoding: utf-8 -*- $:.push File.expand_path("../lib", __FILE__) require "timeliness/version" Gem::Specification.new do |s| s.name = "timeliness" s.version = Timeliness::VERSION s.platform = Gem::Platform::RUBY s.authors = ["Adam Meehan"] s.email = %q{adam.meehan@gmail.com} s.homepage = %q{http://github.com/adzap/timeliness} s.summary = %q{Date/time parsing for the control freak.} s.description = %q{Fast date/time parser with customisable formats, timezone and I18n support.} s.license = "MIT" s.rubyforge_project = %q{timeliness} s.add_development_dependency 'activesupport', '>= 3.2' s.add_development_dependency 'tzinfo', '>= 0.3.31' s.add_development_dependency 'rspec', '~> 3.4' s.add_development_dependency 'timecop' s.add_development_dependency 'i18n' s.files = `git ls-files`.split("\n") s.files = `git ls-files`.split("\n") - %w{ .gitignore .rspec Gemfile Gemfile.lock } s.extra_rdoc_files = ["README.rdoc", "CHANGELOG.rdoc"] s.require_paths = ["lib"] end