aemo 0.5.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/aemo/nem12.rb CHANGED
@@ -14,6 +14,9 @@ module AEMO
14
14
  # Namespace for classes and modules that handle AEMO Gem NEM12 interactions
15
15
  # @since 0.1.4
16
16
  class NEM12
17
+ CRLF = "\r\n"
18
+ CSV_SEPARATOR = ','
19
+
17
20
  @file_contents = nil
18
21
  @header = nil
19
22
  @nmi_data_details = []
@@ -26,10 +29,108 @@ module AEMO
26
29
  attr_reader :data_details, :interval_data, :interval_events
27
30
  attr_accessor :file_contents, :header, :nmi_data_details, :nmi
28
31
 
32
+ # Class methods.
33
+ class << self
34
+ # @param [String] path_to_file the path to a file
35
+ # @return [Array<AEMO::NEM12>] NEM12 object
36
+ def parse_nem12_file(path_to_file, strict: true)
37
+ parse_nem12(File.read(path_to_file), strict:)
38
+ end
39
+
40
+ # @param [String] contents the path to a file
41
+ # @param [Boolean] strict
42
+ # @return [Array<AEMO::NEM12>] An array of NEM12 objects
43
+ def parse_nem12(contents, strict: true)
44
+ file_contents = contents.tr("\r", "\n").tr("\n\n", "\n").split("\n").delete_if(&:empty?)
45
+ # nothing to further process
46
+ return [] if file_contents.empty?
47
+
48
+ unless file_contents.first.parse_csv[0] == '100'
49
+ raise ArgumentError,
50
+ 'First row should be have a RecordIndicator of 100 and be of type Header Record'
51
+ end
52
+
53
+ nem12s = []
54
+ header = AEMO::NEM12.parse_nem12_100(file_contents.first, strict:)
55
+ file_contents.each do |line|
56
+ case line[0..2].to_i
57
+ when 200
58
+ nem12s << AEMO::NEM12.new('')
59
+ nem12s.last.header = header
60
+ nem12s.last.parse_nem12_200(line, strict:)
61
+ when 300
62
+ nem12s.last.parse_nem12_300(line, strict:)
63
+ when 400
64
+ nem12s.last.parse_nem12_400(line, strict:)
65
+ # when 500
66
+ # nem12s.last.parse_nem12_500(line, strict: strict)
67
+ # when 900
68
+ # nem12s.last.parse_nem12_900(line, strict: strict)
69
+ end
70
+ end
71
+ # Return the array of NEM12 groups
72
+ nem12s
73
+ end
74
+
75
+ # Parses the header record
76
+ # @param [String] line A single line in string format
77
+ # @param [Boolean] strict
78
+ # @return [Hash] the line parsed into a hash of information
79
+ def parse_nem12_100(line, strict: true) # rubocop:disable Naming/VariableNumber
80
+ csv = line.parse_csv
81
+
82
+ raise ArgumentError, 'RecordIndicator is not 100' if csv[0] != '100'
83
+ raise ArgumentError, 'VersionHeader is not NEM12' if csv[1] != 'NEM12'
84
+
85
+ raise ArgumentError, 'Time is not valid' if strict && !AEMO::Time.valid_timestamp12?(csv[2])
86
+
87
+ raise ArgumentError, 'FromParticipant is not valid' if csv[3].match(/.{1,10}/).nil?
88
+ raise ArgumentError, 'ToParticipant is not valid' if csv[4].match(/.{1,10}/).nil?
89
+
90
+ datetime = strict && AEMO::Time.valid_timestamp12?(csv[2]) ? AEMO::Time.parse_timestamp12(csv[2]) : nil
91
+
92
+ {
93
+ record_indicator: csv[0].to_i,
94
+ version_header: csv[1],
95
+ datetime:,
96
+ from_participant: csv[3],
97
+ to_participant: csv[4]
98
+ }
99
+ end
100
+
101
+ # Default NEM12 100 row record.
102
+ #
103
+ # @return [String]
104
+ def default_nem12_100 # rubocop:disable Naming/VariableNumber
105
+ timestamp = AEMO::Time.format_timestamp12(::Time.now)
106
+
107
+ "100,NEM12,#{timestamp},ENOSI,ENOSI#{CRLF}"
108
+ end
109
+
110
+ # Default NEM12 100 row record.
111
+ #
112
+ # @return [String]
113
+ def default_nem12_900 # rubocop:disable Naming/VariableNumber
114
+ "900#{CRLF}"
115
+ end
116
+
117
+ # For a list of nem12s, turn into a single NEM12 CSV string with default header row.
118
+ #
119
+ # @param [Array<AEMO::NEM12>] nem12s
120
+ # @return [String]
121
+ def to_nem12_csv(nem12s:)
122
+ [
123
+ default_nem12_100,
124
+ nem12s.map(&:to_nem12_200_csv),
125
+ default_nem12_900
126
+ ].flatten.join
127
+ end
128
+ end
129
+
29
130
  # Initialize a NEM12 file
