timespan 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +4 -1
- data/Gemfile.lock +76 -12
- data/README.md +119 -12
- data/Rakefile +2 -2
- data/VERSION +1 -1
- data/config/locales/timespan/da.yml +5 -0
- data/lib/timespan.rb +160 -166
- data/lib/timespan/compare.rb +59 -0
- data/lib/timespan/mongoid.rb +31 -0
- data/lib/timespan/printer.rb +71 -0
- data/lib/timespan/rails/engine.rb +9 -0
- data/lib/timespan/span.rb +43 -0
- data/lib/timespan/units.rb +92 -0
- data/spec/timespan/compare_spec.rb +50 -0
- data/spec/timespan/locales/duration_da.yml +20 -0
- data/spec/timespan/printer_spec.rb +24 -0
- data/spec/timespan/span_spec.rb +67 -0
- data/spec/timespan/units_spec.rb +54 -0
- data/spec/timespan_spec.rb +81 -46
- data/timespan.gemspec +22 -4
- metadata +49 -5
@@ -0,0 +1,59 @@
|
|
1
|
+
class Timespan
|
2
|
+
class TimeDuration
|
3
|
+
include Timespan::Units
|
4
|
+
|
5
|
+
attr_reader :duration, :reverse
|
6
|
+
|
7
|
+
def initialize duration, options = {}
|
8
|
+
@duration = ::Duration.new(duration)
|
9
|
+
@reverse = options[:reverse] || (@duration.total < 0)
|
10
|
+
end
|
11
|
+
|
12
|
+
def total
|
13
|
+
reverse ? -(duration.total.abs) : duration.total
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.create_reverse duration
|
17
|
+
self.new duration, :reverse => true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module Compare
|
22
|
+
|
23
|
+
include Comparable
|
24
|
+
|
25
|
+
def time_left time = nil
|
26
|
+
time_compare = time || Time.now
|
27
|
+
diff = end_time - time_compare
|
28
|
+
Timespan::TimeDuration.new(diff)
|
29
|
+
end
|
30
|
+
|
31
|
+
def expired?
|
32
|
+
time_left.total <= 0
|
33
|
+
end
|
34
|
+
|
35
|
+
def <=> time
|
36
|
+
raise ArgumentError, "Not a valid argument for Timespan comparison, was #{time}" unless valid_compare?(time)
|
37
|
+
case time
|
38
|
+
when Timespan
|
39
|
+
seconds <=> time.seconds
|
40
|
+
when Time
|
41
|
+
seconds <=> time.to_i
|
42
|
+
when Date, DateTime
|
43
|
+
time.to_time.to_i
|
44
|
+
when Integer
|
45
|
+
seconds <=> time
|
46
|
+
when ActiveSupport::Duration
|
47
|
+
seconds <=> time.to_i
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def valid_compare? time
|
52
|
+
valid_compare_types.any? {|type| time.kind_of? type }
|
53
|
+
end
|
54
|
+
|
55
|
+
def valid_compare_types
|
56
|
+
[Timespan, Time, Date, DateTime, Integer]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require "timespan"
|
2
|
+
require "mongoid/fields"
|
3
|
+
|
4
|
+
# Mongoid serialization support for Timespan type.
|
5
|
+
module Mongoid
|
6
|
+
module Fields
|
7
|
+
class Timespan
|
8
|
+
include Mongoid::Fields::Serializable
|
9
|
+
|
10
|
+
# Deserialize a Timespan given the hash stored by Mongodb
|
11
|
+
#
|
12
|
+
# @param [Hash] Timespan as hash
|
13
|
+
# @return [Timespan] deserialized Timespan
|
14
|
+
def deserialize(timespan_hash)
|
15
|
+
return if !timespan_hash
|
16
|
+
::Timespan.new(:from => timespan_hash[:from], :to => timespan_hash[:to])
|
17
|
+
end
|
18
|
+
|
19
|
+
# Serialize a Timespan or a Hash (with Timespan units) or a Duration in some form to
|
20
|
+
# a BSON serializable type.
|
21
|
+
#
|
22
|
+
# @param [Timespan, Hash, Integer, String] value
|
23
|
+
# @return [Hash] Timespan in seconds
|
24
|
+
def serialize(value)
|
25
|
+
return if value.blank?
|
26
|
+
timespan = ::Timespan.new(value)
|
27
|
+
{:from => time_span.start_time, :to => time_span.end_time, :Timespan => time_span.Timespan}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'i18n'
|
2
|
+
|
3
|
+
class Timespan
|
4
|
+
class << self
|
5
|
+
attr_writer :time_format
|
6
|
+
|
7
|
+
def time_format
|
8
|
+
@time_format ||= "%d %b %Y"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module Printer
|
13
|
+
# locale-dependent terminology
|
14
|
+
# %~s, %~m, %~h, %~d and %~w
|
15
|
+
#
|
16
|
+
# %td => total days
|
17
|
+
# %th => total hours
|
18
|
+
# %tm => total minutes
|
19
|
+
# %ts => total seconds
|
20
|
+
def to_s mode = :full
|
21
|
+
meth = "print_#{mode}"
|
22
|
+
raise ArgumentError, "Print mode not supported, was: #{mode}" unless respond_to?(meth)
|
23
|
+
send(meth)
|
24
|
+
end
|
25
|
+
|
26
|
+
def print_dates
|
27
|
+
"#{i18n_t 'from'} #{print :start_time} #{i18n_t 'to'} #{print :end_time}"
|
28
|
+
end
|
29
|
+
|
30
|
+
def print_duration
|
31
|
+
print :duration
|
32
|
+
end
|
33
|
+
|
34
|
+
def print_full
|
35
|
+
[print_dates, i18n_t('lasting'), print_duration].join(' ')
|
36
|
+
end
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
def i18n_t label
|
41
|
+
I18n.t(label, :scope => :timespan, :default => label.to_s)
|
42
|
+
end
|
43
|
+
|
44
|
+
def print type
|
45
|
+
return duration.format(duration_format) if type == :duration
|
46
|
+
raise ArgumentError, "Not a valid print type, was: #{type}" unless valid_print_type? type
|
47
|
+
send(type).strftime(time_format)
|
48
|
+
end
|
49
|
+
|
50
|
+
def valid_print_type? type
|
51
|
+
%w{start_time end_time}.include? type.to_s
|
52
|
+
end
|
53
|
+
|
54
|
+
def time_format
|
55
|
+
Timespan.time_format
|
56
|
+
end
|
57
|
+
|
58
|
+
def duration_format
|
59
|
+
identifiers = []
|
60
|
+
identifiers << 'y' if (duration.years > 0)
|
61
|
+
identifiers << 'o' if (duration.months > 0)
|
62
|
+
identifiers << 'w' if (duration.weeks > 0)
|
63
|
+
identifiers << 'd' if (duration.days > 0)
|
64
|
+
identifiers << 'h' if (duration.hours > 0)
|
65
|
+
identifiers << 'm' if (duration.minutes > 0)
|
66
|
+
identifiers << 's' if (duration.seconds > 0)
|
67
|
+
|
68
|
+
identifiers.map {|id| "%#{id} %~#{id}" }.join(' ')
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class Timespan
|
2
|
+
class DurationParseError < StandardError; end
|
3
|
+
|
4
|
+
module Span
|
5
|
+
attr_reader :duration
|
6
|
+
|
7
|
+
def duration= duration
|
8
|
+
@duration = case duration
|
9
|
+
when Duration
|
10
|
+
duration
|
11
|
+
when Numeric, Hash
|
12
|
+
Duration.new duration
|
13
|
+
when String
|
14
|
+
Duration.new parse_duration(duration)
|
15
|
+
else
|
16
|
+
raise ArgumentError, "Unsupported duration type: #{duration}"
|
17
|
+
end
|
18
|
+
unless is_new?
|
19
|
+
add_dirty :duration
|
20
|
+
refresh!
|
21
|
+
calculate!
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
def parse_duration text
|
28
|
+
spanner_parse text
|
29
|
+
rescue Spanner::ParseError => e
|
30
|
+
chronic_parse text
|
31
|
+
rescue ChronicDuration::DurationParseError => e
|
32
|
+
raise Timespan::DurationParseError, "Internal error: neither Spanner or ChronicDuration could parse '#{duration}'"
|
33
|
+
end
|
34
|
+
|
35
|
+
def spanner_parse text
|
36
|
+
Spanner.parse(text.gsub /and/, '')
|
37
|
+
end
|
38
|
+
|
39
|
+
def chronic_parse text
|
40
|
+
ChronicDuration.parse text
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
class Timespan
|
2
|
+
module Units
|
3
|
+
def to_milliseconds
|
4
|
+
@to_seconds ||= (seconds * 1000.0).round
|
5
|
+
end
|
6
|
+
alias_method :to_mils, :to_milliseconds
|
7
|
+
alias_method :millis, :to_mils
|
8
|
+
alias_method :milliseconds, :to_mils
|
9
|
+
|
10
|
+
def seconds
|
11
|
+
@seconds ||= duration.total
|
12
|
+
end
|
13
|
+
|
14
|
+
alias_method :to_secs, :seconds
|
15
|
+
alias_method :to_seconds, :seconds
|
16
|
+
|
17
|
+
def to_minutes
|
18
|
+
@to_minutes ||= (to_seconds / 60.0).round
|
19
|
+
end
|
20
|
+
alias_method :to_m, :to_minutes
|
21
|
+
alias_method :to_mins, :to_minutes
|
22
|
+
alias_method :minutes, :to_minutes
|
23
|
+
|
24
|
+
def to_hours
|
25
|
+
@to_hours ||= (to_minutes / 60.0).round
|
26
|
+
end
|
27
|
+
alias_method :to_h, :to_hours
|
28
|
+
alias_method :hrs, :to_hours
|
29
|
+
alias_method :hours, :to_hours
|
30
|
+
|
31
|
+
def to_days
|
32
|
+
@to_days ||= (to_hours / 24.0).round
|
33
|
+
end
|
34
|
+
alias_method :to_d, :to_days
|
35
|
+
alias_method :days, :to_days
|
36
|
+
|
37
|
+
def to_weeks
|
38
|
+
@to_weeks ||= (to_days / 7.0).round
|
39
|
+
end
|
40
|
+
alias_method :to_w, :to_weeks
|
41
|
+
alias_method :weeks, :to_days
|
42
|
+
|
43
|
+
def to_months
|
44
|
+
@to_months ||= (to_days / 30.0).round
|
45
|
+
end
|
46
|
+
alias_method :to_mon, :to_months
|
47
|
+
alias_method :months, :to_months
|
48
|
+
|
49
|
+
def to_years
|
50
|
+
@to_years ||= (to_days.to_f / 365.25).round
|
51
|
+
end
|
52
|
+
alias_method :to_y, :to_years
|
53
|
+
alias_method :years, :to_years
|
54
|
+
|
55
|
+
def to_decades
|
56
|
+
@to_decades ||= (to_years / 10.0).round
|
57
|
+
end
|
58
|
+
alias_method :decades, :to_decades
|
59
|
+
|
60
|
+
def to_centuries
|
61
|
+
@to_centuries ||= (to_decades / 10.0).round
|
62
|
+
end
|
63
|
+
alias_method :centuries, :to_centuries
|
64
|
+
|
65
|
+
def self.duration_units
|
66
|
+
%w{seconds minutes hours days weeks months years}
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.units
|
70
|
+
duration_units + %w{decades centuries}
|
71
|
+
end
|
72
|
+
|
73
|
+
def units
|
74
|
+
Timespan::Units.units
|
75
|
+
end
|
76
|
+
|
77
|
+
duration_units.each do |unit|
|
78
|
+
define_method :"#{unit}=" do |number|
|
79
|
+
raise ArgumentError, "Must be a Numeric, was: #{number.inspect}" unless number.kind_of? Numeric
|
80
|
+
self.duration = Duration.new(unit.to_sym => number)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def decades= number
|
85
|
+
self.years = number * 10
|
86
|
+
end
|
87
|
+
|
88
|
+
def centuries= number
|
89
|
+
self.decades = number * 10
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Timespan" do
|
4
|
+
subject { timespan }
|
5
|
+
|
6
|
+
context 'From and To with 1 day apart' do
|
7
|
+
let(:timespan) { Timespan.new :from => from, :to => to}
|
8
|
+
|
9
|
+
let(:from) { Chronic.parse("1 day ago") }
|
10
|
+
let(:to) { Time.now }
|
11
|
+
|
12
|
+
describe '.compare == ' do
|
13
|
+
specify do
|
14
|
+
(subject == 1.day).should be_true
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '.compare < ' do
|
19
|
+
specify do
|
20
|
+
(subject < 1.day).should be_false
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '.compare > ' do
|
25
|
+
specify do
|
26
|
+
(subject > 1.day).should be_false
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'From 2 days ago until today' do
|
32
|
+
let(:timespan) { Timespan.new :from => "2 days ago", :to => "1 hour ago" }
|
33
|
+
|
34
|
+
describe '.time_left' do
|
35
|
+
it 'should have 0 days left' do
|
36
|
+
timespan.time_left.days.should == 0
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should have 1 hour left' do
|
40
|
+
timespan.time_left.hrs.should == -1
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe '.expired?' do
|
45
|
+
it 'should have 0 days left' do
|
46
|
+
timespan.expired?.should be_true
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
da:
|
2
|
+
timespan:
|
3
|
+
from: fra
|
4
|
+
to: til
|
5
|
+
lasting: der varer ialt
|
6
|
+
ruby_duration:
|
7
|
+
second: sekond
|
8
|
+
seconds: sekonder
|
9
|
+
minute: minut
|
10
|
+
minutes: minutter
|
11
|
+
hour: time
|
12
|
+
hours: timer
|
13
|
+
day: dag
|
14
|
+
days: dage
|
15
|
+
week: uge
|
16
|
+
weeks: uges
|
17
|
+
month: måned
|
18
|
+
months: måneder
|
19
|
+
year: år
|
20
|
+
years: år
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'i18n'
|
3
|
+
|
4
|
+
I18n.load_path << File.join(File.dirname(__FILE__), 'locales', 'duration_da.yml')
|
5
|
+
|
6
|
+
describe Timespan::Span do
|
7
|
+
subject { timespan }
|
8
|
+
|
9
|
+
let(:from) { Chronic.parse("1 day ago") }
|
10
|
+
let(:to) { Time.now }
|
11
|
+
|
12
|
+
before do
|
13
|
+
I18n.locale = :da
|
14
|
+
end
|
15
|
+
|
16
|
+
describe 'print in danish - multiple modes' do
|
17
|
+
let(:timespan) { Timespan.new :from => from, :to => to }
|
18
|
+
|
19
|
+
its(:to_s) { should == 'fra 03 May 2012 til 04 May 2012 der varer ialt 1 dag' }
|
20
|
+
|
21
|
+
specify { subject.to_s(:dates).should == 'fra 03 May 2012 til 04 May 2012' }
|
22
|
+
specify { subject.to_s(:duration).should == '1 dag' }
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Timespan::Span do
|
4
|
+
subject { timespan }
|
5
|
+
|
6
|
+
let(:from) { Chronic.parse("1 day ago") }
|
7
|
+
let(:to) { Time.now }
|
8
|
+
|
9
|
+
describe 'set seconds to new' do
|
10
|
+
let(:timespan) { Timespan.new :from => from, :to => to }
|
11
|
+
|
12
|
+
before :each do
|
13
|
+
@old_timespan = timespan.clone
|
14
|
+
@new_timespan = timespan.clone
|
15
|
+
@new_timespan.seconds = 120
|
16
|
+
end
|
17
|
+
|
18
|
+
its(:duration) { should be_a Duration }
|
19
|
+
specify { subject.send(:dirty).should be_empty }
|
20
|
+
|
21
|
+
it 'should have diff durations' do
|
22
|
+
@old_timespan.duration.should_not == @new_timespan.duration
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should have diff timespans in minutes' do
|
26
|
+
@old_timespan.minutes.should_not == @new_timespan.minutes
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe 'set minutes to new' do
|
31
|
+
let(:timespan) { Timespan.new :from => from, :to => to }
|
32
|
+
|
33
|
+
before :each do
|
34
|
+
@old_timespan = timespan.clone
|
35
|
+
@new_timespan = timespan.clone
|
36
|
+
@new_timespan.minutes = 50
|
37
|
+
end
|
38
|
+
|
39
|
+
its(:duration) { should be_a Duration }
|
40
|
+
specify { subject.send(:dirty).should be_empty }
|
41
|
+
|
42
|
+
it 'should have diff durations' do
|
43
|
+
@old_timespan.duration.should_not == @new_timespan.duration
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should have diff timespans in minutes' do
|
47
|
+
@old_timespan.minutes.should_not == @new_timespan.minutes
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe 'set duration to new' do
|
52
|
+
let(:timespan) { Timespan.new :from => from, :to => to }
|
53
|
+
|
54
|
+
before :each do
|
55
|
+
@old_timespan = timespan.clone
|
56
|
+
@new_timespan = timespan.clone
|
57
|
+
@new_timespan.duration = "7 days"
|
58
|
+
end
|
59
|
+
|
60
|
+
its(:duration) { should be_a Duration }
|
61
|
+
specify { subject.send(:dirty).should be_empty }
|
62
|
+
|
63
|
+
it 'should have diff timespans' do
|
64
|
+
@old_timespan.days.should_not == @new_timespan.days
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|