workpattern 0.6.0 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 32e6c1903732854fa9a9ae45a5c02ebb408391ce931e2b8b20b9c7f3f349bae0
4
- data.tar.gz: 1a99b95b71dda679fe674a7ff83595cac0922afb39b1012cc0239aafbf57c486
3
+ metadata.gz: b4784e020f63bed8a312b50553d245f54fe9ebf5c003555f706bc6d4b74ca447
4
+ data.tar.gz: f401a880126a8dea9113037a7b20d50ba123ec66727d8e1d35d74bf5d7539f32
5
5
  SHA512:
6
- metadata.gz: 5c19219fa8b5f49ecb48e8072bc92143f688f4e2ed3edf32a11415e3d7e3b886e29d222818309ca2df570f95be68ed4d9c341d01fc43548b8302faa6a1a30052
7
- data.tar.gz: 0247b415ada6052c16041a4ed18f3cbe28bc6af1d2dae3bc11f625fd50a77ca912ef52df46b98a3bb44858e6e50ec1a34799096821806de939e79e8c36765b4d
6
+ metadata.gz: a2d348d3aa2a2ecfd1174f1446176b26f7f481af8eafcecdcbf68be8d9594d1e1dac253d93c8d31389a5ba62b23503c80b9ae5b9159df19021fd2bd3f41a9f08
7
+ data.tar.gz: b132fd4596abf3df95b7a955a376e34b7b8796fae5c848014145e91e2565aaeab2c146ce8beb4881852b5d1de989bd98724f352c8cd86a0a68c3fb013c8f7133
data/.gitignore CHANGED
@@ -6,3 +6,5 @@
6
6
  /pkg/
7
7
  /spec/reports/
8
8
  /tmp/