30
131
  # @param [string] nmi
31
132
  # @param [Hash] options
32
- def initialize(nmi, options = {})
133
+ def initialize(nmi, options: {})
33
134
  @nmi = AEMO::NMI.new(nmi) unless nmi.empty?
34
135
  @data_details = []
35
136
  @interval_data = []
@@ -41,49 +142,42 @@ module AEMO
41
142
 
42
143
  # Returns the NMI Identifier or nil
43
144
  def nmi_identifier
44
- @nmi.nil? ? nil : @nmi.nmi
45
- end
46
-
47
- # Parses the header record
48
- # @param [String] line A single line in string format
49
- # @param [Hash] options
50
- # @return [Hash] the line parsed into a hash of information
51
- def self.parse_nem12_100(line, options = {})
52
- csv = line.parse_csv
53
-
54
- raise ArgumentError, 'RecordIndicator is not 100' if csv[0] != '100'
55
- raise ArgumentError, 'VersionHeader is not NEM12' if csv[1] != 'NEM12'
56
- raise ArgumentError, 'Time is not valid' if options[:strict] && (csv[2].match(/\d{12}/).nil? || csv[2] != Time.parse("#{csv[2]}00").strftime('%Y%m%d%H%M'))
57
- raise ArgumentError, 'FromParticipant is not valid' if csv[3].match(/.{1,10}/).nil?
58
- raise ArgumentError, 'ToParticipant is not valid' if csv[4].match(/.{1,10}/).nil?
59
-
60
- {
61
- record_indicator: csv[0].to_i,
62
- version_header: csv[1],
63
- datetime: Time.parse("#{csv[2]}+1000"),
64
- from_participant: csv[3],
65
- to_participant: csv[4]
66
- }
145
+ @nmi&.nmi
67
146
  end
68
147
 
69
148
  # Parses the NMI Data Details
70
149
  # @param [String] line A single line in string format
71
- # @param [Hash] options
150
+ # @param [Boolean] strict
72
151
  # @return [Hash] the line parsed into a hash of information
73
- def parse_nem12_200(line, options = {})
152
+ def parse_nem12_200(line, strict: true) # rubocop:disable Naming/VariableNumber
74
153
  csv = line.parse_csv
75
154
 
76
155
  raise ArgumentError, 'RecordIndicator is not 200' if csv[0] != '200'
77
156
  raise ArgumentError, 'NMI is not valid' unless AEMO::NMI.valid_nmi?(csv[1])
78
- raise ArgumentError, 'NMIConfiguration is not valid' if options[:strict] && (csv[2].nil? || csv[2].match(/.{1,240}/).nil?)
157
+
158
+ if strict && (csv[2].nil? || csv[2].match(/.{1,240}/).nil?)
159
+ raise ArgumentError,
160
+ 'NMIConfiguration is not valid'
161
+ end
162
+
79
163
  raise ArgumentError, 'RegisterID is not valid' if !csv[3].nil? && csv[3].match(/.{1,10}/).nil?
80
164
  raise ArgumentError, 'NMISuffix is not valid' if csv[4].nil? || csv[4].match(/[A-HJ-NP-Z][1-9A-HJ-NP-Z]/).nil?
