partial-date 1.1.4 → 1.1.5

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/ChangeLog.textile CHANGED
@@ -1,3 +1,13 @@
1
+ h3. 1.1.5 / 2012-05-30
2
+
3
+ * Allow negative years and year range from -1048576 to 1048576 (20 bits).
4
+ * Implemented negative year with signing mask.
5
+ * Year is no longer mandatory
6
+ * Created more specific error classes
7
+ * Implemented readonly attribute Date#bits to allow public access to the bit store for comparison in <=>.
8
+ * Moved bits.rb back into date.rb
9
+
10
+
1
11
  h3. 1.1.4 / 2012-05-29
2
12
 
3
13
  * Changed error messages for month and day to 0 - 12 and 0 - 31 respectively (since zero values are allowed).
data/README.textile CHANGED
@@ -1,16 +1,19 @@
1
1
  h1. partial-date
2
2
 
3
+ * See the "ChangeLog":https://github.com/58bits/partial-date/blob/master/ChangeLog.textile for details of this release.
4
+
3
5
  * "Homepage":https://github.com/58bits/partial-date#readme
4
6
  * "Issues":https://github.com/58bits/partial-date/issues
5
7
  * "Documentation":http://rubydoc.info/gems/partial-date/frames
6
8
 
9
+
7
10
  h2. Description
8
11
 
9
- A simple date class that can be used to store partial date values in a single column/attribute. An example use case would include an archive, or catalogue entry where the complete date is unknown. Year is mandatory, but month and day are optional.
12
+ A simple date class that can be used to store partial date values in a single column/attribute. An example use case would include an archive, or catalogue entry where the complete date is unknown. Year is optional and can be a negative value. Month and day are optional, but month must be set before day.
10
13
 
11
14
  h2. Features
12
15
 
13
- PartialDate::Date uses a 23 bit register as the backing store for date instances, and bit fiddling to get or set year, month and day values. As such it will perform well in a loop or collection of date objects.
16
+ PartialDate::Date uses a 30 bit register as the backing store for date instances, and bit fiddling to get or set year, month and day values. As such it will perform well in a loop or collection of date objects.
14
17
 
15
18
  Use @date.value@ to get or set an Integer value that can be used to rehydrate a date object, or save the date value to a persistance store in a readable Integer form e.g. 20121201 for 2012 December 01.
16
19
 
data/lib/partial-date.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  require 'partial-date/version'
2
2
  require 'partial-date/error'
3
- require 'partial-date/bits'
4
3
  require 'partial-date/date'
5
4
  require 'date'
@@ -1,7 +1,28 @@
1
1
 
2
2
  # Public module containing Date, Error and Version types.
3
3
  module PartialDate
4
+
5
+ # Key:
6
+ # The first 5 bits are the day (max 31)
7
+ # The next 4 bits are the month (max 12)
8
+ # The next 20 bits are the year (max 1048576)
9
+ # The most significant bit (MSB) is a 1 bit sign bit (for negative years).
4
10
 
11
+ DAY_MASK = 0b000000000000000000000000011111
12
+ MONTH_MASK = 0b000000000000000000000111100000
13
+ YEAR_MASK = 0b011111111111111111111000000000
14
+ SIGN_MASK = 0b100000000000000000000000000000
15
+
16
+ ZERO_SIGN_MASK = 0b011111111111111111111111111111
17
+ ZERO_YEAR_MASK = 0b100000000000000000000111111111
18
+ ZERO_MONTH_MASK = 0b111111111111111111111000011111
19
+ ZERO_DAY_MASK = 0b111111111111111111111111000000
20
+
21
+ SIGN_SHIFT = 29
22
+ YEAR_SHIFT = 9
23
+ MONTH_SHIFT = 5
24
+
25
+
5
26
  # Public: A class for handling partial date storage. Partial dates are stored
6
27
  # as an 8 digit integer with optional month and day values.
7
28
  #
@@ -19,6 +40,15 @@ module PartialDate
19
40
  class Date
