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