81
- raise ArgumentError, 'MDMDataStreamIdentifier is not valid' if !csv[5].nil? && !csv[5].empty? && !csv[5].match(/^\s*$/) && csv[5].match(/[A-Z0-9]{2}/).nil?
82
- raise ArgumentError, 'MeterSerialNumber is not valid' if !csv[6].nil? && !csv[6].empty? && !csv[6].match(/^\s*$/) && csv[6].match(/[A-Z0-9]{2}/).nil?
165
+
166
+ if !csv[5].nil? && !csv[5].empty? && !csv[5].match(/^\s*$/) && csv[5].match(/[A-Z0-9]{2}/).nil?
167
+ raise ArgumentError,
168
+ 'MDMDataStreamIdentifier is not valid'
169
+ end
170
+
171
+ if !csv[6].nil? && !csv[6].empty? && !csv[6].match(/^\s*$/) && csv[6].match(/[A-Z0-9]{2}/).nil?
172
+ raise ArgumentError,
173
+ 'MeterSerialNumber is not valid'
174
+ end
175
+
83
176
  raise ArgumentError, 'UOM is not valid' if csv[7].nil? || csv[7].upcase.match(/[A-Z0-9]{2}/).nil?
84
177
  raise ArgumentError, 'UOM is not valid' unless UOM.keys.map(&:upcase).include?(csv[7].upcase)
85
178
  raise ArgumentError, 'IntervalLength is not valid' unless %w[1 5 10 15 30].include?(csv[8])
86
- # raise ArgumentError, 'NextScheduledReadDate is not valid' if csv[9].match(/\d{8}/).nil? || csv[9] != Time.parse('#{csv[9]}').strftime('%Y%m%d')
179
+
180
+ # raise ArgumentError, 'NextScheduledReadDate is not valid' if !AEMO::Time.valid_timestamp8?(csv[9])
87
181
 
88
182
  @nmi = AEMO::NMI.new(csv[1])
89
183
 
@@ -103,43 +197,64 @@ module AEMO
103
197
  end
104
198
 
105
199
  # @param [String] line A single line in string format
106
- # @param [Hash] options
200
+ # @param [Boolean] strict
107
201
  # @return [Array of hashes] the line parsed into a hash of information
108
- def parse_nem12_300(line, options = {})
202
+ def parse_nem12_300(line, strict: true) # rubocop:disable Naming/VariableNumber
109
203
  csv = line.parse_csv
110
- raise TypeError, 'Expected NMI Data Details to exist with IntervalLength specified' if @data_details.last.nil? || @data_details.last[:interval_length].nil?
204
+
205
+ if @data_details.last.nil? || @data_details.last[:interval_length].nil?
206
+ raise TypeError,
207
+ 'Expected NMI Data Details to exist with IntervalLength specified'
208
+ end
111
209
 
112
210
  # ref: AEMO's MDFF Spec NEM12 and NEM13 v1.01 (2014-05-14)
113
211
  record_fixed_fields = %w[RecordIndicator IntervalDate QualityMethod ReasonCode ReasonDescription UpdateDatetime MSATSLoadDateTime]
114
212
  number_of_intervals = 1440 / @data_details.last[:interval_length]
213
+
115
214
  raise TypeError, 'Invalid record length' if csv.length != record_fixed_fields.length + number_of_intervals
116
215
 
117
216
  intervals_offset = number_of_intervals + 2
118
217
 
119
218
  raise ArgumentError, 'RecordIndicator is not 300' if csv[0] != '300'
120
- raise ArgumentError, 'IntervalDate is not valid' if csv[1].match(/\d{8}/).nil? || csv[1] != Time.parse(csv[1].to_s).strftime('%Y%m%d')
219
+ raise ArgumentError, 'IntervalDate is not valid' unless AEMO::Time.valid_timestamp8?(csv[1])
220
+
121
221
  (2..(number_of_intervals + 1)).each do |i|
122
222
  raise ArgumentError, "Interval number #{i - 1} is not valid" if csv[i].nil? || csv[i].match(/\d+(\.\d+)?/).nil?
123
223
  end
124
- raise ArgumentError, 'QualityMethod is not valid' unless csv[intervals_offset + 0].class == String
224
+
225
+ raise ArgumentError, 'QualityMethod is not valid' unless csv[intervals_offset + 0].instance_of?(String)
125
226
  raise ArgumentError, 'QualityMethod does not have valid length' unless [1, 3].include?(csv[intervals_offset + 0].length)
