ndr_support 3.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +14 -0
- data/.rubocop.yml +27 -0
- data/.ruby-version +1 -0
- data/.travis.yml +22 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/Guardfile +16 -0
- data/LICENSE.txt +21 -0
- data/README.md +91 -0
- data/Rakefile +12 -0
- data/code_safety.yml +258 -0
- data/gemfiles/Gemfile.rails32 +6 -0
- data/gemfiles/Gemfile.rails32.lock +108 -0
- data/gemfiles/Gemfile.rails41 +6 -0
- data/gemfiles/Gemfile.rails41.lock +111 -0
- data/gemfiles/Gemfile.rails42 +6 -0
- data/gemfiles/Gemfile.rails42.lock +111 -0
- data/lib/ndr_support.rb +21 -0
- data/lib/ndr_support/array.rb +52 -0
- data/lib/ndr_support/concerns/working_days.rb +94 -0
- data/lib/ndr_support/date_and_time_extensions.rb +103 -0
- data/lib/ndr_support/daterange.rb +196 -0
- data/lib/ndr_support/fixnum/calculations.rb +15 -0
- data/lib/ndr_support/fixnum/julian_date_conversions.rb +14 -0
- data/lib/ndr_support/hash.rb +52 -0
- data/lib/ndr_support/integer.rb +12 -0
- data/lib/ndr_support/nil.rb +38 -0
- data/lib/ndr_support/ourdate.rb +97 -0
- data/lib/ndr_support/ourtime.rb +51 -0
- data/lib/ndr_support/regexp_range.rb +65 -0
- data/lib/ndr_support/safe_file.rb +185 -0
- data/lib/ndr_support/safe_path.rb +268 -0
- data/lib/ndr_support/string/cleaning.rb +136 -0
- data/lib/ndr_support/string/conversions.rb +137 -0
- data/lib/ndr_support/tasks.rb +1 -0
- data/lib/ndr_support/time/conversions.rb +13 -0
- data/lib/ndr_support/utf8_encoding.rb +72 -0
- data/lib/ndr_support/utf8_encoding/control_characters.rb +53 -0
- data/lib/ndr_support/utf8_encoding/force_binary.rb +44 -0
- data/lib/ndr_support/utf8_encoding/object_support.rb +31 -0
- data/lib/ndr_support/version.rb +5 -0
- data/lib/ndr_support/yaml/serialization_migration.rb +65 -0
- data/lib/tasks/audit_code.rake +423 -0
- data/ndr_support.gemspec +39 -0
- data/test/array_test.rb +20 -0
- data/test/concerns/working_days_test.rb +122 -0
- data/test/daterange_test.rb +194 -0
- data/test/fixnum/calculations_test.rb +28 -0
- data/test/hash_test.rb +84 -0
- data/test/integer_test.rb +14 -0
- data/test/nil_test.rb +40 -0
- data/test/ourdate_test.rb +27 -0
- data/test/ourtime_test.rb +27 -0
- data/test/regexp_range_test.rb +135 -0
- data/test/resources/filesystem_paths.yml +37 -0
- data/test/safe_file_test.rb +597 -0
- data/test/safe_path_test.rb +168 -0
- data/test/string/cleaning_test.rb +176 -0
- data/test/string/conversions_test.rb +353 -0
- data/test/test_helper.rb +41 -0
- data/test/time/conversions_test.rb +15 -0
- data/test/utf8_encoding/control_characters_test.rb +84 -0
- data/test/utf8_encoding/force_binary_test.rb +64 -0
- data/test/utf8_encoding_test.rb +170 -0
- data/test/yaml/serialization_test.rb +145 -0
- metadata +295 -0
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
require 'ndr_support/concerns/working_days'
|
4
|
+
[Time, Date, DateTime].each { |klass| klass.send(:include, WorkingDays) }
|
5
|
+
|
6
|
+
class Date
|
7
|
+
# Note: default date format is specified in config/environment.rb
|
8
|
+
def to_verbose
|
9
|
+
strftime('%d %B %Y')
|
10
|
+
end # our long format
|
11
|
+
|
12
|
+
# to_iso output must be SQL safe for security reasons
|
13
|
+
def to_iso
|
14
|
+
strftime('%Y-%m-%d')
|
15
|
+
end # ISO date format
|
16
|
+
|
17
|
+
def to_ours
|
18
|
+
to_s
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_YYYYMMDD # convert dates into format 'YYYYMMDD' - used in tracing
|
22
|
+
self.year.to_s + self.month.to_s.rjust(2, '0') + self.day.to_s.rjust(2, '0')
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
#-------------------------------------------------------------------------------
|
27
|
+
|
28
|
+
class Time
|
29
|
+
def to_ours
|
30
|
+
strftime('%d.%m.%Y %H:%M')
|
31
|
+
end # Show times in our format
|
32
|
+
|
33
|
+
# to_iso output must be SQL safe for security reasons
|
34
|
+
def to_iso
|
35
|
+
strftime('%Y-%m-%dT%H:%M:%S')
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
#-------------------------------------------------------------------------------
|
40
|
+
|
41
|
+
module NdrSupport
|
42
|
+
class << self
|
43
|
+
# Within the NDR, we change default date formatting, as below.
|
44
|
+
# This can cause problems with YAML emitted by syck, so we have to
|
45
|
+
# patch Date#to_yaml too.
|
46
|
+
def apply_era_date_formats!
|
47
|
+
update_date_formats!
|
48
|
+
update_time_formats!
|
49
|
+
|
50
|
+
attempt_date_patch!
|
51
|
+
end
|
52
|
+
|
53
|
+
def attempt_date_patch!
|
54
|
+
# There are potential load order issues with this patch,
|
55
|
+
# as it needs to be applied once syck has loaded.
|
56
|
+
fail('Date#to_yaml must exist to be patched!') unless Date.respond_to?(:to_yaml)
|
57
|
+
apply_date_patch!
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def apply_date_patch!
|
63
|
+
# With YAML's crazy engine switching, it seems we
|
64
|
+
# can't rely including a module to define this method:
|
65
|
+
Date.module_eval do
|
66
|
+
def to_yaml(opts = {})
|
67
|
+
::YAML.quick_emit(object_id, opts) do |out|
|
68
|
+
out.scalar('tag:yaml.org,2002:timestamp', to_s(:yaml), :plain)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Override default date and time formats:
|
75
|
+
def update_date_formats!
|
76
|
+
Date::DATE_FORMATS.update(
|
77
|
+
:db => '%Y-%m-%d %H:%M:%S',
|
78
|
+
:ui => '%d.%m.%Y',
|
79
|
+
:yaml => '%Y-%m-%d', # For Dates
|
80
|
+
:default => '%d.%m.%Y'
|
81
|
+
)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Rails 2 loads Oracle dates (with timestamps) as DateTime or Time values
|
85
|
+
# (before or after 1970) whereas Rails 1.2 treated them as Date objects.
|
86
|
+
# Therefore we have a formatting challenge, which we overcome by hiding
|
87
|
+
# the time if it's exactly midnight
|
88
|
+
def update_time_formats!
|
89
|
+
Time::DATE_FORMATS.update(
|
90
|
+
:db => '%Y-%m-%d %H:%M:%S',
|
91
|
+
:ui => '%d.%m.%Y %H:%M',
|
92
|
+
:yaml => '%Y-%m-%d %H:%M:%S %:z', # For DateTimes
|
93
|
+
:default => lambda do |time|
|
94
|
+
non_zero_time = time.hour != 0 || time.min != 0 || time.sec != 0
|
95
|
+
time.strftime(non_zero_time ? '%d.%m.%Y %H:%M' : '%d.%m.%Y')
|
96
|
+
end
|
97
|
+
)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Maintain API:
|
103
|
+
NdrSupport.attempt_date_patch!
|
@@ -0,0 +1,196 @@
|
|
1
|
+
require 'active_support/time'
|
2
|
+
require 'ndr_support/fixnum/julian_date_conversions'
|
3
|
+
|
4
|
+
# Our "vague date" class, which can represent a single date or a date range.
|
5
|
+
class Daterange
|
6
|
+
attr_reader :date1, :date2, :source
|
7
|
+
|
8
|
+
OKYEARS = 1880..2020
|
9
|
+
|
10
|
+
def self.extract(dates_string)
|
11
|
+
dates_string.to_s.split(',').map { |str| new(str) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.merge(dates_string)
|
15
|
+
ranges = extract(dates_string)
|
16
|
+
new(ranges.map(&:date1).compact.min, ranges.map(&:date2).compact.max)
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(x1 = nil, x2 = nil)
|
20
|
+
x1 = x1.to_datetime if x1.is_a?(Date) || x1.is_a?(Time)
|
21
|
+
x2 = x2.to_datetime if x2.is_a?(Date) || x2.is_a?(Time)
|
22
|
+
|
23
|
+
if x1.is_a?(DateTime) && x2.is_a?(DateTime)
|
24
|
+
@date1 = [x1, x2].min
|
25
|
+
@date2 = [x1, x2].max
|
26
|
+
@source = nil
|
27
|
+
elsif x1.is_a?(Daterange) && x2.nil? # Patient model line 645
|
28
|
+
@date1 = x1.date1
|
29
|
+
@date2 = x1.date2
|
30
|
+
@source = x1.source
|
31
|
+
elsif x1.is_a?(DateTime) && x2.nil?
|
32
|
+
@date1 = x1
|
33
|
+
@date2 = x1
|
34
|
+
@source = nil
|
35
|
+
elsif x1.is_a?(String) && x2.nil?
|
36
|
+
self.source = (x1)
|
37
|
+
else
|
38
|
+
@date1 = nil
|
39
|
+
@date2 = nil
|
40
|
+
@source = nil
|
41
|
+
end
|
42
|
+
self.freeze
|
43
|
+
end
|
44
|
+
|
45
|
+
# If we have a valid date range, return a string representation of it
|
46
|
+
# TODO: possibly add support for to_s(format) e.g. to_s(:short)
|
47
|
+
def to_s
|
48
|
+
return '' unless @date1 && @date2
|
49
|
+
if @date1 == @date2 # single date
|
50
|
+
tidy_string_if_midnight(@date1)
|
51
|
+
elsif tidy_string_if_midnight(@date1) == tidy_string_if_midnight(@date2.at_beginning_of_year) &&
|
52
|
+
tidy_string_if_midnight(@date2) == tidy_string_if_midnight(@date1.at_end_of_year.at_beginning_of_day) # whole year
|
53
|
+
@date1.strftime('%Y')
|
54
|
+
elsif tidy_string_if_midnight(@date1) == tidy_string_if_midnight(@date2.at_beginning_of_month) &&
|
55
|
+
tidy_string_if_midnight(@date2) == tidy_string_if_midnight(@date1.at_end_of_month.at_beginning_of_day) # whole month
|
56
|
+
@date1.strftime('%m.%Y')
|
57
|
+
else # range
|
58
|
+
tidy_string_if_midnight(@date1) + ' to ' + tidy_string_if_midnight(@date2)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# used in Address model
|
63
|
+
# to_iso output must be SQL safe for security reasons
|
64
|
+
def to_iso
|
65
|
+
date1.is_a?(DateTime) ? date1.to_iso : ''
|
66
|
+
end
|
67
|
+
|
68
|
+
# A long string representation of the date or range
|
69
|
+
def verbose
|
70
|
+
return 'Bad date(s)' unless @date1 && @date2
|
71
|
+
if @date1 == @date2 # single date
|
72
|
+
@date1.to_verbose
|
73
|
+
else # range
|
74
|
+
'The period ' + @date1.to_verbose + ' to ' + @date2.to_verbose +
|
75
|
+
' inclusive (' + (@date2 - @date1 + 1).to_s + ' days)'
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def date1=(d)
|
80
|
+
if @source
|
81
|
+
@source += ' [d1 modified]'
|
82
|
+
else
|
83
|
+
@source = '[d1 modified]'
|
84
|
+
end
|
85
|
+
@date1 = d
|
86
|
+
end
|
87
|
+
|
88
|
+
def date2=(d)
|
89
|
+
if @source
|
90
|
+
@source += ' [d2 modified]'
|
91
|
+
else
|
92
|
+
@source = '[d2 modified]'
|
93
|
+
end
|
94
|
+
@date2 = d
|
95
|
+
end
|
96
|
+
|
97
|
+
def <=>(other)
|
98
|
+
self.date1 <=> other.date1
|
99
|
+
end
|
100
|
+
|
101
|
+
def ==(other)
|
102
|
+
self.date1 == other.date1 && self.date2 == other.date2
|
103
|
+
end
|
104
|
+
|
105
|
+
def intersects?(other)
|
106
|
+
!(self.empty? || other.empty?) && self.date1 <= other.date2 && self.date2 >= other.date1
|
107
|
+
end
|
108
|
+
|
109
|
+
def empty?
|
110
|
+
# An unspecified date will be empty. A valid or invalid date will not.
|
111
|
+
@date1.nil? && @source.blank?
|
112
|
+
end
|
113
|
+
|
114
|
+
def exact?
|
115
|
+
@date1 == @date2
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def tidy_string_if_midnight(datetime)
|
121
|
+
if datetime.hour == 0 && datetime.min == 0 && datetime.sec == 0
|
122
|
+
# it's midnight
|
123
|
+
datetime.to_date.to_s(:ui)
|
124
|
+
else
|
125
|
+
return datetime.to_time.to_s(:ui)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Update our attribute values using a string representation of the date(s).
|
130
|
+
# +s+ consists of one or more dates separated with spaces.
|
131
|
+
# Each date can be in various formats, e.g. d/m/yyyy, ddmmyyyy, yyyy-mm-dd.
|
132
|
+
# Each date can omit days or months, e.g. yyyy, dd/yyyy, yyyy-mm
|
133
|
+
def source=(s)
|
134
|
+
@source = s
|
135
|
+
ss = s.upcase.sub(/TO/, ' ') # accept default _to_s format
|
136
|
+
if ss =~ /[^0-9\-\/\. ]/ # only allow digits, hyphen, slash, dot, space
|
137
|
+
@date1 = @date2 = nil
|
138
|
+
else
|
139
|
+
da = [] # temporary array of arrays of dates
|
140
|
+
ss.split(' ').each do |vaguedate|
|
141
|
+
da << str_to_date_array(vaguedate)
|
142
|
+
end
|
143
|
+
da.flatten!
|
144
|
+
if da.include?(nil)
|
145
|
+
@date1 = @date2 = nil
|
146
|
+
else
|
147
|
+
da.sort!
|
148
|
+
@date1, @date2 = da.first, da.last
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# Take a string representation of a single date (which may be incomplete,
|
154
|
+
# e.g year only or year/month only) and return an array of two dates,
|
155
|
+
# being the earliest and latest that fit the partial date.
|
156
|
+
def str_to_date_array(ds)
|
157
|
+
parts = date_string_parts(ds)
|
158
|
+
return if parts.nil? || OKYEARS.exclude?(parts[0])
|
159
|
+
|
160
|
+
case parts.length
|
161
|
+
when 1 # just a year
|
162
|
+
j1 = Date.new(parts[0], 1, 1).jd
|
163
|
+
j2 = Date.new(parts[0], 12, 31).jd
|
164
|
+
when 2 # year and month
|
165
|
+
j1 = Date.new(parts[0], parts[1], 1).jd
|
166
|
+
j2 = Date.new(parts[0], parts[1], -1).jd
|
167
|
+
when 3 # full date
|
168
|
+
j1 = j2 = Date.new(parts[0], parts[1], parts[2]).jd
|
169
|
+
end
|
170
|
+
|
171
|
+
[j1.jd_to_datetime, j2.jd_to_datetime]
|
172
|
+
rescue
|
173
|
+
nil
|
174
|
+
end
|
175
|
+
|
176
|
+
# Take a string representation of a single date (which may be incomplete,
|
177
|
+
# e.g year only or year/month only) and return an array of 1..3 integers
|
178
|
+
# representing the year, month and day
|
179
|
+
def date_string_parts(ds)
|
180
|
+
if ds =~ /([\/\.\-])/ # find a slash or dot or hyphen
|
181
|
+
delimiter = $1
|
182
|
+
result = ds.split(delimiter)
|
183
|
+
elsif ds.length == 8 # ddmmyyyy
|
184
|
+
result = [ds[0..1], ds[2..3], ds[4..7]]
|
185
|
+
elsif ds.length == 6 # mmyyyy
|
186
|
+
result = [ds[0..1], ds[2..5]]
|
187
|
+
elsif ds.length == 4 # yyyy
|
188
|
+
result = [ds]
|
189
|
+
else
|
190
|
+
result = []
|
191
|
+
end
|
192
|
+
return nil unless (1..3) === result.length
|
193
|
+
result.reverse! unless delimiter == '-' # change to YMD if not ISO format
|
194
|
+
result.collect(&:to_i)
|
195
|
+
end
|
196
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Fixnum
|
2
|
+
# Gets binomial coefficients:
|
3
|
+
#
|
4
|
+
# 4.choose(2) #=> 6
|
5
|
+
#
|
6
|
+
def choose(k)
|
7
|
+
fail(ArgumentError, "cannot choose #{k} from #{self}") unless (0..self) === k
|
8
|
+
self.factorial / (k.factorial * (self - k).factorial)
|
9
|
+
end
|
10
|
+
|
11
|
+
def factorial
|
12
|
+
fail("cannot calculate #{self}.factorial") unless self >= 0 # limited implementation
|
13
|
+
self.zero? ? 1 : (1..self).inject { |product, i| product * i }
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'ndr_support/ourdate'
|
2
|
+
|
3
|
+
# Extend Fixnum for use in our Daterange class
|
4
|
+
class Fixnum
|
5
|
+
# Julian date number to Ruby Date
|
6
|
+
def jd_to_date
|
7
|
+
Date.jd(self)
|
8
|
+
end
|
9
|
+
|
10
|
+
def jd_to_datetime
|
11
|
+
date = jd_to_date
|
12
|
+
Ourdate.build_datetime(date.year, date.month, date.day)
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
class Hash
|
2
|
+
# Special intersection method allowing us to intersect a hash with
|
3
|
+
# an array of keys. Matches string and symbol keys (like stringify_keys)
|
4
|
+
#
|
5
|
+
# For example
|
6
|
+
# {:a => 1, :b => :two, 'c' => '3'} & [:a, :c]
|
7
|
+
# => {:a => 1, 'c' => '3'}
|
8
|
+
def &(*keys)
|
9
|
+
h = {}
|
10
|
+
each { |k, v| h[k] = v if keys.flatten.map(&:to_s).include?(k.to_s) }
|
11
|
+
h
|
12
|
+
end
|
13
|
+
|
14
|
+
# This method allows us to walk the path of nested hashes to reference a value
|
15
|
+
#
|
16
|
+
# For example,
|
17
|
+
# my_hash = { 'one' => '1', 'two' => { 'twopointone' => '2.1', 'twopointtwo' => '2.2' } }
|
18
|
+
# my_hash['one'] becomes my_hash.value_by_path('one')
|
19
|
+
# my_hash['two']['twopointone'] becomes my_hash.value_by_path('two', 'twopointone')
|
20
|
+
#
|
21
|
+
def value_by_path(first_key, *descendant_keys)
|
22
|
+
result = self[first_key]
|
23
|
+
descendant_keys.each do |key|
|
24
|
+
result = result[key]
|
25
|
+
end
|
26
|
+
result
|
27
|
+
end
|
28
|
+
|
29
|
+
# This method merges this hash with another, but also merges the :rawtext
|
30
|
+
# (rather than replacing the current hashes rawtext with the second).
|
31
|
+
# Additionally it can raise a RuntimeError to prevent the second hash
|
32
|
+
# overwriting the value for a key from the first.
|
33
|
+
def rawtext_merge(hash2, prevent_overwrite = true)
|
34
|
+
hash1_rawtext = self[:rawtext] || {}
|
35
|
+
hash2_rawtext = hash2[:rawtext] || {}
|
36
|
+
|
37
|
+
if prevent_overwrite
|
38
|
+
non_unique_rawtext_keys = hash1_rawtext.keys & hash2_rawtext.keys
|
39
|
+
unless non_unique_rawtext_keys.empty?
|
40
|
+
fail("Non-unique rawtext keys: #{non_unique_rawtext_keys.inspect}")
|
41
|
+
end
|
42
|
+
non_unique_non_rawtext_keys = (keys & hash2.keys) - [:rawtext]
|
43
|
+
unless non_unique_non_rawtext_keys.empty?
|
44
|
+
fail("Non-unique non-rawtext keys: #{non_unique_non_rawtext_keys.inspect}")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
merge(hash2).merge(
|
49
|
+
:rawtext => hash1_rawtext.merge(hash2_rawtext)
|
50
|
+
)
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class Integer
|
2
|
+
# Rounds up to _p_ digits. For graphs. Josh Pencheon 22/08/2007
|
3
|
+
def round_up_to(p)
|
4
|
+
return nil if p > self.to_s.length || p < 0
|
5
|
+
p = p.to_i
|
6
|
+
s = self.to_s.split('')
|
7
|
+
d = s[0..(p - 1)]
|
8
|
+
d[p - 1] = s[p - 1].to_i + 1
|
9
|
+
s[p..-1].each_with_index { |_v, i| d[i + p] = '0' }
|
10
|
+
d.join.to_i
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# Extend Nilclass to avoid nil.xxx errors when empty data returned from database
|
2
|
+
class NilClass
|
3
|
+
def to_date
|
4
|
+
nil
|
5
|
+
end
|
6
|
+
|
7
|
+
def titleize
|
8
|
+
nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def surnameize
|
12
|
+
nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def postcodeize(*)
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def upcase
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
|
23
|
+
def clean(*)
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
|
27
|
+
def squash
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def gsub(*)
|
32
|
+
''
|
33
|
+
end
|
34
|
+
|
35
|
+
def strip
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'active_support/time'
|
3
|
+
require 'ndr_support/date_and_time_extensions'
|
4
|
+
require 'ndr_support/daterange'
|
5
|
+
|
6
|
+
# Convert a string into a single date
|
7
|
+
# (helped by String.thedate)
|
8
|
+
class Ourdate
|
9
|
+
attr_reader :thedate
|
10
|
+
|
11
|
+
# We need a daylight saving time safe was of defining the date today.
|
12
|
+
def self.today
|
13
|
+
current_time = Time.now
|
14
|
+
#--
|
15
|
+
# TODO: Use Ourdate.build_datetime everywhere below:
|
16
|
+
#++
|
17
|
+
if ActiveRecord::Base.default_timezone == :local
|
18
|
+
build_datetime(current_time.year, current_time.month, current_time.day)
|
19
|
+
else
|
20
|
+
#--
|
21
|
+
# Only supports fake GMT time -- needs improvement
|
22
|
+
# Maybe use Time.zone.local or Time.local_time(year, month, day)
|
23
|
+
#++
|
24
|
+
Time.gm(current_time.year, current_time.month, current_time.day, 0, 0, 0, 0).to_datetime
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Construct a daylight saving time safe datetime, with arguments
|
29
|
+
#--
|
30
|
+
# FIXME: Note that the arguments should be numbers, not strings -- it works
|
31
|
+
# with strings arguments only after the 1970 epoch; before, it returns nil.
|
32
|
+
#++
|
33
|
+
def self.build_datetime(year, month = 1, day = 1, hour = 0, min = 0, sec = 0, usec = 0)
|
34
|
+
if ActiveRecord::Base.default_timezone == :local
|
35
|
+
# Time.local_time(year, month, day, hour, min, sec, usec).to_datetime
|
36
|
+
# Behave like oracle_adapter.rb
|
37
|
+
seconds = sec + Rational(usec, 10**6)
|
38
|
+
time_array = [year, month, day, hour, min, seconds]
|
39
|
+
begin
|
40
|
+
#--
|
41
|
+
# TODO: Fails unit tests unless we .to_datetime here
|
42
|
+
# but the risk is we lose the usec component unnecesssarily.
|
43
|
+
# Investigate removing .to_datetime below.
|
44
|
+
#++
|
45
|
+
Time.send(ActiveRecord::Base.default_timezone, *time_array).to_datetime
|
46
|
+
rescue
|
47
|
+
zone_offset = ActiveRecord::Base.default_timezone == :local ? DateTime.now.offset : 0
|
48
|
+
# Append zero calendar reform start to account for dates skipped by calendar reform
|
49
|
+
DateTime.new(*time_array[0..5] << zone_offset << 0) rescue nil
|
50
|
+
end
|
51
|
+
else
|
52
|
+
# Only supports fake GMT time -- needs improvement
|
53
|
+
# Maybe use Time.zone.local or Time.local_time(year, month, day)
|
54
|
+
Time.utc(year, month, day, hour, min, sec, usec).to_datetime
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def initialize(x = nil)
|
59
|
+
if x.is_a?(Date)
|
60
|
+
@thedate = x
|
61
|
+
elsif x.is_a?(Time)
|
62
|
+
@thedate = x.to_datetime
|
63
|
+
elsif x.is_a?(String)
|
64
|
+
self.source = x
|
65
|
+
else
|
66
|
+
@thedate = nil
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_s
|
71
|
+
@thedate ? @thedate.to_date.to_s(:ui) : ''
|
72
|
+
end
|
73
|
+
|
74
|
+
def empty?
|
75
|
+
# An unspecified date will be empty. A valid or invalid date will not.
|
76
|
+
@thedate.nil? && @source.blank?
|
77
|
+
end
|
78
|
+
|
79
|
+
def source=(s)
|
80
|
+
dr = Daterange.new(s)
|
81
|
+
if dr.date1 == dr.date2
|
82
|
+
@thedate = dr.date1
|
83
|
+
else
|
84
|
+
@thedate = nil
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Compute date difference in years (e.g. patient age), as an integer
|
89
|
+
# For a positive result, the later date should be the first argument.
|
90
|
+
# Leap days are treated as for age calculations.
|
91
|
+
def self.date_difference_in_years(date2, date1)
|
92
|
+
(date2.strftime('%Y%m%d').sub(/0229$/, '0228').to_i -
|
93
|
+
date1.strftime('%Y%m%d').sub(/0229$/, '0228').to_i) / 10_000
|
94
|
+
end
|
95
|
+
|
96
|
+
private :source=
|
97
|
+
end
|