20
41
  include Comparable
21
42
 
43
+ # Public: Readonly accessor for the raw bit integer date value.
44
+ # This allows us to perform our <=> comparions against this
45
+ # value instead of comparing year, month and day, or date.value
46
+ # which requires multiplication to calculate.
47
+ #
48
+ # Returns the single Integer backing store with 'bits' flipped
49
+ # for year, month and day.
50
+ attr_reader :bits
51
+
22
52
  # Public: Create a new partial date class from a block of integers
23
53
  # or strings.
24
54
  #
@@ -73,7 +103,7 @@ module PartialDate
73
103
  #
74
104
  # Returns an integer representation of a partial date.
75
105
  def value
76
- Bits.get_date(@bits)
106
+ self.class.get_date(@bits)
77
107
  end
78
108
 
79
109
  # Public: Set a date value using an interger in partial date format.
@@ -84,10 +114,10 @@ module PartialDate
84
114
  #
85
115
  # Returns nothing
86
116
  def value=(value)
87
- if value.is_a?(Integer) && (value >= 10000 && value <= 99991231)
88
- @bits = Bits.set_date(@bits, value)
117
+ if value.is_a?(Integer) && (value >= -10485761231 && value <= 10485761231)
118
+ @bits = self.class.set_date(@bits, value)
89
119
  else
90
- raise PartialDateError, "Date value must be an integer betwen 10000 and 99991231"
120
+ raise PartialDateError, "Date value must be an integer betwen -10485761231 and 10485761231"
91
121
  end
92
122
  end
93
123
 
@@ -105,55 +135,53 @@ module PartialDate
105
135
  def year=(value)
106
136
 
107
137
  if value.nil?
108
- raise PartialDateError, "Year cannot be nil"
138
+ raise YearError, "Year cannot be nil"
109
139
  end
110
140
 
111
141
  if value.is_a?(String)
112
- if value =~ /\A\d{4}\z/
142
+ if value =~ /\A\d{1,7}\z/
113
143
  value = value.to_i
114
144
  else
115
- raise PartialDateError, "Year must be a valid four digit string or integer between 1 and 9999"
145
+ raise YearError, "Year must be a valid string or integer from -1048576 to 1048576"
116
146
  end
117
147
  end
118
148
 
119
- if value.is_a?(Integer) && (value <= 9999 && value > 0)
120
- @bits = Bits.set_year(@bits, value)
149
+ if value.is_a?(Integer) && (value >= -1048576 && value <= 1048576)
150
+ @bits = self.class.set_year(@bits, value)
121
151
  else
122
- raise PartialDateError, "Year must be an integer between 1 and 9999"
152
+ raise YearError, "Year must be an integer integer from -1048576 to 1048576"
123
153
  end
124
154
  end
125
155
 
126
156
  # Public: Get the year from a partial date.
127
157
  def year
128
- Bits.get_year(@bits)
158
+ self.class.get_year(@bits)
129
159
  end
130
160
 
131
161
  # Public: Set the month of a partial date.
132
162
  def month=(value)
133
163
 
134
- raise PartialDateError, "A year must be set before a month" if year == 0
135
-
136
164
  value = 0 if value.nil?
137
165
 
138
166
  if value.is_a?(String)
139
167
  if value =~ /\A\d{1,2}\z/
140
168
  value = value.to_i
141
169
  else
142
- raise PartialDateError, "Month must be a valid one or two digit string or integer between 0 and 12"
170
+ raise MonthError, "Month must be a valid one or two digit string or integer between 0 and 12"
143
171
  end
144
172
  end
145
173
 
146
174
  if value.is_a?(Integer) && (value <= 12 && value >= 0)
147
- @bits = Bits.set_month(@bits, value)
148
- @bits = Bits.set_day(@bits, 0) if value == 0
175
+ @bits = self.class.set_month(@bits, value)
176
+ @bits = self.class.set_day(@bits, 0) if value == 0
149
177
  else
