fuzzy_date 0.8.2

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.
Files changed (4) hide show
  1. data/README +57 -0
  2. data/lib/fuzzy_date.rb +305 -0
  3. data/test/test_fuzzy_date.rb +122 -0
  4. metadata +56 -0
data/README ADDED
@@ -0,0 +1,57 @@
1
+ Routines to manage the FuzzyDate Class. There is also an extension to the
2
+ ActiveRecord::Base class to provide an 'acts_as_fuzzy_date' class method.
3
+
4
+ Historical dates are often incomplete or approximate and this class allows
5
+ such dates to be worked with and stored on a database.
6
+
7
+ construct a new object with FuzzyDate.new( year,month,day,wday,circa)
8
+ year: optional..is the year number 0 .. big as you like, -ve for BCE
9
+ month: optional.. is month number 1..12, or nil
10
+ day: optional.. is day number 1-31 or nil
11
+ wday: optional.. day of the week - 0=Sunday .. 6=Saturday
12
+ circa: true = daye is approx, or nil/false
13
+
14
+ note that if you supply the weekday and a year/month/day the system will
15
+ use your weekday as supplied even if this does not actually correspond
16
+ in reality to the date supplied.
17
+
18
+ construct a new object with FuzzyDate.parse("date_string")
19
+ date_string = eg: "tuesday"
20
+ "12 nov 1012"
21
+ "circa 412 bc"
22
+
23
+ Use in a database table by storing in an integer field of a format large
24
+ enough to hold the digits of your maximum year + 8. Eg. a BIGNUM field
25
+ stores 19 useful characters allowing the year to go to 99,999,999,999.
26
+ ie 99 billion years.
27
+
28
+ Use with an ActiveRecord class ..
29
+
30
+ require 'fuzzy_date'
31
+
32
+ class HistoricalPerson < ActiveRecord::Base
33
+ acts_as_fuzzy_date : birth_date, death_date
34
+ end
35
+
36
+
37
+
38
+ Copyright (c) Clive Andrews / Reality Bites 2008, 2009, 2010
39
+
40
+ Permission is hereby granted, free of charge, to any person obtaining
41
+ a copy of this software and associated documentation files (the
42
+ "Software"), to deal in the Software without restriction, including
43
+ without limitation the rights to use, copy, modify, merge, publish,
44
+ distribute, sublicense, and/or sell copies of the Software, and to
45
+ permit persons to whom the Software is furnished to do so, subject to
46
+ the following conditions:
47
+
48
+ The above copyright notice and this permission notice shall be
49
+ included in all copies or substantial portions of the Software.
50
+
51
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
52
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
53
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
54
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
55
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
56
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
57
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/lib/fuzzy_date.rb ADDED
@@ -0,0 +1,305 @@
1
+ ################################################################################
2
+ #
3
+ # Routines to manage the FuzzyDate Class. There is also an extension to the
4
+ # ActiveRecord::Base class to provide an 'acts_as_fuzzy_date' class method.
5
+ #--
6
+ # Copyright (c) Clive Andrews / Reality Bites 2008, 2009, 2010
7
+ #
8
+ # Permission is hereby granted, free of charge, to any person obtaining
9
+ # a copy of this software and associated documentation files (the
10
+ # "Software"), to deal in the Software without restriction, including
11
+ # without limitation the rights to use, copy, modify, merge, publish,
12
+ # distribute, sublicense, and/or sell copies of the Software, and to
13
+ # permit persons to whom the Software is furnished to do so, subject to
14
+ # the following conditions:
15
+ #
16
+ # The above copyright notice and this permission notice shall be
17
+ # included in all copies or substantial portions of the Software.
18
+ #
19
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
21
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
23
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
24
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
25
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
26
+ #
27
+ # version 20100224-1
28
+ # ++
29
+ #
30
+ ################################################################################
31
+ require 'date'
32
+
33
+
34
+ # A fuzzy date is a representation of a date which may well be incomplete
35
+ # or imprecise. You can enter the exact date or for example just the day of
36
+ # the week or the year or a combination thereof. One can also add the Circa
37
+ # prefix to any date. The FuzzyDate object is immutable so if you wish to
38
+ # change a FuzzyDate value it is neccessary to create a new FuzzyDate.
39
+ class FuzzyDate
40
+
41
+ include Comparable
42
+
43
+ # overload the Date class to modify its parsing routine a little
44
+ #
45
+ class FDate < Date # :nodoc:
46
+ def self._parse_year(str, e)
47
+ if str.sub!(/(\d{3,})\b/in, ' ')
48
+ e.year = $1.to_i
49
+ true
50
+ else
51
+ super(str,e)
52
+ end
53
+ end
54
+ end # FDate
55
+
56
+ # create a new FuzzyDate object. There are no checks here to
57
+ # validate if the date is valid or not.. eg there is no check
58
+ # that the day of the week actually corresponds to the actual
59
+ # date if completely specified.
60
+ def initialize(year=nil,month=nil,day=nil,wday=nil,circa=nil)
61
+ raise "FuzzyDate: invalid month" if month && ((month>12) || (month<1))
62
+ raise "FuzzyDate: invalid day" if day && ((day>31) || (day<1))
63
+ raise "FuzzyDate: invalid weekday" if wday && ((wday>6) || (wday<0))
64
+ raise "FuzzyDate: year too big !" if year && (year.abs > 99999999999 )
65
+ @year = year && year.abs
66
+ @month = month
67
+ @day = day
68
+ @wday = wday
69
+ @circa = circa
70
+ @bce = year && (year.to_i < 0)
71
+ end
72
+
73
+ # returns an integer representing the day of the week, 0..6
74
+ # with Sunday=0. returns nil if not known.
75
+ def wday
76
+ if !@wday && complete? && (@year > 0)
77
+ to_date.wday
78
+ else
79
+ @wday
80
+ end
81
+ end
82
+
83
+ # returns the day of the month ( 1..n ). returns nil if not known.
84
+ def day
85
+ @day
86
+ end
87
+
88
+ # returns the month number (1..12). returns nil if not known.
89
+ def month
90
+ @month
91
+ end
92
+
93
+ # returns the year number (including century)
94
+ def year
95
+ @year
96
+ end
97
+
98
+ # is the date approximate ?
99
+ def circa?
100
+ @circa
101
+ end
102
+
103
+ # is the date before the year zero.
104
+ def bce?
105
+ @bce
106
+ end
107
+
108
+ # return an integer representing only the month and day
109
+ def birthday
110
+ month * 100 + day if month && day
111
+ end
112
+
113
+ # is the date complete
114
+ def complete?
115
+ year && (month && month < 13) && (day && day < 32)
116
+ end
117
+
118
+ # is the date completely unknown
119
+ def unknown?
120
+ !@year && !@month && !@day && !@wday
121
+ end
122
+
123
+ # convert to integer format
124
+ def to_i
125
+ to_db
126
+ end
127
+
128
+ # convert the fuzzy date into a format which can be stored in a database. The
129
+ # storage format is integer format (BIGINT) with digits having the following positional
130
+ # significance:
131
+ #
132
+ # (-/+) (YYYYYYYYYYY.....Y)MMDDd[01][01]
133
+ #
134
+ # where + is AD or CE
135
+ # - is BC or BCE
136
+ # 1 at end = circa or C otherwise 0
137
+ # 1 at 2nd from end = year unknown - otherwise missing year = year 0
138
+ # YYYYY is the year number 0 or missing = year absent OR value is unknown
139
+ # MM is the month number or 13 for unknown
140
+ # DD is the day of the month or 32 for unknown
141
+ # d is the day of the week where 8 = unknown, 1=Sunday .. 7=Saturday
142
+ #
143
+ #
144
+ # this wierd format has been chosen to allow sorting within the database and to
145
+ # avoid leading zeros containing information from being stripped from the representation.
146
+ #
147
+ # save in a mysql database in format BIGINT
148
+ #
149
+ def to_db
150
+ str = ''
151
+ str= str + (@year > 0 ? @year.to_s : '') if @year
152
+ str= str + (@month ? "%02d" % @month.to_s : '13')
153
+ str= str + (@day ? "%02d" % @day.to_s : '32')
154
+ str= str + (@wday ? (@wday+1).to_s : '8')
155
+ str= str + (@year ? '0' : '1')
156
+ str= str + (@circa ? '1' : '0')
157
+ i = str.to_i
158
+ i = -i if @bce
159
+ i
160
+ end
161
+
162
+ # create a new FuzzyDate object from the database formatted
163
+ # integer.
164
+ def self.new_from_db(i)
165
+ return nil unless i
166
+ str = i.to_s
167
+ return nil if str == '0'
168
+ bce = false
169
+ raise "Invalid Fuzzy Time String - #{str}" unless str =~/^[+-]?\d{6,}$/
170
+ str.sub!(/^-/){|m| bce= true;""}
171
+ str = ("000000" + str)[-7,7] if str.length < 7
172
+ circa = (str[-1,1] == '1')
173
+ year = (str[-2,1] == '1' ? nil : 0)
174
+ wday = str[-3,1].to_i - 1
175
+ day = str[-5,2].to_i
176
+ month = str[-7,2].to_i
177
+ wday = nil if ((wday<0) || (wday>6))
178
+ day = nil if ((day==0) || (day>31))
179
+ month = nil if ((month==0) || (month > 12))
180
+ year = (str[0..-8].to_i > 0 ? str[0..-8].to_i : year )
181
+ year = -year if year && bce
182
+ new(year,month,day,wday,circa)
183
+ end
184
+
185
+ # convert to a human readable string
186
+ def to_s
187
+ if unknown?
188
+ str = "unknown"
189
+ else
190
+ str = ""
191
+ str = "circa " if circa?
192
+ str+= FDate::DAYNAMES[wday] + " " if wday
193
+ str+= day.to_s + " " if day
194
+ str+= FDate::MONTHNAMES[month] + " " if month
195
+ str+= year.to_s if year
196
+ str += " bce" if bce?
197
+ str.strip
198
+ end
199
+ end
200
+
201
+ # if the date is complete then return a regular
202
+ # Date object
203
+ def to_date
204
+ return nil unless complete?
205
+ Date.new(@year,@month,@day)
206
+ end
207
+
208
+ # create a FuzzyDate object from a Date object
209
+ def self.new_from_date( date)
210
+ new(date.year,date.month,date.day)
211
+ end
212
+
213
+ # create a new date object by parsing a string
214
+ def self.parse( str )
215
+
216
+ return unless str && str.length > 0
217
+
218
+ continue = true
219
+ circa = false
220
+ bce = false
221
+ unknown = false
222
+
223
+ #filter out 'c' or 'circa'
224
+ str.sub!(/CIRCA/i){|m| circa=true;continue=nil} if continue
225
+ str.sub!(/^CA /i){|m| circa=true;continue=nil} if continue
226
+ str.sub!(/^C /i){|m| circa=true;continue=nil} if continue
227
+ str.sub!(/ABOUT/i){|m| circa=true;continue=nil} if continue
228
+ str.sub!(/AROUND/i){|m| circa=true;continue=nil} if continue
229
+ str.sub!(/ROUND/i){|m| circa=true;continue=nil} if continue
230
+ str.sub!(/APPROX/i){|m| circa=true;continue=nil} if continue
231
+ str.sub!(/APPROXIMATELY/i){|m| circa=true;continue=nil} if continue
232
+
233
+ #filter out 'bc' 'bce'
234
+ continue = true
235
+ str.sub!(/BCE/i){|m| bce=true;continue=nil}
236
+ str.sub!(/BC/i){|m| bce=true;continue=nil} if continue
237
+
238
+ #filter out 'unknown'
239
+ continue = true
240
+ str.sub!(/UNKNOWN/i){|m| unknown=true;continue=nil}
241
+
242
+ # if date is unknown then return an empty FuzzyDate
243
+ return self.new if unknown
244
+
245
+ # now try to parse the remaining string with the Date parse
246
+ # method.
247
+
248
+ components= FDate._parse(str,false)
249
+ year = components[:year]
250
+ month = components[:mon]
251
+ day = components[:mday]
252
+ wday = components[:wday]
253
+
254
+ # fudge the results a bit
255
+ year,day = day,nil if (day && !month && !year) || (!year && (day.to_i > 31))
256
+ if year && year < 0
257
+ year = year.abs
258
+ bce = true
259
+ end
260
+
261
+ year = -year if bce
262
+ self.new(year,month,day,wday,circa)
263
+ end
264
+
265
+ def <=>(other) self.to_db <=> other.to_db end
266
+
267
+ end #class FuzzyDate
268
+
269
+ # add an acts_as_fuzzy_date helper to ActiveRecord to define
270
+ # a table field as a FuzzyDate.
271
+ #
272
+ # eg:
273
+ #
274
+ # require 'fuzzy_date'
275
+ #
276
+ # class HistoricalPerson < ActiveRecord::Base
277
+ # acts_as_fuzzy_date : birth_date, death_date
278
+ # end
279
+ #
280
+ if defined? ActiveRecord::Base
281
+ class ActiveRecord::Base
282
+ class << self
283
+ def acts_as_fuzzy_date(*args)
284
+ args.each do |name|
285
+ str =<<-EOF
286
+ def #{name}
287
+ FuzzyDate.new_from_db(self['#{name}'])
288
+ end
289
+
290
+ def #{name}=(s)
291
+ if s.kind_of? String
292
+ self['#{name}'] = FuzzyDate.parse(s).to_db unless s.strip.empty?
293
+ elsif s.kind_of? FuzzyDate
294
+ self['#{name}']=s.to_db
295
+ elsif !s
296
+ self['#{name}'] = nil
297
+ end
298
+ end
299
+ EOF
300
+ class_eval str
301
+ end
302
+ end
303
+ end
304
+ end
305
+ end
@@ -0,0 +1,122 @@
1
+ $:.unshift File.join(File.dirname(__FILE__),"..","lib")
2
+
3
+ require "test/unit"
4
+ require 'fuzzy_date'
5
+
6
+ class TestItem < Test::Unit::TestCase
7
+ def test_basic_parse
8
+ # complete date
9
+ assert_not_nil(FuzzyDate.new)
10
+ d1 = FuzzyDate.parse("Thurs 31 december 1998" )
11
+ assert_equal(31, d1.day )
12
+ assert_equal(12, d1.month )
13
+ assert_equal(1998,d1.year)
14
+ assert_equal(4, d1.wday )
15
+ assert ( ! d1.bce?)
16
+ assert ( ! d1.circa?)
17
+ assert ( d1.complete?)
18
+ assert ( !d1.unknown?)
19
+ # empty
20
+ d1 = FuzzyDate.parse("" )
21
+ assert_nil( d1.day )
22
+ assert_nil( d1.month )
23
+ assert_nil(d1.year)
24
+ assert ( ! d1.circa?)
25
+ assert_equal(nil, d1.wday )
26
+ assert ( ! d1.bce?)
27
+ assert ( !d1.complete?)
28
+ assert ( d1.unknown?)
29
+ # just year
30
+ d1 = FuzzyDate.parse("1540" )
31
+ assert_nil( d1.day )
32
+ assert_nil( d1.month )
33
+ assert_equal(1540,d1.year)
34
+ assert_equal(nil, d1.wday )
35
+ assert ( ! d1.bce?)
36
+ assert ( !d1.complete?)
37
+ assert ( ! d1.circa?)
38
+ assert ( !d1.unknown?)
39
+ # just month
40
+ d1 = FuzzyDate.parse("september" )
41
+ assert_nil( d1.day )
42
+ assert_equal(9, d1.month )
43
+ assert_nil(d1.year)
44
+ assert_equal(nil, d1.wday )
45
+ assert ( ! d1.bce?)
46
+ assert ( !d1.complete?)
47
+ assert ( ! d1.circa?)
48
+ assert ( !d1.unknown?)
49
+ # just weekday
50
+ d1 = FuzzyDate.parse("tuesday" )
51
+ assert_equal(nil, d1.day )
52
+ assert_equal(2, d1.wday )
53
+ assert_nil( d1.month )
54
+ assert_nil(d1.year)
55
+ assert ( ! d1.bce?)
56
+ assert ( !d1.complete?)
57
+ assert ( ! d1.circa?)
58
+ assert ( !d1.unknown?)
59
+ # circa
60
+ d1 = FuzzyDate.parse("c sunday 1066" )
61
+ assert_equal(nil, d1.day )
62
+ assert_equal(0, d1.wday )
63
+ assert_nil( d1.month )
64
+ assert_equal(1066,d1.year)
65
+ assert ( ! d1.bce?)
66
+ assert ( !d1.complete?)
67
+ assert ( d1.circa?)
68
+ assert ( !d1.unknown?)
69
+ # bce
70
+ d1 = FuzzyDate.parse(" 23 march 366 bc" )
71
+ assert_equal(23, d1.day )
72
+ assert_equal(nil, d1.wday )
73
+ assert_equal(3, d1.month )
74
+ assert_equal(366,d1.year)
75
+ assert ( d1.bce?)
76
+ assert ( d1.complete?)
77
+ assert ( !d1.circa?)
78
+ assert ( !d1.unknown?)
79
+ end
80
+
81
+ def test_database
82
+ 10000.times do
83
+ d = make_fuzzy_date
84
+ puts d.to_s
85
+ i = d.to_i
86
+ i = i * 1 # reformat ?
87
+ d2 = FuzzyDate.new_from_db( i )
88
+ assert_equal( d.to_i, d2.to_i)
89
+ end
90
+ end
91
+
92
+ def test_compare
93
+ assert( FuzzyDate.parse("23 april 2000") == FuzzyDate.parse("23 april 2000") )
94
+ assert( FuzzyDate.parse("24 april 2000") > FuzzyDate.parse("23 april 2000") )
95
+ assert( FuzzyDate.parse("april 2000") > FuzzyDate.parse("march 2000") )
96
+ assert( FuzzyDate.parse("jan 2001") > FuzzyDate.parse("dec 2000") )
97
+ assert( FuzzyDate.parse("sept") > FuzzyDate.parse("july") )
98
+ end
99
+
100
+ # generate a random fuzzy date
101
+ def make_fuzzy_date
102
+ # 1 2 3 4 5 6 7 8 9 10 11 12
103
+ tab=[31,28,31,30,31,30,31,31,30,31,30,31]
104
+ year = rand(2000)
105
+ month = rand(12) + 1
106
+ wday = rand(7)
107
+ day = rand(tab[month-1]) + 1
108
+ circa = rand(4) == 0
109
+ bce = rand(2) == 0
110
+ do_year = rand(3) > 0
111
+ do_month = rand(3) > 0
112
+ do_day = rand(3) > 0
113
+ do_wday = rand(3) > 0
114
+
115
+ year = - year if bce
116
+ FuzzyDate.new( do_year && year,
117
+ do_month && month,
118
+ do_day && day,
119
+ do_wday && wday,
120
+ circa)
121
+ end
122
+ end
metadata ADDED
@@ -0,0 +1,56 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fuzzy_date
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.8.2
5
+ platform: ruby
6
+ authors:
7
+ - Clive Andrews
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-02-23 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: FuzzyDate is a Ruby class for working with incomplete dates.
17
+ email: pacman@realitybites.eu
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - README
26
+ - lib/fuzzy_date.rb
27
+ has_rdoc: true
28
+ homepage: http://www.realitybites.eu
29
+ licenses: []
30
+
31
+ post_install_message:
32
+ rdoc_options:
33
+ - --charset=UTF-8
34
+ require_paths:
35
+ - lib
36
+ required_ruby_version: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: "0"
41
+ version:
42
+ required_rubygems_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: "0"
47
+ version:
48
+ requirements: []
49
+
50
+ rubyforge_project: fuzzy_date
51
+ rubygems_version: 1.3.5
52
+ signing_key:
53
+ specification_version: 3
54
+ summary: FuzzyDate is a Ruby class for working with incomplete dates.Can be used in Combination with ActiveRecord.
55
+ test_files:
56
+ - test/test_fuzzy_date.rb