aemo 0.5.0 → 0.6.0

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