150
- raise PartialDateError, "Month must an be integer between 0 and 12"
178
+ raise MonthError, "Month must an be integer between 0 and 12"
151
179
  end
152
180
  end
153
181
 
154
182
  # Public: Get the month from a partial date.
155
183
  def month
156
- Bits.get_month(@bits)
184
+ self.class.get_month(@bits)
157
185
  end
158
186
 
159
187
 
@@ -161,7 +189,7 @@ module PartialDate
161
189
  # nil and empty strings are allowed.
162
190
  def day=(value)
163
191
 
164
- raise PartialDateError, "A month must be set before a day" if month == 0
192
+ raise DayError, "A month must be set before a day" if month == 0
165
193
 
166
194
  value = 0 if value.nil?
167
195
 
@@ -169,25 +197,25 @@ module PartialDate
169
197
  if value =~ /\A\d{1,2}\z/
170
198
  value = value.to_i
171
199
  else
172
- raise PartialDateError, "Day must be a valid one or two digit string or integer between 0 and 31"
200
+ raise DayError, "Day must be a valid one or two digit string or integer between 0 and 31"
173
201
  end
174
202
  end
175
203
 
176
204
  if value.is_a?(Integer) && (value >= 0 && value <= 31)
177
205
  begin
178
206
  date = ::Date.civil(self.year, self.month, value) if value > 0
179
- @bits = Bits.set_day(@bits, value)
207
+ @bits = self.class.set_day(@bits, value)
180
208
  rescue
181
- raise PartialDateError, "Day must be a valid day for the given month"
209
+ raise DayError, "Day must be a valid day for the given month"
182
210
  end
183
211
  else
184
- raise PartialDateError, "Day must be an integer between 0 and 31"
212
+ raise DayError, "Day must be an integer between 0 and 31"
185
213
  end
186
214
  end
187
215
 
188
216
  # Public: Get the day from a partial date.
189
217
  def day
190
- Bits.get_day(@bits)
218
+ self.class.get_day(@bits)
191
219
  end
192
220
 
193
221
  # Public: Returns a formatted string representation of the partial date.
@@ -200,41 +228,79 @@ module PartialDate
200
228
  #
201
229
  # Returns string representation of date.
202
230
  def to_s
203
- if self.year > 0
204
- result = self.year.to_s.rjust(4, '0')
205
- result = result + "-" + self.month.to_s.rjust(2, '0') if self.month > 0
206
- result = result + "-" + self.day.to_s.rjust(2, '0') if self.day > 0
231
+ if year > 0
232
+ result = year.to_s.rjust(4, '0')
233
+ result = result + "-" + month.to_s.rjust(2, '0') if month > 0
234
+ result = result + "-" + day.to_s.rjust(2, '0') if day > 0
207
235
  return result
208
236
  else
209
237
  return ""
210
238
  end
211
239
  end
212
240
 
213
- # Public: Spaceship operator for date comparisons. Comparisons start
214
- # with year, then month, then day - which are bitmask functions and
215
- # faster than 'self.value' which requires math to produce the integer.
241
+ # Public: Spaceship operator for date comparisons. Comparisons are
242
+ # made using the bit containing backing store. However the sign bit
243
+ # is in MSB - so we need to left shift both values by 1 first.
216
244
  #
217
245
  # Returns -1, 1, or 0
218
246
  def <=>(other_date)
219
- if self.year < other_date.year
220
- -1
221
- elsif self.year > other_date.year
222
- 1
223
- else
224
- if self.month < other_date.month
225
- -1
226
- elsif self.month > other_date.month
227
- 1
247
+ (@bits << 1) <=> (other_date.bits << 1)
248
+ end
249
+
250
+
251
+ def self.get_date(register)
252
+ date = (get_year(register) * 10000).abs + (get_month(register) * 100) + get_day(register)
253
+ if get_sign(register) == 1
254
+ date * -1
228
255
  else
229
- if self.day < other_date.day
230
- -1
231
- elsif self.day > other_date.day
232
- 1
233
- else
234
- 0
235
- end
256
+ date
236
257
  end
