ruby-grafana-reporter 0.1.7 → 0.2.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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +166 -339
  3. data/bin/ruby-grafana-reporter +5 -4
  4. data/lib/VERSION.rb +5 -3
  5. data/lib/grafana/abstract_panel_query.rb +22 -20
  6. data/lib/grafana/abstract_query.rb +132 -127
  7. data/lib/grafana/abstract_sql_query.rb +51 -42
  8. data/lib/grafana/dashboard.rb +77 -66
  9. data/lib/grafana/errors.rb +66 -61
  10. data/lib/grafana/grafana.rb +130 -131
  11. data/lib/grafana/panel.rb +41 -39
  12. data/lib/grafana/panel_image_query.rb +52 -49
  13. data/lib/grafana/variable.rb +217 -259
  14. data/lib/grafana_reporter/abstract_report.rb +112 -109
  15. data/lib/grafana_reporter/application/application.rb +404 -229
  16. data/lib/grafana_reporter/application/errors.rb +33 -30
  17. data/lib/grafana_reporter/application/webservice.rb +231 -0
  18. data/lib/grafana_reporter/asciidoctor/alerts_table_query.rb +104 -99
  19. data/lib/grafana_reporter/asciidoctor/annotations_table_query.rb +99 -96
  20. data/lib/grafana_reporter/asciidoctor/errors.rb +40 -37
  21. data/lib/grafana_reporter/asciidoctor/extensions/alerts_table_include_processor.rb +92 -86
  22. data/lib/grafana_reporter/asciidoctor/extensions/annotations_table_include_processor.rb +91 -86
  23. data/lib/grafana_reporter/asciidoctor/extensions/panel_image_block_macro.rb +69 -67
  24. data/lib/grafana_reporter/asciidoctor/extensions/panel_image_inline_macro.rb +68 -65
  25. data/lib/grafana_reporter/asciidoctor/extensions/panel_property_inline_macro.rb +61 -58
  26. data/lib/grafana_reporter/asciidoctor/extensions/panel_query_table_include_processor.rb +78 -75
  27. data/lib/grafana_reporter/asciidoctor/extensions/panel_query_value_inline_macro.rb +73 -70
  28. data/lib/grafana_reporter/asciidoctor/extensions/processor_mixin.rb +20 -18
  29. data/lib/grafana_reporter/asciidoctor/extensions/show_environment_include_processor.rb +43 -41
  30. data/lib/grafana_reporter/asciidoctor/extensions/sql_table_include_processor.rb +70 -67
  31. data/lib/grafana_reporter/asciidoctor/extensions/sql_value_inline_macro.rb +66 -65
  32. data/lib/grafana_reporter/asciidoctor/extensions/value_as_variable_include_processor.rb +61 -57
  33. data/lib/grafana_reporter/asciidoctor/panel_first_value_query.rb +34 -32
  34. data/lib/grafana_reporter/asciidoctor/panel_image_query.rb +25 -23
  35. data/lib/grafana_reporter/asciidoctor/panel_property_query.rb +44 -43
  36. data/lib/grafana_reporter/asciidoctor/panel_table_query.rb +38 -36
  37. data/lib/grafana_reporter/asciidoctor/query_mixin.rb +310 -309
  38. data/lib/grafana_reporter/asciidoctor/report.rb +177 -159
  39. data/lib/grafana_reporter/asciidoctor/sql_first_value_query.rb +37 -34
  40. data/lib/grafana_reporter/asciidoctor/sql_table_query.rb +39 -32
  41. data/lib/grafana_reporter/configuration.rb +257 -326
  42. data/lib/grafana_reporter/errors.rb +48 -38
  43. data/lib/grafana_reporter/logger/two_way_logger.rb +58 -52
  44. data/lib/ruby-grafana-reporter.rb +29 -27
  45. metadata +10 -23
