periodical 0.2.0 → 1.0.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.
- checksums.yaml +4 -4
- data/.rspec +4 -0
- data/.simplecov +9 -0
- data/.travis.yml +8 -3
- data/Gemfile +4 -1
- data/README.md +14 -4
- data/Rakefile +9 -1
- data/lib/periodical/duration.rb +10 -18
- data/lib/periodical/filter.rb +1 -1
- data/lib/periodical/period.rb +51 -64
- data/lib/periodical/version.rb +1 -1
- data/periodical.gemspec +1 -1
- data/spec/periodical/period_spec.rb +31 -0
- metadata +7 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: da0ceda3373a9c1cf5e18751ce263b9b0b476c16
|
4
|
+
data.tar.gz: 7d22d18c11b786f1bda8196d845f9d02d50e92da
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 119d9eb7004ff7edf05efb77f27f9623ec812a1d9537d58736eeac68e3e648e7ab316f73739686e0f874aa36429fcae6f29ce038add39e8d15ebd2e253c68644
|
7
|
+
data.tar.gz: 1aadd82add21e3eb8f2c482a71d46bed39c1297e4b406e1ee604bc358ae2a362003e64bbbbe9e22cfcf2d4243c364525bf5d5a935b52e38bc300d722bd1a28d5
|
data/.rspec
ADDED
data/.simplecov
ADDED
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -2,19 +2,29 @@
|
|
2
2
|
|
3
3
|
Periodical is a simple framework for working with durations and periods. A duration measures a range of time bounded by a `from` date and `to` date. A period is a relative unit of time such as `4 weeks`.
|
4
4
|
|
5
|
+
[](https://travis-ci.org/ioquatix/periodical)
|
6
|
+
[](https://codeclimate.com/github/ioquatix/periodical)
|
7
|
+
[](https://coveralls.io/r/ioquatix/periodical)
|
8
|
+
|
9
|
+
## Motivation
|
10
|
+
|
11
|
+
The original idea for this library came from a [Python script which performed backup rotation](http://www.scottlu.com/Content/Snapfilter.html) from 2009. In particular, I thought it had a novel way to retain backups according to a given policy (e.g. one backup every year for 10 years, one backup every month for 12 months, one backup every week for 8 weeks, one backup every day for 30 days). This is done by constructing a special slot based hash structure with keys based on the date being stored. This functionality is used by [LSync](https://github.com/ioquatix/LSync) for performing backup rotation (i.e. deleting old backups).
|
12
|
+
|
13
|
+
In addition, I had a need to implement periodical billing in [Financier](https://github.com/ioquatix/financier). Not only can this gem advance a date by a given period, it can compute the number of periods between two dates. This is useful for invoicing, say, once every 6 months for a weekly or monthly service.
|
14
|
+
|
5
15
|
## Installation
|
6
16
|
|
7
17
|
Add this line to your application's Gemfile:
|
8
18
|
|
9
|
-
|
19
|
+
gem 'periodical'
|
10
20
|
|
11
21
|
And then execute:
|
12
22
|
|
13
|
-
|
23
|
+
$ bundle
|
14
24
|
|
15
25
|
Or install it yourself as:
|
16
26
|
|
17
|
-
|
27
|
+
$ gem install periodical
|
18
28
|
|
19
29
|
## Usage
|
20
30
|
|
@@ -41,7 +51,7 @@ The main use case for this framework involves periodic billing or accounting (e.
|
|
41
51
|
|
42
52
|
Released under the MIT license.
|
43
53
|
|
44
|
-
Copyright, 2010, 2014, by [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams).
|
54
|
+
Copyright, 2010, 2014, 2016, by [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams).
|
45
55
|
|
46
56
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
47
57
|
of this software and associated documentation files (the "Software"), to deal
|
data/Rakefile
CHANGED
@@ -1,6 +1,14 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
2
|
require "rspec/core/rake_task"
|
3
3
|
|
4
|
-
RSpec::Core::RakeTask.new(:spec)
|
4
|
+
RSpec::Core::RakeTask.new(:spec) do |task|
|
5
|
+
if ENV['COVERAGE']
|
6
|
+
begin
|
7
|
+
require('simplecov/version')
|
8
|
+
task.rspec_opts = %w{--require simplecov}
|
9
|
+
rescue LoadError
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
5
13
|
|
6
14
|
task :default => :spec
|
data/lib/periodical/duration.rb
CHANGED
@@ -21,17 +21,9 @@
|
|
21
21
|
require 'date'
|
22
22
|
|
23
23
|
module Periodical
|
24
|
-
|
25
|
-
def initialize(from, to)
|
26
|
-
@from = from
|
27
|
-
@to = to
|
28
|
-
end
|
29
|
-
|
30
|
-
attr :from
|
31
|
-
attr :to
|
32
|
-
|
24
|
+
Duration = Struct.new(:from, :to) do
|
33
25
|
def days
|
34
|
-
|
26
|
+
to - from
|
35
27
|
end
|
36
28
|
|
37
29
|
def weeks
|
@@ -39,29 +31,29 @@ module Periodical
|
|
39
31
|
end
|
40
32
|
|
41
33
|
def whole_months
|
42
|
-
(
|
34
|
+
(to.year * 12 + to.month) - (from.year * 12 + from.month)
|
43
35
|
end
|
44
36
|
|
45
37
|
def months
|
46
38
|
whole = self.whole_months
|
47
39
|
|
48
|
-
partial_start =
|
49
|
-
partial_end =
|
40
|
+
partial_start = from >> whole
|
41
|
+
partial_end = from >> whole + 1
|
50
42
|
|
51
|
-
return whole + (
|
43
|
+
return whole + (to - partial_start) / (partial_end - partial_start)
|
52
44
|
end
|
53
45
|
|
54
46
|
def whole_years
|
55
|
-
|
47
|
+
to.year - from.year
|
56
48
|
end
|
57
49
|
|
58
50
|
def years
|
59
51
|
whole = self.whole_years
|
60
52
|
|
61
|
-
partial_start =
|
62
|
-
partial_end =
|
53
|
+
partial_start = from >> (whole * 12)
|
54
|
+
partial_end = from >> ((whole + 1) * 12)
|
63
55
|
|
64
|
-
return whole + (
|
56
|
+
return whole + (to - partial_start) / (partial_end - partial_start)
|
65
57
|
end
|
66
58
|
|
67
59
|
# Calculate the number of periods between from and to
|
data/lib/periodical/filter.rb
CHANGED
data/lib/periodical/period.rb
CHANGED
@@ -19,83 +19,70 @@
|
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
21
|
module Periodical
|
22
|
-
|
23
|
-
VALID_UNITS = [:days, :weeks, :months, :years]
|
22
|
+
Period = Struct.new(:count, :unit) do
|
23
|
+
VALID_UNITS = [:days, :weeks, :months, :years].freeze
|
24
24
|
|
25
|
-
#
|
26
|
-
def
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
if parts.size == 1
|
34
|
-
if parts[0].to_i == 0
|
35
|
-
count = 1
|
36
|
-
unit = parts[0].to_sym
|
37
|
-
else
|
38
|
-
count = parts[0].to_i
|
39
|
-
unit = :days
|
40
|
-
|
41
|
-
if count == 7
|
42
|
-
count = 1
|
43
|
-
unit = :weeks
|
44
|
-
end
|
45
|
-
|
46
|
-
if count == 365
|
47
|
-
count = 1
|
48
|
-
unit = :years
|
49
|
-
end
|
50
|
-
end
|
25
|
+
# Plural is preferred, as in "1 or more days".
|
26
|
+
def initialize(count = 1, unit = :days)
|
27
|
+
super(count, unit)
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_s
|
31
|
+
if self.count != 1
|
32
|
+
"#{self.count} #{self.unit}"
|
51
33
|
else
|
52
|
-
|
53
|
-
unit = parts[1].gsub(/\s+/, '_').downcase.to_sym
|
34
|
+
self.unit.to_s
|
54
35
|
end
|
55
|
-
|
56
|
-
unless VALID_UNITS.include? unit
|
57
|
-
raise ArgumentError.new("Invalid unit specified #{unit}!")
|
58
|
-
end
|
59
|
-
|
60
|
-
if count == 0
|
61
|
-
raise ArgumentError.new("Count must be non-zero!")
|
62
|
-
end
|
63
|
-
|
64
|
-
return self.new(count, unit)
|
65
36
|
end
|
66
37
|
|
67
|
-
def
|
68
|
-
|
69
|
-
@unit = unit
|
38
|
+
def advance(date, multiple = 1)
|
39
|
+
self.send("advance_#{unit}", date, multiple * self.count)
|
70
40
|
end
|
71
41
|
|
72
|
-
|
73
|
-
|
42
|
+
private
|
43
|
+
|
44
|
+
def advance_days(date, count)
|
45
|
+
date + count
|
74
46
|
end
|
75
47
|
|
76
|
-
def
|
77
|
-
|
78
|
-
@unit.to_s.gsub(/s$/, '')
|
79
|
-
else
|
80
|
-
to_s
|
81
|
-
end
|
48
|
+
def advance_weeks(date, count)
|
49
|
+
date + (7 * count)
|
82
50
|
end
|
83
51
|
|
84
|
-
def
|
85
|
-
|
52
|
+
def advance_months(date, count)
|
53
|
+
date >> count
|
54
|
+
end
|
55
|
+
|
56
|
+
def advance_years(date, count)
|
57
|
+
date >> (12 * count)
|
58
|
+
end
|
59
|
+
|
60
|
+
class << self
|
61
|
+
# Accepts strings in the format of "2 weeks" or "weeks"
|
62
|
+
def parse(string)
|
63
|
+
parts = string.split(/\s+/, 2)
|
64
|
+
|
65
|
+
if parts.size == 1
|
66
|
+
count = 1
|
67
|
+
unit = parts[0]
|
68
|
+
else
|
69
|
+
count, unit = parts
|
70
|
+
end
|
71
|
+
|
72
|
+
self.new(count.to_i, unit.to_sym)
|
73
|
+
end
|
86
74
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
75
|
+
def load(string)
|
76
|
+
if string
|
77
|
+
string = string.strip
|
78
|
+
|
79
|
+
parse(string) unless string.empty?
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def dump(period)
|
84
|
+
period.to_s if period
|
95
85
|
end
|
96
86
|
end
|
97
|
-
|
98
|
-
attr :unit
|
99
|
-
attr :count
|
100
87
|
end
|
101
88
|
end
|
data/lib/periodical/version.rb
CHANGED
data/periodical.gemspec
CHANGED
@@ -19,6 +19,6 @@ Gem::Specification.new do |spec|
|
|
19
19
|
spec.required_ruby_version = '>= 2.0'
|
20
20
|
|
21
21
|
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
-
spec.add_development_dependency "rspec", "~> 3.
|
22
|
+
spec.add_development_dependency "rspec", "~> 3.4.0"
|
23
23
|
spec.add_development_dependency "rake"
|
24
24
|
end
|
@@ -29,5 +29,36 @@ module Periodical::PeriodSpec
|
|
29
29
|
|
30
30
|
expect(period.advance(duration.from, 12)).to be == duration.to
|
31
31
|
end
|
32
|
+
|
33
|
+
it "should parse a singular period" do
|
34
|
+
period = Periodical::Period.parse("years")
|
35
|
+
|
36
|
+
expect(period.count).to be == 1
|
37
|
+
expect(period.unit).to be == :years
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should parse a multiple count period" do
|
41
|
+
period = Periodical::Period.parse("5 days")
|
42
|
+
|
43
|
+
expect(period.count).to be == 5
|
44
|
+
expect(period.unit).to be == :days
|
45
|
+
end
|
46
|
+
|
47
|
+
it "can load nil" do
|
48
|
+
expect(Periodical::Period.load(nil)).to be == nil
|
49
|
+
expect(Periodical::Period.load("")).to be == nil
|
50
|
+
end
|
51
|
+
|
52
|
+
it "can dump nil" do
|
53
|
+
expect(Periodical::Period.dump(nil)).to be == nil
|
54
|
+
end
|
55
|
+
|
56
|
+
it "can load string" do
|
57
|
+
expect(Periodical::Period.load("5 weeks")).to be == Periodical::Period.new(5, :weeks)
|
58
|
+
end
|
59
|
+
|
60
|
+
it "can dump nil" do
|
61
|
+
expect(Periodical::Period.dump(Periodical::Period.new(5, :weeks))).to be == "5 weeks"
|
62
|
+
end
|
32
63
|
end
|
33
64
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: periodical
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2016-03-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -30,14 +30,14 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 3.
|
33
|
+
version: 3.4.0
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: 3.
|
40
|
+
version: 3.4.0
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rake
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -59,6 +59,8 @@ executables: []
|
|
59
59
|
extensions: []
|
60
60
|
extra_rdoc_files: []
|
61
61
|
files:
|
62
|
+
- ".rspec"
|
63
|
+
- ".simplecov"
|
62
64
|
- ".travis.yml"
|
63
65
|
- Gemfile
|
64
66
|
- README.md
|
@@ -92,7 +94,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
92
94
|
version: '0'
|
93
95
|
requirements: []
|
94
96
|
rubyforge_project:
|
95
|
-
rubygems_version: 2.
|
97
|
+
rubygems_version: 2.5.2
|
96
98
|
signing_key:
|
97
99
|
specification_version: 4
|
98
100
|
summary: Periodical is a simple framework for working with durations and periods.
|