258
+ end
259
+
260
+ def self.set_date(register, value)
261
+ register = set_sign(register, 1) if value < 0
262
+ register = set_year(register, (value.abs / 10000).abs)
263
+ register = set_month(register, ((value - (value / 10000).abs * 10000) / 100).abs)
264
+ register = set_day(register, value - (value / 100).abs * 100)
265
+ end
266
+
267
+ def self.get_sign(register)
268
+ (register & SIGN_MASK) >> SIGN_SHIFT
269
+ end
270
+
271
+ def self.set_sign(register, value)
272
+ register = (register & ZERO_SIGN_MASK) | (value << SIGN_SHIFT)
273
+ end
274
+
275
+ def self.get_year(register)
276
+ year = (register & YEAR_MASK) >> YEAR_SHIFT
277
+ if get_sign(register) == 1
278
+ year * -1
279
+ else
280
+ year
237
281
  end
238
282
  end
283
+
284
+ def self.set_year(register, value)
285
+ register = set_sign(register, 1) if value < 0
286
+ register = (register & ZERO_YEAR_MASK) | (value.abs << YEAR_SHIFT)
287
+ end
288
+
289
+ def self.get_month(register)
290
+ (register & MONTH_MASK) >> MONTH_SHIFT
291
+ end
292
+
293
+ def self.set_month(register, value)
294
+ register = (register & ZERO_MONTH_MASK) | (value << MONTH_SHIFT)
295
+ end
296
+
297
+ def self.get_day(register)
298
+ register & DAY_MASK
299
+ end
300
+
301
+ def self.set_day(register, value)
302
+ register = (register & ZERO_DAY_MASK) | value
303
+ end
304
+
239
305
  end
240
306
  end
@@ -1,4 +1,13 @@
1
1
  module PartialDate
2
2
  class PartialDateError < StandardError
3
3
  end
4
+
5
+ class YearError < PartialDateError
6
+ end
7
+
8
+ class MonthError < PartialDateError
9
+ end
10
+
11
+ class DayError < PartialDateError
12
+ end
4
13
  end
@@ -1,4 +1,4 @@
1
1
  module PartialDate
2
2
  # partial-date version
3
- VERSION = "1.1.4"
3
+ VERSION = "1.1.5"
4
4
  end
data/spec/date_spec.rb CHANGED
@@ -44,7 +44,7 @@ describe PartialDate::Date do
44
44
  end
45
45
 
46
46
  it "should not allow an invalid date value to be set in a date instance" do
47
- expect {new_date = PartialDate::Date.new {|d| d.value = 100000000}}.to raise_error(PartialDate::PartialDateError, "Date value must be an integer betwen 10000 and 99991231")
47
+ expect {new_date = PartialDate::Date.new {|d| d.value = 10485761232 }}.to raise_error(PartialDate::PartialDateError, "Date value must be an integer betwen -10485761231 and 10485761231")
48
48
  end
49
49
 
50
50
  it "should return a string representation of date in the correct format" do
@@ -62,55 +62,63 @@ describe PartialDate::Date do
62
62
  new_date.to_s.should match(/\A\d{4}\z/)
63
63
  end
64
64
 
65
- describe "Year" do
66
- it "should raise an error if year is set to nil" do
67
- expect {date.year = nil}.to raise_error(PartialDate::PartialDateError)
65
+ describe "Sign" do
66
+ it "should be set to 1" do
67
+ register = 0
68
+ register = PartialDate::Date.set_sign(register, 1)
69
+ PartialDate::Date.get_sign(register).should == 1
68
70
  end
69
-
70
- it "should raise an error if year is set to an invalid string" do
71
- expect {date.year = "ABCD" }.to raise_error(PartialDate::PartialDateError, "Year must be a valid four digit string or integer between 1 and 9999")
71
+ it "should be 1 if year is a negative value" do
72
+ register = 0
73
+ register = PartialDate::Date.set_year(register, -9999)
74
+ PartialDate::Date.get_sign(register).should == 1
72
75
  end
