ndr_support 3.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +14 -0
  3. data/.rubocop.yml +27 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +22 -0
  6. data/CODE_OF_CONDUCT.md +13 -0
  7. data/Gemfile +4 -0
  8. data/Guardfile +16 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +91 -0
  11. data/Rakefile +12 -0
  12. data/code_safety.yml +258 -0
  13. data/gemfiles/Gemfile.rails32 +6 -0
  14. data/gemfiles/Gemfile.rails32.lock +108 -0
  15. data/gemfiles/Gemfile.rails41 +6 -0
  16. data/gemfiles/Gemfile.rails41.lock +111 -0
  17. data/gemfiles/Gemfile.rails42 +6 -0
  18. data/gemfiles/Gemfile.rails42.lock +111 -0
  19. data/lib/ndr_support.rb +21 -0
  20. data/lib/ndr_support/array.rb +52 -0
  21. data/lib/ndr_support/concerns/working_days.rb +94 -0
  22. data/lib/ndr_support/date_and_time_extensions.rb +103 -0
  23. data/lib/ndr_support/daterange.rb +196 -0
  24. data/lib/ndr_support/fixnum/calculations.rb +15 -0
  25. data/lib/ndr_support/fixnum/julian_date_conversions.rb +14 -0
  26. data/lib/ndr_support/hash.rb +52 -0
  27. data/lib/ndr_support/integer.rb +12 -0
  28. data/lib/ndr_support/nil.rb +38 -0
  29. data/lib/ndr_support/ourdate.rb +97 -0
  30. data/lib/ndr_support/ourtime.rb +51 -0
  31. data/lib/ndr_support/regexp_range.rb +65 -0
  32. data/lib/ndr_support/safe_file.rb +185 -0
  33. data/lib/ndr_support/safe_path.rb +268 -0
  34. data/lib/ndr_support/string/cleaning.rb +136 -0
  35. data/lib/ndr_support/string/conversions.rb +137 -0
  36. data/lib/ndr_support/tasks.rb +1 -0
  37. data/lib/ndr_support/time/conversions.rb +13 -0
  38. data/lib/ndr_support/utf8_encoding.rb +72 -0
  39. data/lib/ndr_support/utf8_encoding/control_characters.rb +53 -0
  40. data/lib/ndr_support/utf8_encoding/force_binary.rb +44 -0
  41. data/lib/ndr_support/utf8_encoding/object_support.rb +31 -0
  42. data/lib/ndr_support/version.rb +5 -0
  43. data/lib/ndr_support/yaml/serialization_migration.rb +65 -0
  44. data/lib/tasks/audit_code.rake +423 -0
  45. data/ndr_support.gemspec +39 -0
  46. data/test/array_test.rb +20 -0
  47. data/test/concerns/working_days_test.rb +122 -0
  48. data/test/daterange_test.rb +194 -0
  49. data/test/fixnum/calculations_test.rb +28 -0
  50. data/test/hash_test.rb +84 -0
  51. data/test/integer_test.rb +14 -0
  52. data/test/nil_test.rb +40 -0
  53. data/test/ourdate_test.rb +27 -0
  54. data/test/ourtime_test.rb +27 -0
  55. data/test/regexp_range_test.rb +135 -0
  56. data/test/resources/filesystem_paths.yml +37 -0
  57. data/test/safe_file_test.rb +597 -0
  58. data/test/safe_path_test.rb +168 -0
  59. data/test/string/cleaning_test.rb +176 -0
  60. data/test/string/conversions_test.rb +353 -0
  61. data/test/test_helper.rb +41 -0
  62. data/test/time/conversions_test.rb +15 -0
  63. data/test/utf8_encoding/control_characters_test.rb +84 -0
  64. data/test/utf8_encoding/force_binary_test.rb +64 -0
  65. data/test/utf8_encoding_test.rb +170 -0
  66. data/test/yaml/serialization_test.rb +145 -0
  67. 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