@@ -1,49 +1,52 @@
1
- module Grafana
2
- # Query, which allows to render a {Panel} as a PNG image.
3
- class PanelImageQuery < AbstractPanelQuery
4
- # Returns the URL for rendering the panel. Uses {Panel#render_url} and sets additional url parameters according {https://grafana.com/docs/grafana/latest/reference/share_panel Grafana Share Panel}.
5
- #
6
- # @see AbstractQuery#url
7
- # @return [String] string for rendering the panel
8
- def url
9
- @panel.render_url + url_parameters
10
- end
11
-
12
- # Changes the result of the request to be of type +image/png+.
13
- #
14
- # @see AbstractQuery#request
15
- def request
16
- { accept: 'image/png' }
17
- end
18
-
19
- # Adds default variables for querying the image.
20
- #
21
- # @see AbstractQuery#pre_process
22
- def pre_process(_grafana)
23
- @variables['fullscreen'] = Variable.new(true)
24
- @variables['theme'] = Variable.new('light')
25
- @variables['timeout'] = Variable.new(timeout) if timeout
26
- @variables['timeout'] ||= Variable.new(60)
27
- end
28
-
29
- # Checks if the rendering has been performed properly.
30
- # If so, the resulting image is stored in the @result variable, otherwise an error is raised.
31
- #
32
- # @see AbstractQuery#post_process
33
- def post_process
34
- raise ImageCouldNotBeRenderedError, @panel if @result.body.include?('<html')
35
- end
36
-
37
- private
38
-
39
- def url_parameters
40
- url_vars = variables.select { |k, _v| k =~ /^(?:timeout|height|width|theme|fullscreen)/ || k =~ /^var-.+/ }
41
- url_vars['from'] = Variable.new(@from) if @from
42
- url_vars['to'] = Variable.new(@to) if @to
43
- url_params = URI.encode_www_form(url_vars.map { |k, v| [k, v.raw_value.to_s] })
44
- return '' if url_params.empty?
45
-
46
- '&' + url_params
47
- end
48
- end
49
- end
1
+ # frozen_string_literal: true
2
+
3
+ module Grafana
4
+ # Query, which allows to render a {Panel} as a PNG image.
5
+ class PanelImageQuery < AbstractPanelQuery
6
+ # Returns the URL for rendering the panel. Uses {Panel#render_url} and sets additional url
7
+ # parameters according {https://grafana.com/docs/grafana/latest/reference/share_panel Grafana Share Panel}.
8
+ #
9
+ # @see AbstractQuery#url
10
+ # @return [String] string for rendering the panel
11
+ def url
12
+ @panel.render_url + url_parameters
13
+ end
14
+
15
+ # Changes the result of the request to be of type +image/png+.
16
+ #
17
+ # @see AbstractQuery#request
18
+ def request
19
+ { accept: 'image/png' }
20
+ end
21
+
22
+ # Adds default variables for querying the image.
23
+ #
24
+ # @see AbstractQuery#pre_process
25
+ def pre_process(_grafana)
26
+ @variables['fullscreen'] = Variable.new(true)
27
+ @variables['theme'] = Variable.new('light')
28
+ @variables['timeout'] = Variable.new(timeout) if timeout
29
+ @variables['timeout'] ||= Variable.new(60)
30
+ end
31
+
32
+ # Checks if the rendering has been performed properly.
33
+ # If so, the resulting image is stored in the @result variable, otherwise an error is raised.
34
+ #
35
+ # @see AbstractQuery#post_process
36
+ def post_process
37
+ raise ImageCouldNotBeRenderedError, @panel if @result.body.include?('<html')
38
+ end
39
+
40
+ private
41
+
42
+ def url_parameters
43
+ url_vars = variables.select { |k, _v| k =~ /^(?:timeout|height|width|theme|fullscreen)/ || k =~ /^var-.+/ }
44
+ url_vars['from'] = Variable.new(@from) if @from
45
+ url_vars['to'] = Variable.new(@to) if @to
46
+ url_params = URI.encode_www_form(url_vars.map { |k, v| [k, v.raw_value.to_s] })
47
+ return '' if url_params.empty?
48
+
49
+ "&#{url_params}"
50
+ end
51
+ end
52
+ end
@@ -1,259 +1,217 @@
1
- module Grafana
2
- # This class contains a representation of
3
- # {https://grafana.com/docs/grafana/latest/variables/templates-and-variables grafana variables},
4
- # aka grafana templates.
5
- #
6
- # The main need therefore rises in order to replace variables properly in different
7
- # texts, e.g. SQL statements or results.
8
- class Variable
9
- attr_reader :name, :text, :raw_value
10
-
11
- # @param config_or_value [Hash, Object] configuration hash of a variable out of an {Dashboard} instance or a value of any kind.
12
- def initialize(config_or_value)
13
- if config_or_value.is_a? Hash
14
- @config = config_or_value
15
- @name = @config['name']
16
- unless @config['current'].nil?
17
- @raw_value = @config['current']['value']
18
- @text = @config['current']['text']
19
- end
20
- else
21
- @config = {}
22
- @raw_value = config_or_value
23
- @text = config_or_value.to_s
24
- end
25
- end
26
-
27
- # Returns the stored value formatted according the given format.
28
- #
29
- # Supported formats are: +csv+, +distributed+, +doublequote+, +json+, +percentencode+, +pipe+, +raw+, +regex+, +singlequote+, +sqlstring+, +lucene+, +date+ or +glob+ (default)
30
- #
31
- # For details see {https://grafana.com/docs/grafana/latest/variables/advanced-variable-format-options Grafana Advanced variable format options}.
32
- #
33
- # For details of +date+ format, see {https://grafana.com/docs/grafana/latest/variables/variable-types/global-variables/#__from-and-__to}. Please note that input for +date+ format is unixtime in milliseconds.
34
- #
35
- # @param format [String] desired format
36
- # @return [String] value of stored variable according the specified format
37
- def value_formatted(format = '')
38
- value = @raw_value
39
-
40
- # handle value 'All' properly
41
- # TODO fix check for selection of All properly
42
- if value == 'All' or @text == 'All'
43
- if !@config['options'].empty?
44
- value = @config['options'].map { |item| item['value'] }
45
- elsif !@config['query'].empty?
46
- # TODO: replace variables in this query, too
47
- return @config['query']
48
- # TODO handle 'All' value properly for query attributes
49
- else
50
- # TODO how to handle All selection properly at this point?
51
- end
52
- end
53
-
54
- case format
55
- when 'csv'
56
- return value.join(',').to_s if multi?
57
-
58
- value.to_s
59
-
60
- when 'distributed'
61
- return value.join(",#{name}=") if multi?
62
-
63
- value
64
- when 'doublequote'
65
- if multi?
66
- value = value.map { |item| "\"#{item.gsub(/[\\]/, '\\\\').gsub(/"/, '\\"')}\"" }
67
- return value.join(',')
68
- end
69
- "\"#{value.gsub(/"/, '\\"')}\""
70
-
71
- when 'json'
72
- if multi?
73
- value = value.map { |item| "\"#{item.gsub(/["\\]/, '\\\\' + '\0')}\"" }
74
- return "[#{value.join(',')}]"
75
- end
76
- "\"#{value.gsub(/"/, '\\"')}\""
77
-
78
- when 'percentencode'
79
- value = "{#{value.join(',')}}" if multi?
80
- ERB::Util.url_encode(value)
81
-
82
- when 'pipe'
83
- return value.join('|') if multi?
84
-
85
- value
86
-
87
- when 'raw'
88
- return "{#{value.join(',')}}" if multi?
89
-
90
- value
91
-
92
- when 'regex'
93
- if multi?
94
- value = value.map { |item| item.gsub(%r{[/$\.\|\\]}, '\\\\' + '\0') }
95
- return "(#{value.join('|')})"
96
- end
97
- value.gsub(%r{[/$\.\|\\]}, '\\\\' + '\0')
98
-
99
- when 'singlequote'
100
- if multi?
101
- value = value.map { |item| "'#{item.gsub(/[']/, '\\\\' + '\0')}'" }
102
- return value.join(',')
103
- end
104
- "'#{value.gsub(/[']/, '\\\\' + '\0')}'"
105
-
106
- when 'sqlstring'
107
- if multi?
108
- value = value.map { |item| "'#{item.gsub(/'/, "''")}'" }
109
- return value.join(',')
110
- end
111
- "'#{value.gsub(/'/, "''")}'"
112
-
113
- when 'lucene'
114
- if multi?
115
- value = value.map { |item| "\"#{item.gsub(%r{[" |=/\\]}, '\\\\' + '\0')}\"" }
116
- return "(#{value.join(' OR ')})"
117
- end
118
- value.gsub(%r{[" |=/\\]}, '\\\\' + '\0')
119
-
120
- when /^date(?:[:](?<format>.*))?$/
121
- #TODO validate how grafana handles multivariables with date format
122
- get_date_formatted(value, $1)
123
-
124
- when ''
125
- # default
126
- if multi?
127
- value = value.map { |item| "'#{item.gsub(/'/, "''")}'" }
128
- return value.join(',')
129
- end
130
- value.gsub(/'/, "''")
131
-
132
- else
133
- # glob and all unknown
134
- #TODO add check for array value properly for all cases
135
- return "{#{value.join(',')}}" if multi? and value.is_a?(Array)
136
-
137
- value
138
- end
139
- end
140
-
141
- # @return [Boolean] true, if the value can contain multiple selections, i.e. is an Array
142
- def multi?
143
- return @config['multi'] unless @config['multi'].nil?
144
-
145
- @raw_value.is_a? Array
146
- end
147
-
148
- # @return [Object] raw value of the variable
149
- def raw_value=(new_val)
150
- @raw_value = new_val
151
- @raw_value = @raw_value.to_s unless @raw_value.is_a?(Array)
152
- new_text = @raw_value
153
- if @config['options']
154
- val = @config['options'].select { |item| item['value'] == @raw_value }
155
- new_text = val.first['text'] unless val.empty?
156
- end
157
- @text = new_text
158
- end
159
-
160
- private
161
-
162
- # Realize time formatting according
163
- # {https://grafana.com/docs/grafana/latest/variables/variable-types/global-variables/#__from-and-__to}
164
- # and {https://momentjs.com/docs/#/displaying/}.
165
- def get_date_formatted(value, format)
166
- return (Float(value) / 1000).to_i.to_s if format == 'seconds'
167
- return Time.at((Float(value) / 1000).to_i).utc.iso8601(3) if !format or format == 'iso'
168
-
169
- # build array of known matches
170
- matches = []
171
- work_string = format
172
- while work_string.length > 0
173
- tmp = work_string.scan(/^(?:M{1,4}|D{1,4}|d{1,4}|e|E|w{1,2}|W{1,2}|Y{4}|Y{2}|A|a|H{1,2}|h{1,2}|k{1,2}|m{1,2}|s{1,2}|S+|X)/)
174
- unless tmp.empty?
175
- matches << tmp[0]
176
- work_string.delete_prefix!(tmp[0])
177
- else
178
- matches << work_string[0]
179
- work_string.delete_prefix!(work_string[0])
180
- end
181
- end
182
-
183
- #TODO move case when to hash
184
- format_string = ""
185
- matches.each do |match|
186
- format_string += case match
187
- when 'M'
188
- '%-m'
189
- when 'MM'
190
- '%m'
191
- when 'MMM'
192
- '%b'
193
- when 'MMMM'
194
- '%B'
195
- when 'D'
196
- '%-d'
197
- when 'DD'
198
- '%d'
199
- when 'DDD'
200
- '%-j'
201
- when 'DDDD'
202
- '%j'
203
- when 'YY'
204
- '%y'
205
- when 'YYYY'
206
- '%Y'
207
- when 'd'
208
- '%w'
209
- when 'ddd'
210
- '%a'
211
- when 'dddd'
212
- '%A'
213
- when 'e'
214
- '%w'
215
- when 'E'
216
- '%u'
217
- when 'w'
218
- '%-U'
219
- when 'ww'
220
- '%U'
221
- when 'W'
222
- '%-V'
223
- when 'WW'
224
- '%V'
225
- when 'YY'
226
- '%y'
227
- when 'YYYY'
228
- '%Y'
229
- when 'A'
230
- '%p'
231
- when 'a'
232
- '%P'
233
- when 'H'
234
- '%-H'
235
- when 'HH'
236
- '%H'
237
- when 'h'
238
- '%-I'
239
- when 'hh'
240
- '%I'
241
- when 'm'
242
- '%-M'
243
- when 'mm'
244
- '%M'
245
- when 's'
246
- '%-S'
247
- when 'ss'
248
- '%S'
249
- when 'X'
250
- '%s'
251
- else
252
- match
253
- end
254
- end
255
-
256
- Time.at((Float(value) / 1000).to_i).strftime(format_string)
257
- end
258
- end
259
- end
1
+ # frozen_string_literal: true
2
+
3
+ module Grafana
4
+ # This class contains a representation of
5
+ # {https://grafana.com/docs/grafana/latest/variables/templates-and-variables grafana variables},
6
+ # aka grafana templates.
7
+ #
8
+ # The main need therefore rises in order to replace variables properly in different
9
+ # texts, e.g. SQL statements or results.
10
+ class Variable
11
+ attr_reader :name, :text, :raw_value
12
+
13
+ DATE_MATCHES = { 'M' => '%-m', 'MM' => '%m', 'MMM' => '%b', 'MMMM' => '%B',
14
+ 'D' => '%-d', 'DD' => '%d', 'DDD' => '%-j', 'DDDD' => '%j',
15
+ 'd' => '%w', 'ddd' => '%a', 'dddd' => '%A',
16
+ 'YY' => '%y', 'YYYY' => '%Y',
17
+ 'h' => '%-I', 'hh' => '%I',
18
+ 'H' => '%-H', 'HH' => '%H',
19
+ 'm' => '%-M', 'mm' => '%M',
20
+ 's' => '%-S', 'ss' => '%S',
21
+ 'w' => '%-U', 'ww' => '%U',
22
+ 'W' => '%-V', 'WW' => '%V',
23
+ 'a' => '%P',
24
+ 'A' => '%p',
25
+ 'e' => '%w',
26
+ 'E' => '%u',
27
+ 'X' => '%s' }.freeze
28
+
29
+ # @param config_or_value [Hash, Object] configuration hash of a variable out of an {Dashboard} instance
30
+ # or a value of any kind.
31
+ def initialize(config_or_value)
32
+ if config_or_value.is_a? Hash
33
+ @config = config_or_value
34
+ @name = @config['name']
35
+ unless @config['current'].nil?
36
+ @raw_value = @config['current']['value']
37
+ @text = @config['current']['text']
38
+ end
39
+ else
40
+ @config = {}
41
+ @raw_value = config_or_value
42
+ @text = config_or_value.to_s
43
+ end
44
+ end
45
+
46
+ # Returns the stored value formatted according the given format.
47
+ #
48
+ # Supported formats are: +csv+, +distributed+, +doublequote+, +json+, +percentencode+, +pipe+, +raw+,
49
+ # +regex+, +singlequote+, +sqlstring+, +lucene+, +date+ or +glob+ (default)
50
+ #
51
+ # For details see {https://grafana.com/docs/grafana/latest/variables/advanced-variable-format-options
52
+ # Grafana Advanced variable format options}.
53
+ #
54
+ # For details of +date+ format, see
55
+ # {https://grafana.com/docs/grafana/latest/variables/variable-types/global-variables/#__from-and-__to
56
+ # Grafana global variables $__from and $__to}.
57
+ # Please note that input for +date+ format is unixtime in milliseconds.
58
+ #
59
+ # @param format [String] desired format
60
+ # @return [String] value of stored variable according the specified format
61
+ def value_formatted(format = '')
62
+ value = @raw_value
63
+
64
+ # handle value 'All' properly
65
+ # TODO: fix check for selection of All properly
66
+ if (value == 'All') || (@text == 'All')
67
+ if !@config['options'].empty?
68
+ value = @config['options'].map { |item| item['value'] }
69
+ elsif !@config['query'].empty?
70
+ # TODO: replace variables in this query, too
71
+ return @config['query']
72
+ # TODO: handle 'All' value properly for query attributes
73
+ else
74
+ # TODO: how to handle All selection properly at this point?
75
+ end
76
+ end
77
+
78
+ case format
79
+ when 'csv'
80
+ return value.join(',').to_s if multi?
81
+
82
+ value.to_s
83
+
84
+ when 'distributed'
85
+ return value.join(",#{name}=") if multi?
86
+
87
+ value
88
+ when 'doublequote'
89
+ if multi?
90
+ value = value.map { |item| "\"#{item.gsub(/\\/, '\\\\').gsub(/"/, '\\"')}\"" }
91
+ return value.join(',')
92
+ end
93
+ "\"#{value.gsub(/"/, '\\"')}\""
94
+
95
+ when 'json'
96
+ if multi?
97
+ value = value.map { |item| "\"#{item.gsub(/["\\]/, '\\\\\0')}\"" }
98
+ return "[#{value.join(',')}]"
99
+ end
100
+ "\"#{value.gsub(/"/, '\\"')}\""
101
+
102
+ when 'percentencode'
103
+ value = "{#{value.join(',')}}" if multi?
104
+ ERB::Util.url_encode(value)
105
+
106
+ when 'pipe'
107
+ return value.join('|') if multi?
108
+
109
+ value
110
+
111
+ when 'raw'
112
+ return "{#{value.join(',')}}" if multi?
113
+
114
+ value
115
+
116
+ when 'regex'
117
+ if multi?
118
+ value = value.map { |item| item.gsub(%r{[/$.|\\]}, '\\\\\0') }
119
+ return "(#{value.join('|')})"
120
+ end
121
+ value.gsub(%r{[/$.|\\]}, '\\\\\0')
122
+
123
+ when 'singlequote'
124
+ if multi?
125
+ value = value.map { |item| "'#{item.gsub(/'/, '\\\\\0')}'" }
126
+ return value.join(',')
127
+ end
128
+ "'#{value.gsub(/'/, '\\\\\0')}'"
129
+
130
+ when 'sqlstring'
131
+ if multi?
132
+ value = value.map { |item| "'#{item.gsub(/'/, "''")}'" }
133
+ return value.join(',')
134
+ end
135
+ "'#{value.gsub(/'/, "''")}'"
136
+
137
+ when 'lucene'
138
+ if multi?
139
+ value = value.map { |item| "\"#{item.gsub(%r{[" |=/\\]}, '\\\\\0')}\"" }
140
+ return "(#{value.join(' OR ')})"
141
+ end
142
+ value.gsub(%r{[" |=/\\]}, '\\\\\0')
143
+
144
+ when /^date(?::(?<format>.*))?$/
145
+ # TODO: validate how grafana handles multivariables with date format
146
+ get_date_formatted(value, Regexp.last_match(1))
147
+
148
+ when ''
149
+ # default
150
+ if multi?
151
+ value = value.map { |item| "'#{item.gsub(/'/, "''")}'" }
152
+ return value.join(',')
153
+ end
154
+ value.gsub(/'/, "''")
155
+
156
+ else
157
+ # glob and all unknown
158
+ # TODO add check for array value properly for all cases
159
+ return "{#{value.join(',')}}" if multi? && value.is_a?(Array)
160
+
161
+ value
162
+ end
163
+ end
164
+
165
+ # @return [Boolean] true, if the value can contain multiple selections, i.e. is an Array
166
+ def multi?
167
+ return @config['multi'] unless @config['multi'].nil?
168
+
169
+ @raw_value.is_a? Array
170
+ end
171
+
172
+ # @return [Object] raw value of the variable
173
+ def raw_value=(new_val)
174
+ @raw_value = new_val
175
+ @raw_value = @raw_value.to_s unless @raw_value.is_a?(Array)
176
+ new_text = @raw_value
177
+ if @config['options']
178
+ val = @config['options'].select { |item| item['value'] == @raw_value }
179
+ new_text = val.first['text'] unless val.empty?
180
+ end
181
+ @text = new_text
182
+ end
183
+
184
+ private
185
+
186
+ # Realize time formatting according
187
+ # {https://grafana.com/docs/grafana/latest/variables/variable-types/global-variables/#__from-and-__to}
188
+ # and {https://momentjs.com/docs/#/displaying/}.
189
+ def get_date_formatted(value, format)
190
+ return (Float(value) / 1000).to_i.to_s if format == 'seconds'
191
+ return Time.at((Float(value) / 1000).to_i).utc.iso8601(3) if !format || (format == 'iso')
192
+
193
+ # build array of known matches
194
+ matches = []
195
+ work_string = format
196
+ until work_string.empty?
197
+ tmp = work_string.scan(/^(?:M{1,4}|D{1,4}|d{1,4}|e|E|w{1,2}|W{1,2}|Y{4}|Y{2}|A|a|H{1,2}|
198
+ h{1,2}|k{1,2}|m{1,2}|s{1,2}|S+|X)/x)
199
+ if tmp.empty?
200
+ matches << work_string[0]
201
+ work_string.delete_prefix!(work_string[0])
202
+ else
203
+ matches << tmp[0]
204
+ work_string.delete_prefix!(tmp[0])
205
+ end
206
+ end
207
+
208
+ format_string = ''.dup
209
+ matches.each do |match|
210
+ replacement = DATE_MATCHES[match]
211
+ format_string << (replacement || match)
212
+ end
213
+
214
+ Time.at((Float(value) / 1000).to_i).strftime(format_string)
215
+ end
216
+ end
217
+ end