73
76
 
74
- it "should raise an error if year is set to a five digit string" do
75
- expect {date.year = "10000" }.to raise_error(PartialDate::PartialDateError, "Year must be a valid four digit string or integer between 1 and 9999")
77
+ it "should be 0 if year is a positive value" do
78
+ register = 0
79
+ register = PartialDate::Date.set_year(register, 9999)
80
+ PartialDate::Date.get_sign(register).should == 0
76
81
  end
82
+ end
83
+
77
84
 
78
- it "should raise an error if year is set to a value greater than 9999" do
79
- expect {date.year = 10000 }.to raise_error(PartialDate::PartialDateError, "Year must be an integer between 1 and 9999")
85
+ describe "Year" do
86
+ it "should raise an error if year is set to nil" do
87
+ expect {date.year = nil}.to raise_error(PartialDate::YearError)
80
88
  end
81
89
 
82
- it "should raise an error if year is set to zero" do
83
- expect {date.year = 0 }.to raise_error(PartialDate::PartialDateError, "Year must be an integer between 1 and 9999")
90
+ it "should raise an error if year is set to an invalid string" do
91
+ expect {date.year = "ABCD" }.to raise_error(PartialDate::YearError, "Year must be a valid string or integer from -1048576 to 1048576")
84
92
  end
85
93
 
86
- it "should raise an error if year is set to a value less than zero" do
87
- expect {date.year = -1 }.to raise_error(PartialDate::PartialDateError, "Year must be an integer between 1 and 9999")
94
+ it "should raise an error if year is set to a value greater than 1048576" do
95
+ expect {date.year = 1048577 }.to raise_error(PartialDate::YearError, "Year must be an integer integer from -1048576 to 1048576")
88
96
  end
89
97
 
90
- it "should return a year when a year is set" do
98
+ it "should return a postive year when a positive year is set" do
91
99
  date.year = 2050
92
100
  date.year.should == 2050
93
101
  end
102
+
103
+ it "should return a negative year when a negative year is set" do
104
+ date.year = -9999
105
+ date.year.should == -9999
106
+ end
94
107
  end
95
108
 
96
109
  describe "Month" do
97
110
  before(:each) { date.year = 2000 }
98
111
 
99
- it "should raise an error if a month is set before a year" do
100
- no_year = PartialDate::Date.new
101
- expect {no_year.month = 10}.to raise_error(PartialDate::PartialDateError, "A year must be set before a month")
102
- end
103
-
104
112
  it "should raise an error if month is set to an invalid string" do
105
- expect {date.month = "AB"}.to raise_error(PartialDate::PartialDateError, "Month must be a valid one or two digit string or integer between 0 and 12")
113
+ expect {date.month = "AB"}.to raise_error(PartialDate::MonthError, "Month must be a valid one or two digit string or integer between 0 and 12")
106
114
  end
107
115
 
108
116
  it "should raise an error if month is set to a value greater than 12" do
109
- expect {date.month = 13}.to raise_error(PartialDate::PartialDateError, "Month must an be integer between 0 and 12")
117
+ expect {date.month = 13}.to raise_error(PartialDate::MonthError, "Month must an be integer between 0 and 12")
110
118
  end
111
119
 
112
120
  it "should raise an error if month is set to a value less than zero" do
113
- expect {date.month = -1}.to raise_error(PartialDate::PartialDateError, "Month must an be integer between 0 and 12")
121
+ expect {date.month = -1}.to raise_error(PartialDate::MonthError, "Month must an be integer between 0 and 12")
114
122
  end
115
123
 
116
124
  it "should allow the month to be set to zero" do
@@ -134,23 +142,23 @@ describe PartialDate::Date do
134
142
 
135
143
  it "should raise an error if a day is set before a year and month" do
136
144
  no_month = PartialDate::Date.new