126
- raise ArgumentError, 'QualityMethod does not have valid QualityFlag' unless QUALITY_FLAGS.keys.include?(csv[intervals_offset + 0][0])
227
+
228
+ unless QUALITY_FLAGS.keys.include?(csv[intervals_offset + 0][0])
229
+ raise ArgumentError,
230
+ 'QualityMethod does not have valid QualityFlag'
231
+ end
232
+
127
233
  unless %w[A N V].include?(csv[intervals_offset + 0][0])
128
234
  raise ArgumentError, 'QualityMethod does not have valid length' unless csv[intervals_offset + 0].length == 3
129
- raise ArgumentError, 'QualityMethod does not have valid MethodFlag' unless METHOD_FLAGS.keys.include?(csv[intervals_offset + 0][1..2].to_i)
130
- end
131
- unless %w[A N E].include?(csv[intervals_offset + 0][0])
132
- raise ArgumentError, 'ReasonCode is not valid' unless REASON_CODES.keys.include?(csv[intervals_offset + 1].to_i)
235
+
236
+ unless METHOD_FLAGS.keys.include?(csv[intervals_offset + 0][1..2].to_i)
237
+ raise ArgumentError,
238
+ 'QualityMethod does not have valid MethodFlag'
239
+ end
133
240
  end
134
- if !csv[intervals_offset + 1].nil? && csv[intervals_offset + 1].to_i.zero?
135
- raise ArgumentError, 'ReasonDescription is not valid' unless csv[intervals_offset + 2].class == String && !csv[intervals_offset + 2].empty?
241
+
242
+ raise ArgumentError, 'ReasonCode is not valid' if !%w[A N E].include?(csv[intervals_offset + 0][0]) && !REASON_CODES.keys.include?(csv[intervals_offset + 1].to_i)
243
+
244
+ if !csv[intervals_offset + 1].nil? && csv[intervals_offset + 1].to_i.zero? && !(csv[intervals_offset + 2].instance_of?(String) && !csv[intervals_offset + 2].empty?)
245
+ raise ArgumentError,
246
+ 'ReasonDescription is not valid'
136
247
  end
137
- if options[:strict]
138
- if csv[intervals_offset + 3].match(/\d{14}/).nil? || csv[intervals_offset + 3] != Time.parse(csv[intervals_offset + 3].to_s).strftime('%Y%m%d%H%M%S')
139
- raise ArgumentError, 'UpdateDateTime is not valid'
248
+
249
+ if strict
250
+ unless AEMO::Time.valid_timestamp14?(csv[intervals_offset + 3])
251
+ raise ArgumentError,
252
+ 'UpdateDateTime is not valid'
140
253
  end
141
- if !csv[intervals_offset + 4].blank? && csv[intervals_offset + 4].match(/\d{14}/).nil? || !csv[intervals_offset + 4].blank? && csv[intervals_offset + 4] != Time.parse(csv[intervals_offset + 4].to_s).strftime('%Y%m%d%H%M%S')
142
- raise ArgumentError, 'MSATSLoadDateTime is not valid'
254
+
255
+ if !csv[intervals_offset + 4].blank? && !AEMO::Time.valid_timestamp14?(csv[intervals_offset + 4])
256
+ raise ArgumentError,
257
+ 'MSATSLoadDateTime is not valid'
143
258
  end
144
259
  end
145
260
 
@@ -159,18 +274,18 @@ module AEMO
159
274
  updated_at = nil
160
275
  msats_load_at = nil
161
276
 
162
- if options[:strict]
163
- updated_at = Time.parse(csv[intervals_offset + 3]) unless csv[intervals_offset + 3].blank?
164
- msats_load_at = Time.parse(csv[intervals_offset + 4]) unless csv[intervals_offset + 4].blank?
277
+ if strict
278
+ updated_at = AEMO::Time.parse_timestamp14(csv[intervals_offset + 3]) unless csv[intervals_offset + 3].blank?
279
+ msats_load_at = AEMO::Time.parse_timestamp14(csv[intervals_offset + 4]) unless csv[intervals_offset + 4].blank?
165
280
  end
166
281
 
