chronic-duration-0.10.6/0000755000175600017570000000000012731010133014076 5ustar pravipravichronic-duration-0.10.6/spec/0000755000175600017570000000000012731010133015030 5ustar pravipravichronic-duration-0.10.6/spec/spec_helper.rb0000644000175600017570000000015112731010133017643 0ustar pravipravirequire 'rubygems' require 'bundler/setup' require 'chronic_duration' RSpec.configure do |config| end chronic-duration-0.10.6/spec/lib/0000755000175600017570000000000012731010133015576 5ustar pravipravichronic-duration-0.10.6/spec/lib/chronic_duration_spec.rb0000644000175600017570000002165112731010133022474 0ustar pravipravirequire 'spec_helper' describe ChronicDuration do describe ".parse" do @exemplars = { '1:20' => 60 + 20, '1:20.51' => 60 + 20.51, '4:01:01' => 4 * 3600 + 60 + 1, '3 mins 4 sec' => 3 * 60 + 4, '3 Mins 4 Sec' => 3 * 60 + 4, 'three mins four sec' => 3 * 60 + 4, '2 hrs 20 min' => 2 * 3600 + 20 * 60, '2h20min' => 2 * 3600 + 20 * 60, '6 mos 1 day' => 6 * 30 * 24 * 3600 + 24 * 3600, '1 year 6 mos 1 day' => 1 * 31557600 + 6 * 30 * 24 * 3600 + 24 * 3600, '2.5 hrs' => 2.5 * 3600, '47 yrs 6 mos and 4.5d' => 47 * 31557600 + 6 * 30 * 24 * 3600 + 4.5 * 24 * 3600, 'two hours and twenty minutes' => 2 * 3600 + 20 * 60, 'four hours and forty minutes' => 4 * 3600 + 40 * 60, 'four hours, and fourty minutes' => 4 * 3600 + 40 * 60, '3 weeks and, 2 days' => 3600 * 24 * 7 * 3 + 3600 * 24 * 2, '3 weeks, plus 2 days' => 3600 * 24 * 7 * 3 + 3600 * 24 * 2, '3 weeks with 2 days' => 3600 * 24 * 7 * 3 + 3600 * 24 * 2, '1 month' => 3600 * 24 * 30, '2 months' => 3600 * 24 * 30 * 2, '18 months' => 3600 * 24 * 30 * 18, '1 year 6 months' => (3600 * 24 * (365.25 + 6 * 30)).to_i, 'day' => 3600 * 24, 'minute 30s' => 90 } context "when string can't be parsed" do it "returns nil" do ChronicDuration.parse('gobblygoo').should be_nil end it "cannot parse zero" do ChronicDuration.parse('0').should be_nil end context "when @@raise_exceptions set to true" do it "raises with ChronicDuration::DurationParseError" do ChronicDuration.raise_exceptions = true expect { ChronicDuration.parse('23 gobblygoos') }.to raise_error(ChronicDuration::DurationParseError) ChronicDuration.raise_exceptions = false end end end it "should return zero if the string parses as zero and the keep_zero option is true" do ChronicDuration.parse('0', :keep_zero => true).should == 0 end it "should return a float if seconds are in decimals" do ChronicDuration.parse('12 mins 3.141 seconds').is_a?(Float).should be_true end it "should return an integer unless the seconds are in decimals" do ChronicDuration.parse('12 mins 3 seconds').is_a?(Integer).should be_true end it "should be able to parse minutes by default" do ChronicDuration.parse('5', :default_unit => "minutes").should == 300 end @exemplars.each do |k, v| it "parses a duration like #{k}" do ChronicDuration.parse(k).should == v end end end describe '.output' do @exemplars = { (60 + 20) => { :micro => '1m20s', :short => '1m 20s', :default => '1 min 20 secs', :long => '1 minute 20 seconds', :chrono => '1:20' }, (60 + 20.51) => { :micro => '1m20.51s', :short => '1m 20.51s', :default => '1 min 20.51 secs', :long => '1 minute 20.51 seconds', :chrono => '1:20.51' }, (60 + 20.51928) => { :micro => '1m20.51928s', :short => '1m 20.51928s', :default => '1 min 20.51928 secs', :long => '1 minute 20.51928 seconds', :chrono => '1:20.51928' }, (4 * 3600 + 60 + 1) => { :micro => '4h1m1s', :short => '4h 1m 1s', :default => '4 hrs 1 min 1 sec', :long => '4 hours 1 minute 1 second', :chrono => '4:01:01' }, (2 * 3600 + 20 * 60) => { :micro => '2h20m', :short => '2h 20m', :default => '2 hrs 20 mins', :long => '2 hours 20 minutes', :chrono => '2:20' }, (2 * 3600 + 20 * 60) => { :micro => '2h20m', :short => '2h 20m', :default => '2 hrs 20 mins', :long => '2 hours 20 minutes', :chrono => '2:20:00' }, (6 * 30 * 24 * 3600 + 24 * 3600) => { :micro => '6mo1d', :short => '6mo 1d', :default => '6 mos 1 day', :long => '6 months 1 day', :chrono => '6:01:00:00:00' # Yuck. FIXME }, (365.25 * 24 * 3600 + 24 * 3600 ).to_i => { :micro => '1y1d', :short => '1y 1d', :default => '1 yr 1 day', :long => '1 year 1 day', :chrono => '1:00:01:00:00:00' }, (3 * 365.25 * 24 * 3600 + 24 * 3600 ).to_i => { :micro => '3y1d', :short => '3y 1d', :default => '3 yrs 1 day', :long => '3 years 1 day', :chrono => '3:00:01:00:00:00' }, (3600 * 24 * 30 * 18) => { :micro => '18mo', :short => '18mo', :default => '18 mos', :long => '18 months', :chrono => '18:00:00:00:00' } } @exemplars.each do |k, v| v.each do |key, val| it "properly outputs a duration of #{k} seconds as #{val} using the #{key.to_s} format option" do ChronicDuration.output(k, :format => key).should == val end end end @keep_zero_exemplars = { (true) => { :micro => '0s', :short => '0s', :default => '0 secs', :long => '0 seconds', :chrono => '0' }, (false) => { :micro => nil, :short => nil, :default => nil, :long => nil, :chrono => '0' }, } @keep_zero_exemplars.each do |k, v| v.each do |key, val| it "should properly output a duration of 0 seconds as #{val.nil? ? "nil" : val} using the #{key.to_s} format option, if the keep_zero option is #{k.to_s}" do ChronicDuration.output(0, :format => key, :keep_zero => k).should == val end end end it "returns weeks when needed" do ChronicDuration.output(45*24*60*60, :weeks => true).should =~ /.*wk.*/ end it "returns hours and minutes only when :hours_only option specified" do ChronicDuration.output(395*24*60*60 + 15*60, :limit_to_hours => true).should == '9480 hrs 15 mins' end it "returns the specified number of units if provided" do ChronicDuration.output(4 * 3600 + 60 + 1, units: 2).should == '4 hrs 1 min' ChronicDuration.output(6 * 30 * 24 * 3600 + 24 * 3600 + 3600 + 60 + 1, units: 3, format: :long).should == '6 months 1 day 1 hour' end context "when the format is not specified" do it "uses the default format" do ChronicDuration.output(2 * 3600 + 20 * 60).should == '2 hrs 20 mins' end end @exemplars.each do |seconds, format_spec| format_spec.each do |format, _| it "outputs a duration for #{seconds} that parses back to the same thing when using the #{format.to_s} format" do ChronicDuration.parse(ChronicDuration.output(seconds, :format => format)).should == seconds end end end it "uses user-specified joiner if provided" do ChronicDuration.output(2 * 3600 + 20 * 60, joiner: ', ').should == '2 hrs, 20 mins' end end describe ".filter_by_type" do it "receives a chrono-formatted time like 3:14 and return a human time like 3 minutes 14 seconds" do ChronicDuration.instance_eval("filter_by_type('3:14')").should == '3 minutes 14 seconds' end it "receives chrono-formatted time like 12:10:14 and return a human time like 12 hours 10 minutes 14 seconds" do ChronicDuration.instance_eval("filter_by_type('12:10:14')").should == '12 hours 10 minutes 14 seconds' end it "returns the input if it's not a chrono-formatted time" do ChronicDuration.instance_eval("filter_by_type('4 hours')").should == '4 hours' end end describe ".cleanup" do it "cleans up extraneous words" do ChronicDuration.instance_eval("cleanup('4 days and 11 hours')").should == '4 days 11 hours' end it "cleans up extraneous spaces" do ChronicDuration.instance_eval("cleanup(' 4 days and 11 hours')").should == '4 days 11 hours' end it "inserts spaces where there aren't any" do ChronicDuration.instance_eval("cleanup('4m11.5s')").should == '4 minutes 11.5 seconds' end end describe "work week" do before(:all) do ChronicDuration.hours_per_day = 8 ChronicDuration.days_per_week = 5 end after(:all) do ChronicDuration.hours_per_day = 24 ChronicDuration.days_per_week = 7 end it "should parse knowing the work week" do week = ChronicDuration.parse('5d') ChronicDuration.parse('40h').should == week ChronicDuration.parse('1w').should == week end end end chronic-duration-0.10.6/LICENSE.txt0000644000175600017570000000203212731010133015716 0ustar pravipraviCopyright (c) Henry Poydar 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.chronic-duration-0.10.6/.travis.yml0000644000175600017570000000004312731010133016204 0ustar pravipravirvm: - 1.9.3 - 2.0.0 - 2.1.0 chronic-duration-0.10.6/README.md0000644000175600017570000000535012731010133015360 0ustar pravipravi[![Build Status](https://travis-ci.org/hpoydar/chronic_duration.png?branch=master)](https://travis-ci.org/hpoydar/chronic_duration) # Chronic Duration A simple Ruby natural language parser for elapsed time. (For example, 4 hours and 30 minutes, 6 minutes 4 seconds, 3 days, etc.) Returns all results in seconds. Will return an integer unless you get tricky and need a float. (4 minutes and 13.47 seconds, for example.) The reverse can also be accomplished with the output method. So pass in seconds and you can get strings like 4 mins 31.51 secs (default format), 4h 3m 30s, or 4:01:29. ## Usage >> require 'chronic_duration' => true >> ChronicDuration.parse('4 minutes and 30 seconds') => 270 >> ChronicDuration.parse('0 seconds') => nil >> ChronicDuration.parse('0 seconds', :keep_zero => true) => 0 >> ChronicDuration.output(270) => 4 mins 30 secs >> ChronicDuration.output(0) => nil >> ChronicDuration.output(0, :keep_zero => true) => 0 secs >> ChronicDuration.output(270, :format => :short) => 4m 30s >> ChronicDuration.output(270, :format => :long) => 4 minutes 30 seconds >> ChronicDuration.output(270, :format => :chrono) => 4:30 >> ChronicDuration.output(1299600, :weeks => true) => 2 wks 1 day 1 hr >> ChronicDuration.output(1299600, :weeks => true, :units => 2) => 2 wks 1 day >> ChronicDuration.output(45*24*60*60 + 15*60, :limit_to_hours => true) => 1080 hrs 15 mins >> ChronicDuration.output(1299600, :weeks => true, :units => 2, :joiner => ', ') => 2 wks, 1 day >> ChronicDuration.output(1296000) => 15 days Nil is returned if the string can't be parsed Examples of parse-able strings: * '12.4 secs' * '1:20' * '1:20.51' * '4:01:01' * '3 mins 4 sec' * '2 hrs 20 min' * '2h20min' * '6 mos 1 day' * '47 yrs 6 mos and 4d' * 'two hours and twenty minutes' * '3 weeks and 2 days' ChronicDuration.raise_exceptions can be set to true to raise exceptions when the string can't be parsed. >> ChronicDuration.raise_exceptions = true => true >> ChronicDuration.parse('4 elephants and 3 Astroids') ChronicDuration::DurationParseError: An invalid word "elephants" was used in the string to be parsed. ## Contributing Fork and pull request after your specs are green. Add your handle to the list below. Also looking for additional maintainers. ## Contributors errm,pdf, brianjlandau, jduff, olauzon, roboman, ianlevesque, bolandrm ## TODO * Benchmark, optimize * Context specific matching (E.g., for '4m30s', assume 'm' is minutes not months) * Smartly parse vacation-like durations (E.g., '4 days and 3 nights') * :chrono output option should probably change to something like 4 days 4:00:12 instead of 4:04:00:12 * Other locale support chronic-duration-0.10.6/Gemfile0000644000175600017570000000004512731010133015370 0ustar pravipravisource "http://rubygems.org" gemspec chronic-duration-0.10.6/lib/0000755000175600017570000000000012731010133014644 5ustar pravipravichronic-duration-0.10.6/lib/chronic_duration.rb0000644000175600017570000002055412731010133020531 0ustar pravipravirequire 'numerizer' unless defined?(Numerizer) module ChronicDuration extend self class DurationParseError < StandardError end @@raise_exceptions = false @@hours_per_day = 24 @@days_per_week = 7 def self.raise_exceptions !!@@raise_exceptions end def self.raise_exceptions=(value) @@raise_exceptions = !!value end def self.hours_per_day @@hours_per_day end def self.hours_per_day=(value) @@hours_per_day = value end def self.days_per_week @@days_per_week end def self.days_per_week=(value) @@days_per_week = value end # Given a string representation of elapsed time, # return an integer (or float, if fractions of a # second are input) def parse(string, opts = {}) result = calculate_from_words(cleanup(string), opts) (!opts[:keep_zero] and result == 0) ? nil : result end # Given an integer and an optional format, # returns a formatted string representing elapsed time def output(seconds, opts = {}) int = seconds.to_i seconds = int if seconds - int == 0 # if seconds end with .0 opts[:format] ||= :default opts[:keep_zero] ||= false years = months = weeks = days = hours = minutes = 0 decimal_places = seconds.to_s.split('.').last.length if seconds.is_a?(Float) minute = 60 hour = 60 * minute day = ChronicDuration.hours_per_day * hour month = 30 * day year = 31557600 if seconds >= 31557600 && seconds%year < seconds%month years = seconds / year months = seconds % year / month days = seconds % year % month / day hours = seconds % year % month % day / hour minutes = seconds % year % month % day % hour / minute seconds = seconds % year % month % day % hour % minute elsif seconds >= 60 minutes = (seconds / 60).to_i seconds = seconds % 60 if minutes >= 60 hours = (minutes / 60).to_i minutes = (minutes % 60).to_i if !opts[:limit_to_hours] if hours >= ChronicDuration.hours_per_day days = (hours / ChronicDuration.hours_per_day).to_i hours = (hours % ChronicDuration.hours_per_day).to_i if opts[:weeks] if days >= ChronicDuration.days_per_week weeks = (days / ChronicDuration.days_per_week).to_i days = (days % ChronicDuration.days_per_week).to_i if weeks >= 4 months = (weeks / 4).to_i weeks = (weeks % 4).to_i end end else if days >= 30 months = (days / 30).to_i days = (days % 30).to_i end end end end end end joiner = opts.fetch(:joiner) { ' ' } process = nil case opts[:format] when :micro dividers = { :years => 'y', :months => 'mo', :weeks => 'w', :days => 'd', :hours => 'h', :minutes => 'm', :seconds => 's' } joiner = '' when :short dividers = { :years => 'y', :months => 'mo', :weeks => 'w', :days => 'd', :hours => 'h', :minutes => 'm', :seconds => 's' } when :default dividers = { :years => ' yr', :months => ' mo', :weeks => ' wk', :days => ' day', :hours => ' hr', :minutes => ' min', :seconds => ' sec', :pluralize => true } when :long dividers = { :years => ' year', :months => ' month', :weeks => ' week', :days => ' day', :hours => ' hour', :minutes => ' minute', :seconds => ' second', :pluralize => true } when :chrono dividers = { :years => ':', :months => ':', :weeks => ':', :days => ':', :hours => ':', :minutes => ':', :seconds => ':', :keep_zero => true } process = lambda do |str| # Pad zeros # Get rid of lead off times if they are zero # Get rid of lead off zero # Get rid of trailing : divider = ':' str.split(divider).map { |n| # add zeros only if n is an integer n.include?('.') ? ("%04.#{decimal_places}f" % n) : ("%02d" % n) }.join(divider).gsub(/^(00:)+/, '').gsub(/^0/, '').gsub(/:$/, '') end joiner = '' end result = [:years, :months, :weeks, :days, :hours, :minutes, :seconds].map do |t| next if t == :weeks && !opts[:weeks] num = eval(t.to_s) num = ("%.#{decimal_places}f" % num) if num.is_a?(Float) && t == :seconds keep_zero = dividers[:keep_zero] keep_zero ||= opts[:keep_zero] if t == :seconds humanize_time_unit( num, dividers[t], dividers[:pluralize], keep_zero ) end.compact! result = result[0...opts[:units]] if opts[:units] result = result.join(joiner) if process result = process.call(result) end result.length == 0 ? nil : result end private def humanize_time_unit(number, unit, pluralize, keep_zero) return nil if number == 0 && !keep_zero res = "#{number}#{unit}" # A poor man's pluralizer res << 's' if !(number == 1) && pluralize res end def calculate_from_words(string, opts) val = 0 words = string.split(' ') words.each_with_index do |v, k| if v =~ float_matcher val += (convert_to_number(v) * duration_units_seconds_multiplier(words[k + 1] || (opts[:default_unit] || 'seconds'))) end end val end def cleanup(string) res = string.downcase res = filter_by_type(Numerizer.numerize(res)) res = res.gsub(float_matcher) {|n| " #{n} "}.squeeze(' ').strip res = filter_through_white_list(res) end def convert_to_number(string) string.to_f % 1 > 0 ? string.to_f : string.to_i end def duration_units_list %w(seconds minutes hours days weeks months years) end def duration_units_seconds_multiplier(unit) return 0 unless duration_units_list.include?(unit) case unit when 'years'; 31557600 when 'months'; 3600 * ChronicDuration.hours_per_day * 30 when 'weeks'; 3600 * ChronicDuration.hours_per_day * ChronicDuration.days_per_week when 'days'; 3600 * ChronicDuration.hours_per_day when 'hours'; 3600 when 'minutes'; 60 when 'seconds'; 1 end end # Parse 3:41:59 and return 3 hours 41 minutes 59 seconds def filter_by_type(string) chrono_units_list = duration_units_list.reject {|v| v == "weeks"} if string.gsub(' ', '') =~ /#{float_matcher}(:#{float_matcher})+/ res = [] string.gsub(' ', '').split(':').reverse.each_with_index do |v,k| return unless chrono_units_list[k] res << "#{v} #{chrono_units_list[k]}" end res = res.reverse.join(' ') else res = string end res end def float_matcher /[0-9]*\.?[0-9]+/ end # Get rid of unknown words and map found # words to defined time units def filter_through_white_list(string) res = [] string.split(' ').each do |word| if word =~ float_matcher res << word.strip next end stripped_word = word.strip.gsub(/^,/, '').gsub(/,$/, '') if mappings.has_key?(stripped_word) res << mappings[stripped_word] elsif !join_words.include?(stripped_word) and ChronicDuration.raise_exceptions raise DurationParseError, "An invalid word #{word.inspect} was used in the string to be parsed." end end # add '1' at front if string starts with something recognizable but not with a number, like 'day' or 'minute 30sec' res.unshift(1) if res.length > 0 && mappings[res[0]] res.join(' ') end def mappings { 'seconds' => 'seconds', 'second' => 'seconds', 'secs' => 'seconds', 'sec' => 'seconds', 's' => 'seconds', 'minutes' => 'minutes', 'minute' => 'minutes', 'mins' => 'minutes', 'min' => 'minutes', 'm' => 'minutes', 'hours' => 'hours', 'hour' => 'hours', 'hrs' => 'hours', 'hr' => 'hours', 'h' => 'hours', 'days' => 'days', 'day' => 'days', 'dy' => 'days', 'd' => 'days', 'weeks' => 'weeks', 'week' => 'weeks', 'wks' => 'weeks', 'wk' => 'weeks', 'w' => 'weeks', 'months' => 'months', 'mo' => 'months', 'mos' => 'months', 'month' => 'months', 'years' => 'years', 'year' => 'years', 'yrs' => 'years', 'yr' => 'years', 'y' => 'years' } end def join_words ['and', 'with', 'plus'] end end chronic-duration-0.10.6/lib/chronic_duration/0000755000175600017570000000000012731010133020176 5ustar pravipravichronic-duration-0.10.6/lib/chronic_duration/version.rb0000644000175600017570000000006012731010133022204 0ustar pravipravimodule ChronicDuration VERSION = '0.10.6' end chronic-duration-0.10.6/chronic_duration.gemspec0000644000175600017570000000237212731010133021001 0ustar pravipravi# -*- encoding: utf-8 -*- lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'chronic_duration/version' Gem::Specification.new do |gem| gem.name = "chronic_duration" gem.version = ChronicDuration::VERSION gem.authors = ["hpoydar"] gem.email = ["henry@poydar.com"] gem.description = %q{A simple Ruby natural language parser for elapsed time. (For example, 4 hours and 30 minutes, 6 minutes 4 seconds, 3 days, etc.) Returns all results in seconds. Will return an integer unless you get tricky and need a float. (4 minutes and 13.47 seconds, for example.) The reverse can also be performed via the output method.} gem.summary = %q{A simple Ruby natural language parser for elapsed time} gem.homepage = "https://github.com/hpoydar/chronic_duration" gem.license = "MIT" gem.files = `git ls-files`.split($/) gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) gem.require_paths = ["lib"] gem.add_runtime_dependency "numerizer", "~> 0.1.1" gem.add_development_dependency "rake", "~> 10.0.3" gem.add_development_dependency "rspec", "~> 2.12.0" end chronic-duration-0.10.6/Rakefile0000644000175600017570000000016612731010133015546 0ustar pravipravirequire "bundler/gem_tasks" require 'rspec/core/rake_task' RSpec::Core::RakeTask.new('spec') task :default => :spec chronic-duration-0.10.6/.gitignore0000644000175600017570000000023212731010133016063 0ustar pravipravi*.gem *.rbc .bundle .config .yardoc Gemfile.lock InstalledFiles _yardoc coverage doc/ lib/bundler/man pkg rdoc spec/reports test/tmp test/version_tmp tmp