ndr_support 3.1.1
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.
- 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
|