167
282
  base_interval = {
168
283
  data_details: @data_details.last,
169
- datetime: Time.parse("#{csv[1]}000000+1000"),
284
+ datetime: AEMO::Time.parse_timestamp8(csv[1]),
170
285
  value: nil,
171
- flag: flag,
172
- updated_at: updated_at,
173
- msats_load_at: msats_load_at
286
+ flag:,
287
+ updated_at:,
288
+ msats_load_at:
174
289
  }
175
290
 
176
291
  intervals = []
@@ -185,14 +300,19 @@ module AEMO
185
300
  end
186
301
 
187
302
  # @param [String] line A single line in string format
188
- # @param [Hash] options
303
+ # @param [Boolean] strict
189
304
  # @return [Hash] the line parsed into a hash of information
190
- def parse_nem12_400(line, options = {})
305
+ def parse_nem12_400(line, strict: true) # rubocop:disable Lint/UnusedMethodArgument,Naming/VariableNumber
191
306
  csv = line.parse_csv
192
307
  raise ArgumentError, 'RecordIndicator is not 400' if csv[0] != '400'
193
308
  raise ArgumentError, 'StartInterval is not valid' if csv[1].nil? || csv[1].match(/^\d+$/).nil?
194
309
  raise ArgumentError, 'EndInterval is not valid' if csv[2].nil? || csv[2].match(/^\d+$/).nil?
195
- raise ArgumentError, 'QualityMethod is not valid' if csv[3].nil? || csv[3].match(/^([AN]|([AEFNSV]\d{2}))$/).nil?
310
+
311
+ if csv[3].nil? || csv[3].match(/^([AN]|([AEFNSV]\d{2}))$/).nil?
312
+ raise ArgumentError,
313
+ 'QualityMethod is not valid'
314
+ end
315
+
196
316
  # raise ArgumentError, 'ReasonCode is not valid' if (csv[4].nil? && csv[3].match(/^ANE/)) || csv[4].match(/^\d{3}?$/) || csv[3].match(/^ANE/)
197
317
  # raise ArgumentError, 'ReasonDescription is not valid' if (csv[4].nil? && csv[3].match(/^ANE/)) || ( csv[5].match(/^$/) && csv[4].match(/^0$/) )
198
318
 
@@ -204,7 +324,8 @@ module AEMO
204
324
  interval_start_point = @interval_data.length - number_of_intervals
205
325
 
206
326
  # For each of these
207
- base_interval_event = { datetime: nil, quality_method: csv[3], reason_code: (csv[4].nil? ? nil : csv[4].to_i), reason_description: csv[5] }
327
+ base_interval_event = { datetime: nil, quality_method: csv[3], reason_code: csv[4]&.to_i,
328
+ reason_description: csv[5] }
208
329
 
209
330
  # Interval Numbers are 1-indexed
210
331
  ((csv[1].to_i)..(csv[2].to_i)).each do |i|
@@ -229,16 +350,16 @@ module AEMO
229
350
  # What even is a 500 row?
230
351
  #
231
352
  # @param [String] line A single line in string format
232
- # @param [Hash] _options
353
+ # @param [Boolean] strict
233
354
  # @return [Hash] the line parsed into a hash of information
234
- def parse_nem12_500(_line, _options = {}); end
355
+ def parse_nem12_500(_line, strict: true); end # rubocop:disable Naming/VariableNumber
235
356
 
236
357
  # 900 is the last row a NEM12 should see...
237
358
  #
238
359
  # @param [String] line A single line in string format
239
- # @param [Hash] _options
360
+ # @param [Boolean] strict
240
361
  # @return [Hash] the line parsed into a hash of information
241
- def parse_nem12_900(_line, _options = {}); end
362
+ def parse_nem12_900(_line, strict: true); end # rubocop:disable Naming/VariableNumber
242
363
 
243
364
  # Turns the flag to a string
244
365
  #
@@ -272,48 +393,133 @@ module AEMO
272
393
  def to_csv
273
394
  headers = %w[nmi suffix units datetime value flags]
274
395
  ([headers] + to_a.map do |row|
275
- row[3] = row[3].strftime('%Y%m%d%H%M%S%z')
396
+ row[3] = row[3].strftime('%Y%m%d%TH%M%S%z')
276
397
  row
277
398
  end).map do |row|
278
399
  row.join(', ')
279
400
  end.join("\n")
280
401
  end
281
402
 
