fuzzy_date 0.8.2

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