9
+ Gemfile.lock
10
+ .compound-engineering/*.local.yaml
data/.travis.yml CHANGED
@@ -1,6 +1,5 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 1.9.3
4
3
  - 2.1
5
4
  - 2.2
6
5
  - 2.3
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## Workpattern v0.7.0 (unreleased) ##
2
+
3
+ * Added `Workpattern#to_h` — serialises a workpattern to a plain Ruby hash (JSON-safe; pattern bitmaps are hex-encoded strings).
4
+ * Added `Workpattern.from_h(hash, overwrite: false)` — reconstructs a workpattern from a hash produced by `to_h`. Requires symbol keys; when deserialising from JSON use `JSON.parse(json, symbolize_names: true)`.
5
+ * Removed `Workpattern.persistence_class=` and `Workpattern.persistence?` — both were silently non-functional in all prior releases due to a `@@persist`/`@@persistence` naming bug; no working integration exists.
6
+ * Fixed `DEFAULT_NAME` undefined constant in `Workpattern::Workpattern.initialize` (pre-existing; only triggered when calling the inner class constructor directly with no arguments).
7
+ * Fixed `Array.new(LAST_DAY_OF_WEEK)` → `Array.new(LAST_DAY_OF_WEEK + 1)` in `Week.initialize` for consistency (pre-existing; Ruby auto-extends arrays so no runtime difference).
8
+
1
9
  ## Workpattern v0.6.0 ( 25 Feb, 2021) ##
2
10
 
3
11
  I stopped keeping this Changelog file update back when v0.5.0 was realeased on 19 Oct 2016 and now it is 10Feb 2021 and I'm playing catch-up.
data/Gemfile CHANGED
@@ -3,6 +3,6 @@ source "http://rubygems.org"
3
3
  # Specify your gem's dependencies in workpattern.gemspec
4
4
  gemspec
5
5
 
6
- gem "rake", "~> 13.0"
7
-
6
+ gem "rake", "~> 0.9.2.2" if RUBY_VERSION < "2.4"
7
+ gem "rake", "~> 13.0" if RUBY_VERSION >= "2.4"
8
8
  gem "minitest", "~> 5.0"
data/README.md CHANGED
@@ -91,4 +91,26 @@ Workpattern.delete "My Workpattern"
91
91
 
92
92
  # Delete all Workpatterns
93
93
  Workpattern.clear
94
+ ```
95
+
96
+ ### Serialisation
97
+
98
+ A `Workpattern` can be serialised to a plain Ruby hash and restored later. The hash is JSON-safe — pattern bitmaps are stored as hex strings.
99
+
100
+ ``` ruby
101
+ # Serialise
102
+ h = mywp.to_h
103
+
104
+ # Persist however you like (file, database, Redis, …)
105
+ json = JSON.generate(h)
106
+
107
+ # Restore — JSON.parse must use symbolize_names: true
108
+ h2 = JSON.parse(json, symbolize_names: true)
109
+ mywp2 = Workpattern.from_h(h2)
110
+ ```
111
+
112
+ If a workpattern with the same name already exists, `from_h` raises `NameError`. Pass `overwrite: true` to replace it:
113
+
114
+ ``` ruby
115
+ Workpattern.from_h(h, overwrite: true)
94
116
  ```
@@ -2,7 +2,30 @@ module Workpattern
2
2
 
3
3
  class Day
4
4
 
5
- attr_accessor :pattern, :hours_per_day, :first_working_minute, :last_working_minute
5
+ attr_accessor :hours_per_day, :first_working_minute, :last_working_minute
6
+ attr_reader :pattern
7
+
8
+ def pattern=(value)
9
+ @pattern = value
10
+ set_first_and_last_minutes
11
+ end
12
+
13
+ def to_h
14
+ { pattern: @pattern.to_s(16), hours_per_day: @hours_per_day }
15
+ end
16
+
17
+ def self.from_h(h)
18
+ unless h[:hours_per_day].is_a?(Integer) && h[:hours_per_day] > 0 && h[:hours_per_day] <= HOURS_IN_DAY
19
+ raise ArgumentError, "from_h: hours_per_day must be an Integer between 1 and #{HOURS_IN_DAY}"
20
+ end
21
+ unless h[:pattern].is_a?(String) && h[:pattern].length <= 400
22
+ raise ArgumentError, "from_h: pattern must be a hex String of at most 400 characters"
23
+ end
24
+ day = allocate
25
+ day.hours_per_day = h[:hours_per_day]
26
+ day.pattern = h[:pattern].to_i(16)
27
+ day
28
+ end
6
29
 
7
30
  def initialize(hours_per_day = HOURS_IN_DAY, type = WORK_TYPE)
8
31
  @hours_per_day = hours_per_day
@@ -1,3 +1,3 @@
1
1
  module Workpattern
2
- VERSION = '0.6.0'
2
+ VERSION = '0.7.0'
3
3
  end
@@ -9,18 +9,35 @@ module Workpattern
9
9
  # @private
10
10
  class Week
11
11
  attr_accessor :hours_per_day, :start, :finish, :days
12
- attr_writer :week_total, :total
13
12
 
14
13
  def initialize(start, finish, type = WORK_TYPE, hours_per_day = HOURS_IN_DAY)
15
14
  @hours_per_day = hours_per_day
16
15
  @start = Time.gm(start.year, start.month, start.day)
17
16
  @finish = Time.gm(finish.year, finish.month, finish.day)
18
- @days = Array.new(LAST_DAY_OF_WEEK)
17
+ @days = Array.new(LAST_DAY_OF_WEEK + 1)
19
18
  FIRST_DAY_OF_WEEK.upto(LAST_DAY_OF_WEEK) do |i|
20
19
  @days[i] = Day.new(hours_per_day, type)
21
20
  end
22
21
  end
23
22
 
23
+ def to_h
24
+ { start: { year: @start.year, month: @start.month, day: @start.day },
25
+ finish: { year: @finish.year, month: @finish.month, day: @finish.day },
26
+ days: (FIRST_DAY_OF_WEEK..LAST_DAY_OF_WEEK).map { |i| @days[i].to_h } }
27
+ end
28
+
29
+ def self.from_h(h)
30
+ s = h[:start]
31
+ f = h[:finish]
32
+ week = allocate
33
+ week.hours_per_day = HOURS_IN_DAY
34
+ week.start = Time.gm(s[:year], s[:month], s[:day])
35
+ week.finish = Time.gm(f[:year], f[:month], f[:day])
36
+ week.days = Array.new(LAST_DAY_OF_WEEK + 1)
37
+ h[:days].each_with_index { |dh, i| week.days[i] = Day.from_h(dh) }
38
+ week
39
+ end
40
+
24
41
  def <=>(other)
25
42
  return -1 if start < other.start
26
43
  return 0 if start == other.start
@@ -270,9 +287,6 @@ module Workpattern
270
287
  time + DAY
271
288
  end
272
289
 
273
- def prev_day(time)
274
- time - DAY
275
- end
276
290
 
277
291
  def jd(time)
278
292
  Time.gm(time.year, time.month, time.day)
@@ -40,11 +40,9 @@ module Workpattern
40
40
  # @see #working
41
41
  # @see #resting
42
42
  #
43
- def workpattern(opts = {}, persist = nil)
43
+ def workpattern(opts = {})
44
44
  args = all_workpattern_options(opts)
45
45
 
46
- persist.store(name: @name, workpattern: args) if !persist.nil?
47
-
48
46
  args = standardise_args(args)
49
47
 
50
48
  upd_start = work_pattern.to_utc(args[:start])
@@ -19,10 +19,6 @@ module Workpattern
19
19
  @@workpatterns
20
20
  end
21
21
 
22
- def workpatterns
23
- @@workpatterns
24
- end
25
-
26
22
  # @!attribute [r] name
27
23
  # Name given to the <tt>Workpattern</tt>
28
24
  # @!attribute [r] base
@@ -38,16 +34,6 @@ module Workpattern
38
34
  #
39
35
  attr_reader :name, :base, :span, :from, :to, :weeks
40
36
 
41
- # Class for handling persistence in user's own way
42
- #
43
- def self.persistence_class=(klass)
44
- @@persist = klass
45
- end
46
-
47
- def self.persistence?
48
- @@persist ||= nil
49
- end
50
-
51
37
  # Holds local timezone info
52
38
  @@tz = nil
53
39
 
@@ -75,7 +61,7 @@ module Workpattern
75
61
  # 31st December.
76
62
  # @raise [NameError] if the given name already exists
77
63
  #
78
- def initialize(name = DEFAULT_NAME, base = DEFAULT_BASE_YEAR, span = DEFAULT_SPAN)
64
+ def initialize(name = DEFAULT_WORKPATTERN_NAME, base = DEFAULT_BASE_YEAR, span = DEFAULT_SPAN)
79
65
  if workpatterns.key?(name)
80
66
  raise(NameError, "Workpattern '#{name}' already exists and can't be created again")
81
67
  end
@@ -97,6 +83,11 @@ module Workpattern
97
83
  @week_pattern
98
84
  end
99
85
 
86
+ private def workpatterns
87
+ @@workpatterns
88
+ end
89
+ public
90
+
100
91
  # Deletes all <tt>Workpattern</tt> objects
101
92
  #
102
93
  def self.clear
@@ -151,11 +142,7 @@ module Workpattern
151
142
  # @see #resting
152
143
  #
153
144
  def workpattern(opts = {})
154
- if self.class.persistence?
155
- week_pattern.workpattern(opts, @@persistence)
156
- else
157
- week_pattern.workpattern(opts)
158
- end
145
+ week_pattern.workpattern(opts)
159
146
  end
160
147
 
161
148
  # Convenience method that calls <tt>#workpattern</tt> with the
@@ -178,6 +165,63 @@ module Workpattern
178
165
  workpattern(args)
179
166
  end
180
167
 
168
+ def to_h
169
+ { version: 1,
170
+ name: @name,
171
+ base: @base,
172
+ span: @span,
173
+ weeks: @weeks.map(&:to_h) }
174
+ end
175
+
176
+ def self.from_h(hash, overwrite: false)
177
+ unless hash.key?(:version)
178
+ raise ArgumentError, "from_h: hash is missing a :version key " \
179
+ "(if deserialising from JSON, use symbolize_names: true)"
180
+ end
181
+ unless hash[:version] == 1
182
+ raise ArgumentError, "from_h: unsupported version #{hash[:version].inspect} " \
183
+ "(supported: 1)"
184
+ end
185
+ unless hash[:name].is_a?(String) && !hash[:name].empty?
186
+ raise ArgumentError, "from_h: :name must be a non-empty String"
187
+ end
188
+ unless hash[:base].is_a?(Integer)
189
+ raise ArgumentError, "from_h: :base must be an Integer"
190
+ end
191
+ unless hash[:span].is_a?(Integer) && hash[:span] != 0
192
+ raise ArgumentError, "from_h: :span must be a non-zero Integer"
193
+ end
194
+ unless hash[:weeks].is_a?(Array)
195
+ raise ArgumentError, "from_h: :weeks must be an Array"
196
+ end
197
+
198
+ name = hash[:name]
199
+ if workpatterns.key?(name) && !overwrite
200
+ raise NameError, "Workpattern '#{name}' already exists and can't be created again"
201
+ end
202
+
203
+ wp = allocate
204
+ wp.instance_variable_set(:@name, name)
205
+ wp.instance_variable_set(:@base, hash[:base])
206
+ wp.instance_variable_set(:@span, hash[:span])
207
+
208
+ offset = hash[:span] < 0 ? hash[:span].abs - 1 : 0
209
+ from_time = Time.gm(hash[:base].abs - offset)
210
+ to_time = Time.gm(from_time.year + hash[:span].abs - 1, 12, 31, 23, 59)
211
+ wp.instance_variable_set(:@from, from_time)
212
+ wp.instance_variable_set(:@to, to_time)
213
+
214
+ weeks = SortedSet.new
215
+ hash[:weeks].each { |wh| weeks << Week.from_h(wh) }
216
+ raise ArgumentError, "from_h: :weeks must not be empty" if weeks.empty?
217
+ wp.instance_variable_set(:@weeks, weeks)
218
+ wp.instance_variable_set(:@week_pattern, WeekPattern.new(wp))
219
+
220
+ workpatterns.delete(name) if overwrite
221
+ workpatterns[name] = wp
222
+ wp
223
+ end
224
+
181
225
  # Calculates the resulting date when the <tt>duration</tt> in minutes
182
226
  # is added to the <tt>start</tt> date.
183
227
  # The <tt>duration</tt> is always in whole minutes and subtracts from
data/lib/workpattern.rb CHANGED
@@ -75,6 +75,26 @@ module Workpattern
75
75
  Workpattern.clear
76
76
  end
77
77
 
78
+ # Convenience method to deserialise a Workpattern from a plain hash.
79
+ #
80
+ # @param [Hash] hash produced by Workpattern#to_h
81
+ # @param [Boolean] overwrite replace an existing same-named workpattern
82
+ # @return [Workpattern]
83
+ # @raise [ArgumentError] if the hash is missing or has an unsupported version
84
+ # @raise [NameError] if a same-named workpattern already exists and overwrite is false
85
+ #
86
+ def self.from_h(hash, overwrite: false)
87
+ Workpattern.from_h(hash, overwrite: overwrite)
88
+ end
89
+
90
+ # Convenience method to access the registry of all known Workpattern objects.
91
+ #
92
+ # @return [Hash]
93
+ #
94
+ def self.workpatterns
95
+ Workpattern.workpatterns
96
+ end
97
+
78
98
  # Convenience method to create a Clock object. This can be used for
79
99
  # specifying times if you don't want to create a <tt>DateTime</tt> object
80
100
  #
data/workpattern.gemspec CHANGED
@@ -1,7 +1,11 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  $:.push File.expand_path("../lib", __FILE__)
3
3
 
4
- require_relative "lib/workpattern/version"
4
+ if RUBY_VERSION >= "2.4"
5
+ require_relative "lib/workpattern/version"
6
+ else
7
+ require "workpattern/version"
8
+ end
5
9
 
6
10
  Gem::Specification.new do |spec|
7
11
  spec.name = "workpattern"
@@ -14,16 +18,20 @@ Gem::Specification.new do |spec|
14
18
  spec.homepage = "http://workpattern.org"
15
19
  spec.license = "MIT"
16
20
  spec.required_ruby_version = Gem::Requirement.new(">= 1.9.3")
17
-
18
- spec.metadata["homepage_url"] = spec.homepage
19
- spec.metadata["source_code_uri"] = "https://github.com/callenb/workpattern"
20
- spec.metadata["changelog_uri"] = "https://workpattern.org/2021/02/25/changelog.html"
21
-
21
+ if RUBY_VERSION >= "2.4"
22
+ spec.metadata["homepage_url"] = spec.homepage
23
+ spec.metadata["source_code_uri"] = "https://github.com/callenb/workpattern"
24
+ spec.metadata["changelog_uri"] = "https://workpattern.org/2021/02/25/changelog.html"
25
+ end
22
26
  # Specify which files should be added to the gem when it is released.
23
27
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
- spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
26
- end
28
+ if RUBY_VERSION >= "2.4"
29
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
30
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
31
+ end
32
+ else
33
+ spec.files = `git ls-files`.split("\n")
34
+ end
27
35
  spec.require_paths = ["lib"]
28
36
 
29
37
  # Uncomment to register a new dependency of your gem
@@ -33,6 +41,7 @@ Gem::Specification.new do |spec|
33
41
  # guide at: https://bundler.io/guides/creating_gem.html
34
42
  spec.add_runtime_dependency 'tzinfo'
35
43
  spec.add_runtime_dependency 'sorted_set' if RUBY_VERSION >= "2.4"
44
+
36
45
  spec.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
37
46
  spec.require_paths = ["lib"]
38
47
 
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: workpattern
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Barrie Callender
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2021-02-25 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: tzinfo
@@ -51,7 +50,6 @@ files:
51
50
  - CHANGELOG.md
52
51
  - CODE_OF_CONDUCT.md
53
52
  - Gemfile
54
- - Gemfile.lock
55
53
  - LICENSE.txt
56
54
  - README.md
57
55
  - Rakefile
@@ -75,7 +73,6 @@ metadata:
75
73
  homepage_url: http://workpattern.org
76
74
  source_code_uri: https://github.com/callenb/workpattern
77
75
  changelog_uri: https://workpattern.org/2021/02/25/changelog.html
78
- post_install_message:
79
76
  rdoc_options: []
80
77
  require_paths:
81
78
  - lib
@@ -90,8 +87,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
90
87
  - !ruby/object:Gem::Version
91
88
  version: '0'
92
89
  requirements: []
93
- rubygems_version: 3.0.9
94
- signing_key:
90
+ rubygems_version: 3.6.7
95
91
  specification_version: 4
96
92
  summary: Calculates dates and durations whilst taking into account working and non-working
97
93
  periods down to a minute
data/Gemfile.lock DELETED
@@ -1,31 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- workpattern (0.6.0)
5
- sorted_set
6
- tzinfo
7
-
8
- GEM
9
- remote: http://rubygems.org/
10
- specs:
11
- concurrent-ruby (1.1.8)
12
- minitest (5.14.4)
13
- rake (13.0.3)
14
- rbtree (0.4.4)
15
- set (1.0.1)
16
- sorted_set (1.0.3)
17
- rbtree
18
- set (~> 1.0)
19
- tzinfo (2.0.4)
20
- concurrent-ruby (~> 1.0)
21
-
22
- PLATFORMS
23
- x86_64-linux
24
-
25
- DEPENDENCIES
26
- minitest (~> 5.0)
27
- rake (~> 13.0)
28
- workpattern!
29
-
30
- BUNDLED WITH
31
- 2.2.9