282
- # @param [String] path_to_file the path to a file
283
- # @return [Array<AEMO::NEM12>] NEM12 object
284
- def self.parse_nem12_file(path_to_file, strict = true)
285
- parse_nem12(File.read(path_to_file), strict)
403
+ # Output the AEMO::NEM12 to a valid NEM12 CSV string.
404
+ #
405
+ # @return [String]
406
+ def to_nem12_csv
407
+ [
408
+ to_nem12_100_csv,
409
+ to_nem12_200_csv,
410
+ to_nem12_900_csv
411
+ ].flatten.join
286
412
  end
287
413
 
288
- # @param [String] contents the path to a file
289
- # @param [Boolean] strict
290
- # @return [Array<AEMO::NEM12>] An array of NEM12 objects
291
- def self.parse_nem12(contents, strict = true)
292
- file_contents = contents.tr("\r", "\n").tr("\n\n", "\n").split("\n").delete_if(&:empty?)
293
- # nothing to further process
294
- return [] if file_contents.empty?
295
-
296
- raise ArgumentError, 'First row should be have a RecordIndicator of 100 and be of type Header Record' unless file_contents.first.parse_csv[0] == '100'
297
-
298
- nem12s = []
299
- AEMO::NEM12.parse_nem12_100(file_contents.first, strict: strict)
300
- file_contents.each do |line|
301
- case line[0..2].to_i
302
- when 200
303
- nem12s << AEMO::NEM12.new('')
304
- nem12s.last.parse_nem12_200(line, strict: strict)
305
- when 300
306
- nem12s.last.parse_nem12_300(line, strict: strict)
307
- when 400
308
- nem12s.last.parse_nem12_400(line, strict: strict)
309
- # when 500
310
- # nem12s.last.parse_nem12_500(line, strict: strict)
311
- # when 900
312
- # nem12s.last.parse_nem12_900(line, strict: strict)
414
+ # Output the AEMO::NEM12 to a valid NEM12 100 row CSV string.
415
+ #
416
+ # @return [String]
417
+ def to_nem12_100_csv
418
+ return self.class.default_nem12_100 if header.nil?
419
+
420
+ [
421
+ header[:record_indicator],
422
+ header[:version_header],
423
+ AEMO::Time.format_timestamp12(header[:datetime]),
424
+ header[:from_participant],
425
+ header[:to_participant]
426
+ ].join(CSV_SEPARATOR) + CRLF
427
+ end
428
+
429
+ # Output the AEMO::NEM12 to a valid NEM12 200 row CSV string.
430
+ #
431
+ # @return [String]
432
+ def to_nem12_200_csv
433
+ return nil if data_details.length != 1
434
+
435
+ data_detail = data_details.first
436
+
437
+ [
438
+ [
439
+ data_detail[:record_indicator],
440
+ data_detail[:nmi],
441
+ data_detail[:nmi_configuration],
442
+ data_detail[:register_id],
443
+ data_detail[:nmi_suffix],
444
+ data_detail[:mdm_data_streaming_identifier],
445
+ data_detail[:meter_serial_number],
446
+ data_detail[:uom],
447
+ data_detail[:interval_length],
448
+ data_detail[:next_scheduled_read_date] # NOTE: this is not turned into a timestamp.
449
+ ].join(CSV_SEPARATOR),
450
+ to_nem12_300_csv
451
+ ].flatten.join(CRLF)
452
+ end
453
+
454
+ # Output the AEMO::NEM12 to a valid NEM12 300 row CSV string.
455
+ #
456
+ # @return [String]
457
+ def to_nem12_300_csv
458
+ lines = []
459
+
460
+ daily_datas = interval_data.group_by do |x|
461
+ AEMO::Time.format_timestamp8(x[:datetime] - 1.second)
462
+ end
463
+ daily_datas.keys.sort.each do |key|
464
+ daily_data = daily_datas[key].sort_by { |x| x[:datetime] }
465
+ has_flags = daily_data.map { |x| x[:flag]&.any? }.uniq.include?(true)
466
+
467
+ lines << [
468
+ '300',
469
+ key,
470
+ daily_data.map { |x| x[:value] },
471
+ has_flags ? 'V' : 'A',
472
+ '',
473
+ '',
474
+ daily_data.first[:updated_at] ? AEMO::Time.format_timestamp14(daily_data.first[:updated_at]) : nil,
475
+ daily_data.first[:msats_load_at] ? AEMO::Time.format_timestamp14(daily_data.first[:msats_load_at]) : nil
476
+ ].flatten.join(CSV_SEPARATOR)
477
+
478
+ next unless has_flags
479
+
480
+ lines << to_nem12_400_csv(daily_data:)
481
+ end
482
+
483
+ lines.join(CRLF) + CRLF
484
+ end
485
+
486
+ # Output the AEMO::NEM12 to a valid NEM12 400 row CSV string.
487
+ #
488
+ # @param [Array<Hash>] daily_data
489
+ # @return [String]
490
+ def to_nem12_400_csv(daily_data:)
491
+ daily_data.sort_by! { |x| x[:datetime] }
492
+
493
+ nem12_400_rows = []
494
+
495
+ daily_data.each_with_index do |x, i|
496
+ nem12_400_rows << { flag: x[:flag], start_index: i + 1, finish_index: i + 1 } if nem12_400_rows.empty?
497
+
498
+ if nem12_400_rows.last[:flag] == x[:flag]
499
+ nem12_400_rows.last[:finish_index] = i + 1
500
+ next
313
501
  end