137
- expect {no_month.day = 10}.to raise_error(PartialDate::PartialDateError, "A month must be set before a day")
145
+ expect {no_month.day = 10}.to raise_error(PartialDate::DayError, "A month must be set before a day")
138
146
  end
139
147
 
140
148
  it "should raise an error if day is set to an invalid string" do
141
- expect {date.day = "AB"}.to raise_error(PartialDate::PartialDateError, "Day must be a valid one or two digit string or integer between 0 and 31")
149
+ expect {date.day = "AB"}.to raise_error(PartialDate::DayError, "Day must be a valid one or two digit string or integer between 0 and 31")
142
150
  end
143
151
 
144
152
  it "should raise an error if day is set to a value less than zero" do
145
- expect {date.day = -1}.to raise_error(PartialDate::PartialDateError, "Day must be an integer between 0 and 31")
153
+ expect {date.day = -1}.to raise_error(PartialDate::DayError, "Day must be an integer between 0 and 31")
146
154
  end
147
155
 
148
156
  it "should raise an error if day is set to a value greater than 31" do
149
- expect {date.day = 32}.to raise_error(PartialDate::PartialDateError, "Day must be an integer between 0 and 31")
157
+ expect {date.day = 32}.to raise_error(PartialDate::DayError, "Day must be an integer between 0 and 31")
150
158
  end
151
159
 
152
160
  it "should raise an error if the day is an invalid day for the given month" do
153
- expect {date.day = 31}.to raise_error(PartialDate::PartialDateError, "Day must be a valid day for the given month")
161
+ expect {date.day = 31}.to raise_error(PartialDate::DayError, "Day must be a valid day for the given month")
154
162
  end
155
163
 
156
164
  it "should return zero when set to nil" do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: partial-date
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.4
4
+ version: 1.1.5
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-05-28 00:00:00.000000000 Z
12
+ date: 2012-05-29 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rubygems-tasks
@@ -78,7 +78,6 @@ files:
78
78
  - README.textile
79
79
  - Rakefile
80
80
  - lib/partial-date.rb
81
- - lib/partial-date/bits.rb
82
81
  - lib/partial-date/date.rb
83
82
  - lib/partial-date/error.rb
84
83
  - lib/partial-date/version.rb
@@ -1,52 +0,0 @@
1
- module PartialDate
2
-
3
- class Bits
4
-
5
- # Key:
6
- # The firt 5 bits are the day (max 31)
7
- # The next 4 bits are the month (max 12)
8
- # The topmost/leftmost 14 bits are the year (max 9999)
9
-
10
- DAY_MASK = 0b00000000000000000011111
11
- MONTH_MASK = 0b00000000000000111100000
12
- YEAR_MASK = 0b11111111111111000000000
13
-
14
- ZERO_YEAR_MASK = 0b00000000000000111111111
15
- ZERO_MONTH_MASK = 0b11111111111111000011111
16
- ZERO_DAY_MASK = 0b11111111111111111000000
17
-
18
- def self.get_date(register)
19
- (get_year(register) * 10000) + (get_month(register) * 100) + get_day(register)
20
- end
21
-
22
- def self.set_date(register, value)
23
- register = set_year(register, (value / 10000).abs)
24
- register = set_month(register, ((value - (value / 10000).abs * 10000) / 100).abs)
25
- register = set_day(register, value - (value / 100).abs * 100)
26
- end
27
-
28
- def self.get_year(register)
29
- (register & YEAR_MASK) >> 9
30
- end
31
-
32
- def self.set_year(register, value)
33
- register = (register & ZERO_YEAR_MASK) | (value << 9)
34
- end
35
-
36
- def self.get_month(register)
37
- (register & MONTH_MASK) >> 5
38
- end
39
-
40
- def self.set_month(register, value)
41
- register = (register & ZERO_MONTH_MASK) | (value << 5)
42
- end
43
-
44
- def self.get_day(register)
45
- register & DAY_MASK
46
- end
47
-
48
- def self.set_day(register, value)
49
- register = (register & ZERO_DAY_MASK) | value
50
- end
51
- end
52
- end