partial-date 1.1.4 → 1.1.5

Sign up to get free protection for your applications and to get access to all the features.
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