502
+
503
+ nem12_400_rows << { flag: x[:flag], start_index: i + 1, finish_index: i + 1 }
314
504
  end
315
- # Return the array of NEM12 groups
316
- nem12s
505
+
506
+ nem12_400_rows.map do |row|
507
+ [
508
+ '400',
509
+ row[:start_index],
510
+ row[:finish_index],
511
+ row[:flag].nil? ? 'A' : "#{row[:flag][:quality_flag]}#{row[:flag][:method_flag]}",
512
+ row[:flag].nil? ? '' : row[:flag][:reason_code],
513
+ ''
514
+ ].join(CSV_SEPARATOR)
515
+ end.join(CRLF)
516
+ end
517
+
518
+ # Output the AEMO::NEM12 to a valid NEM12 900 row CSV string.
519
+ #
520
+ # @return [String]
521
+ def to_nem12_900_csv
522
+ self.class.default_nem12_900
317
523
  end
318
524
  end
319
525
  end
data/lib/aemo/nem13.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AEMO
4
- class NEM13
5
- end
4
+ class NEM13; end # rubocop:disable Lint/EmptyClass
6
5
  end
@@ -155,7 +155,7 @@ module AEMO
155
155
  type: 'electricity',
156
156
  includes: [
157
157
  /^(SAAA[A-HJ-NP-VX-Z\d][A-HJ-NP-Z\d]{5})$/,
158
- /^(SASMPL[\d]{4})$/,
158
+ /^(SASMPL\d{4})$/,
159
159
  /^(200[12]\d{6})$/
160
160
  ],
161
161
  excludes: []
@@ -405,8 +405,8 @@ module AEMO
405
405
  # Enumerable support
406
406
  #
407
407
  # @return [Enumerator]
408
- def each(&block)
409
- all.each(&block)
408
+ def each(&)
409
+ all.each(&)
410
410
  end
411
411
 
412
412
  # Finds the Allocation that encompasses a given NMI
@@ -444,7 +444,7 @@ module AEMO
444
444
  @friendly_title = opts.fetch(:friendly_title, title)
445
445
  @exclude_nmi_patterns = opts.fetch(:excludes, [])
446
446
  @include_nmi_patterns = opts.fetch(:includes, [])
447
- @region = AEMO::Region.new(opts.fetch(:region)) if opts.dig(:region)
447
+ @region = AEMO::Region.new(opts.fetch(:region)) if opts[:region]
448
448
  end
449
449
 
450
450
  private
@@ -456,9 +456,8 @@ module AEMO
456
456
  # @return [Symbol]
457
457
  def parse_allocation_type(type)
458
458
  type_sym = type.to_sym
459
- unless SUPPORTED_TYPES.include?(type_sym)
460
- raise AEMO::InvalidNMIAllocationType
461
- end
459
+ raise AEMO::InvalidNMIAllocationType unless SUPPORTED_TYPES.include?(type_sym)
460
+
462
461
  type_sym
463
462
  rescue NoMethodError
464
463
  raise AEMO::InvalidNMIAllocationType