json22d 0.5

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 (4) hide show
  1. checksums.yaml +7 -0
  2. data/lib/json22d.rb +222 -0
  3. data/spec/json22d_spec.rb +291 -0
  4. metadata +74 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b784be733972cbb47cd4ea2b690c3b934a65afd6
4
+ data.tar.gz: d8f1a686b5874b77b16400d051fcc547b96a7739
5
+ SHA512:
6
+ metadata.gz: db66b220c6114cc63609c1c242fb5e3de2a9a83760d6f1fb9a394d03122dd3cd478b6d2ad7994c4e12f55e6c86b1be72114fd4b2b067a68d61ee6f52dacde844
7
+ data.tar.gz: e6dc4e0286e433bc7511cfffba8b11c8a0a317919ad230bb5c514c5a4cfb23415b776f73e79d0e9d2d691ad1b75f7e94966444e9cc3a7a9623cbe808d143d8e8
data/lib/json22d.rb ADDED
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+ require "json"
3
+ require "active_support/inflector"
4
+
5
+ module JSON22d
6
+ extend self
7
+
8
+ VERSION = "0.5"
9
+
10
+ def run(arr, config)
11
+ arr = arr.to_json unless arr.is_a?(String)
12
+ arr = JSON.parse(arr)
13
+ fill_blanks(arr, config, &(block_given? ? Proc.new : nil))
14
+ return Enumerator.new do |y|
15
+ y << header(config)
16
+ arr.each do |h|
17
+ row(block_given? ? yield(h) : h, config).each { |r| y << r }
18
+ end
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def fill_blanks(values, config)
25
+ values = values.first if values.is_a?(Array) && values.size == 1
26
+ values = [values] unless values.is_a?(Array)
27
+ subconfig = config.select { |c| c.is_a?(Hash) }
28
+ subconfig.each do |name|
29
+ key, value = name.first
30
+ comment, key, no_n, shift, unshift = key.to_s.
31
+ match(/^(#)?([^\[]+?)(\[\])?( SHIFT)?( UNSHIFT)?$/)&.
32
+ captures
33
+ if no_n
34
+ max_n = values.reduce(0) do |acc, v|
35
+ v = yield(v) if block_given?
36
+ sub_hash = v&.[](key)
37
+ if sub_hash.is_a?(Array)
38
+ acc = sub_hash.count if acc < sub_hash.count
39
+ elsif !sub_hash.nil?
40
+ acc = 1 if acc < 1
41
+ end
42
+ next acc
43
+ end
44
+
45
+ name.delete(name.keys.first)
46
+ name["#{key}[#{max_n}]#{shift}#{unshift}"] = value
47
+ end
48
+ fill_blanks(values.map do |v|
49
+ v = yield(v) if block_given?
50
+ comment ? v : v&.[](key)
51
+ end, value)
52
+ end
53
+ end
54
+
55
+ def header(config)
56
+ return config.reduce([]) do |acc, name|
57
+ if name.is_a?(Hash)
58
+ key, value = name.first
59
+ comment, key, closures, n, n2, shift, unshift = key.to_s.
60
+ match(/^(#)?([^\[(\s]+)(\[(\d+)\]|\(([^\)]+)\))?( SHIFT)?( UNSHIFT)?$/)&.
61
+ captures
62
+ key, op = key.split(".")
63
+ key = key.singularize
64
+ if comment
65
+ acc + header(value).map { |m| "#{key}.#{m}" }
66
+ elsif n
67
+ next acc + n.to_i.times.reduce([]) do |a, i|
68
+ a + header(value).map do |m|
69
+ if shift
70
+ "#{key}[#{i}]#{m.match(/^[^(\[\s\.]+(.*)$/)&.captures&.first}"
71
+ elsif unshift
72
+ "#{m}[#{i}]"
73
+ elsif op
74
+ "#{key}.#{op}_#{m}"
75
+ else
76
+ "#{key}[#{i}].#{m}"
77
+ end
78
+ end
79
+ end
80
+ elsif closures.nil? || n2
81
+ next acc + header(value).map do |m|
82
+ if shift
83
+ "#{key}#{m.match(/^[^(\[\s\.]+(.*)$/)&.captures&.first}"
84
+ elsif unshift
85
+ "#{m}"
86
+ elsif op
87
+ "#{key}.#{op}_#{m}"
88
+ else
89
+ "#{key}.#{m}"
90
+ end
91
+ end
92
+ else
93
+ # "pos" is the column for determining i.e. offer position in a product
94
+ next acc + (["pos"] + header(value)).map do |m|
95
+ if shift
96
+ "#{key}#{m.match(/^[^(\[\s\.]+(.*)$/)&.captures&.first}"
97
+ elsif unshift
98
+ "#{m}"
99
+ elsif op
100
+ "#{key}.#{op}_#{m}"
101
+ else
102
+ "#{key}.#{m}"
103
+ end
104
+ end
105
+ end
106
+ elsif name.is_a?(Array)
107
+ _key, title = name
108
+ next acc << title.to_s
109
+ else
110
+ name, *_ = name.to_s.match(/^([^(]+)(\(([^\)]+)\))?$/)&.captures
111
+ name = name.match(/^([^+]+)/).captures.first
112
+ next acc << name.to_s
113
+ end
114
+ end
115
+ end
116
+
117
+ def row(hash, config)
118
+ multiply(slice(hash, config))
119
+ end
120
+
121
+ def multiply(array, result = [[]])
122
+ array = [array] unless array.is_a?(Array)
123
+ array.each do |elem|
124
+ if elem.is_a?(Array)
125
+ result = elem.reduce([]) { |a, e| a + multiply(e, result.map(&:dup)) }
126
+ else
127
+ result = result.map { |r| r << elem }
128
+ end
129
+ end
130
+ return result
131
+ end
132
+
133
+ def slice(hash, config)
134
+ return config.reduce([]) do |acc, name|
135
+ if name.is_a?(Hash)
136
+ key, value = name.first
137
+ comment, key, closures, n, n2, _shift, _unshift = key.to_s.
138
+ match(/^(#)?([^\[(\s]+)(\[(\d+)\]|\(([^\)]+)\))?( SHIFT)?( UNSHIFT)?$/)&.
139
+ captures
140
+ key, op = key.split(".")
141
+ sub_hash = hash&.[](key)
142
+ if comment
143
+ acc + slice(hash, value)
144
+ elsif n2 && sub_hash
145
+ next acc << sub_hash.
146
+ reduce([]) { |a, h| a + slice(h, value) }.
147
+ join(n2)
148
+ elsif n && (sub_hash || n.to_i == 0)
149
+ next acc + n.to_i.times.map { |i| sub_hash[i] }.
150
+ reduce([]) { |a, h| a + slice(h, value) }
151
+ elsif sub_hash.is_a?(Array)
152
+ # [i] is the "pos" column
153
+ next acc << [slice({}, value)] if sub_hash.empty?
154
+ next acc << sub_hash.map.
155
+ with_index do |h, i|
156
+ (closures.nil? ? [] : [i]) + slice(h, value)
157
+ end.reduce(nil, &with_op(op))
158
+ else
159
+ next acc + slice(sub_hash, value)
160
+ end
161
+ else
162
+ if hash.nil?
163
+ next acc << hash
164
+ else
165
+ name, _title = name if name.is_a?(Array)
166
+ name, closures, n = name.to_s.
167
+ match(/^([^(]+)(\(([^\)]+)\))?$/)&.captures
168
+ sub_array = hash[name]
169
+ if sub_array.is_a?(Array)
170
+ sub_array = sub_array.map(&method(:sprintf))
171
+ else
172
+ sub_array = sprintf(sub_array)
173
+ end
174
+ if name.include?("+")
175
+ next acc << name.split("+").map { |k| hash[k] }.compact.join(" ")
176
+ elsif n && sub_array.is_a?(Array)
177
+ next acc << sub_array.join(n)
178
+ else
179
+ next acc << sub_array
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
185
+
186
+ def with_op(op)
187
+ if op
188
+ case op
189
+ when "min"
190
+ ->(a, e) do
191
+ e = e.first if e.is_a?(Array)
192
+ ef = e.to_f
193
+ af = a&.to_f
194
+ (af || ef) > ef ? e : (a || e)
195
+ end
196
+ when "max"
197
+ ->(a, e) do
198
+ e = e.first if e.is_a?(Array)
199
+ ef = e.to_f
200
+ af = a&.to_f
201
+ (af || 0) < ef ? e : a
202
+ end
203
+ when "first"
204
+ ->(a, e) do
205
+ e = e.first if e.is_a?(Array)
206
+ a || e
207
+ end
208
+ end
209
+ else
210
+ return ->(a, e) { (a || []) << e }
211
+ end
212
+ end
213
+
214
+ def sprintf(value)
215
+ if !value.respond_to?(:strftime) &&
216
+ value !~ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/
217
+ return value
218
+ end
219
+ value = Time.parse(value) if !value.respond_to?(:strftime)
220
+ return value.strftime("%Y-%m-%d %H:%M:%S %Z")
221
+ end
222
+ end
@@ -0,0 +1,291 @@
1
+ # frozen_string_literal: true
2
+ require "minitest/autorun"
3
+ require "minitest/spec"
4
+ require "json22d"
5
+
6
+ describe "JSON22d" do
7
+ after do
8
+ @arr = @config = nil
9
+ end
10
+ describe "fill missing ranges" do
11
+ before do
12
+ @arr = [
13
+ {"a": [{"i": 1}, {"i": 3}, {"i": 6}]},
14
+ {"a": [{"i": 2}, {"i": 4}]}
15
+ ]
16
+ @config = ["a[]": %w(i)]
17
+ end
18
+
19
+ it "inserts the maximum array length into the header as fields" do
20
+ enum = JSON22d.run(@arr, @config)
21
+ assert_equal 3, enum.next.size
22
+ end
23
+
24
+ it "uses an iteration index for each generated header" do
25
+ enum = JSON22d.run(@arr, @config)
26
+ header = enum.next
27
+ 3.times.each do |i|
28
+ assert_equal "a[#{i}].i", header[i]
29
+ end
30
+ end
31
+ end
32
+
33
+ describe "extract regular fields" do
34
+ before do
35
+ @arr = [{"i": 3, "j": 4}, {"i": "foo"}]
36
+ @config = %w(i j)
37
+ end
38
+
39
+ it "sets the correct header" do
40
+ header = JSON22d.run(@arr, @config).next
41
+ assert_equal ["i", "j"], header
42
+ end
43
+
44
+ it "extracts the data in correct order" do
45
+ enum = JSON22d.run(@arr, @config)
46
+ enum.next # throw away header
47
+ assert_equal [3, 4], enum.next
48
+ assert_equal ["foo", nil], enum.next
49
+ end
50
+ end
51
+
52
+ describe "extract nested fields" do
53
+ before do
54
+ @arr = ["content": {"i": "foo"}]
55
+ @config = ["content": %w(i)]
56
+ end
57
+
58
+ it "sets the correct header" do
59
+ header = JSON22d.run(@arr, @config).next
60
+ assert_equal ["content.i"], header
61
+ end
62
+
63
+ it "extracts the data in correct order" do
64
+ enum = JSON22d.run(@arr, @config)
65
+ enum.next # throw away header
66
+ assert_equal ["foo"], enum.next
67
+ end
68
+ end
69
+
70
+ describe "simulate nested field in header" do
71
+ before do
72
+ @arr = ["content": "foo"]
73
+ @config = ["#my": %w(content)]
74
+ end
75
+
76
+ it "sets the correct header" do
77
+ header = JSON22d.run(@arr, @config).next
78
+ assert_equal ["my.content"], header
79
+ end
80
+
81
+ it "extracts the data while skipping simulated headers" do
82
+ enum = JSON22d.run(@arr, @config)
83
+ enum.next # throw away header
84
+ assert_equal ["foo"], enum.next
85
+ end
86
+ end
87
+
88
+ describe "multiplies fields within arrays" do
89
+ before do
90
+ @arr = ["content": ["bar", "foo"]]
91
+ @config = ["content"]
92
+ end
93
+
94
+ it "sets the correct header" do
95
+ header = JSON22d.run(@arr, @config).next
96
+ assert_equal ["content"], header
97
+ end
98
+
99
+ it "extracts the data in correct order" do
100
+ enum = JSON22d.run(@arr, @config)
101
+ enum.next # throw away header
102
+ assert_equal ["bar"], enum.next
103
+ assert_equal ["foo"], enum.next
104
+ end
105
+ end
106
+
107
+ describe "multiplies nested fields within arrays" do
108
+ before do
109
+ @arr = ["content": [{"i": "bar"}, {"i": "foo"}]]
110
+ @config = ["content": %w(i)]
111
+ end
112
+
113
+ it "sets the correct header" do
114
+ header = JSON22d.run(@arr, @config).next
115
+ assert_equal ["content.i"], header
116
+ end
117
+
118
+ it "extracts the data in correct order" do
119
+ enum = JSON22d.run(@arr, @config)
120
+ enum.next # throw away header
121
+ assert_equal ["bar"], enum.next
122
+ assert_equal ["foo"], enum.next
123
+ end
124
+ end
125
+
126
+ describe "joins two field results with a space" do
127
+ before do
128
+ @arr = [{"i": "bar", "j": "foo"}]
129
+ @config = %w(i+j)
130
+ end
131
+
132
+ it "sets the correct header" do
133
+ header = JSON22d.run(@arr, @config).next
134
+ assert_equal ["i"], header
135
+ end
136
+
137
+ it "extracts and joins the two fields" do
138
+ enum = JSON22d.run(@arr, @config)
139
+ enum.next # throw away header
140
+ assert_equal ["bar foo"], enum.next
141
+ end
142
+ end
143
+
144
+ describe "joins multiple values within subarrays with given delim" do
145
+ before do
146
+ @arr = [{"i": ["bar", "blubb"]}, {"i": ["foo"]}]
147
+ @config = ["i( , )"]
148
+ end
149
+
150
+ it "sets the correct header" do
151
+ header = JSON22d.run(@arr, @config).next
152
+ assert_equal ["i"], header
153
+ end
154
+
155
+ it "extracts and joins the arrays" do
156
+ enum = JSON22d.run(@arr, @config)
157
+ enum.next # throw away header
158
+ assert_equal ["bar , blubb"], enum.next
159
+ assert_equal ["foo"], enum.next
160
+ end
161
+ end
162
+
163
+ describe "joins multiple values after applying nested fields" do
164
+ before do
165
+ @arr = [{"i": [{"j": "bar"}, {"j": "blubb"}]}]
166
+ @config = ["i( , )": %w(j)]
167
+ end
168
+
169
+ it "sets the correct header" do
170
+ header = JSON22d.run(@arr, @config).next
171
+ assert_equal ["i.j"], header
172
+ end
173
+
174
+ it "extracts and joins the arrays" do
175
+ enum = JSON22d.run(@arr, @config)
176
+ enum.next # throw away header
177
+ assert_equal ["bar , blubb"], enum.next
178
+ end
179
+ end
180
+
181
+ describe "multiplies nested array into header" do
182
+ before do
183
+ @arr = ["content": [{"i": "bar"}, {"i": "foo"}]]
184
+ @config = ["content[]": %w(i)]
185
+ end
186
+
187
+ it "sets the correct header" do
188
+ header = JSON22d.run(@arr, @config).next
189
+ assert_equal ["content[0].i", "content[1].i"], header
190
+ end
191
+
192
+ it "extracts the data in correct order" do
193
+ enum = JSON22d.run(@arr, @config)
194
+ enum.next # throw away header
195
+ assert_equal ["bar", "foo"], enum.next
196
+ end
197
+ end
198
+
199
+ describe "multiplies nested array into header with range" do
200
+ before do
201
+ @arr = ["content": [{"i": "bar"}, {"i": "foo"}]]
202
+ @config = ["content[1]": %w(i)]
203
+ end
204
+
205
+ it "sets the correct header" do
206
+ header = JSON22d.run(@arr, @config).next
207
+ assert_equal ["content[0].i"], header
208
+ end
209
+
210
+ it "extracts the data up to range" do
211
+ enum = JSON22d.run(@arr, @config)
212
+ enum.next # throw away header
213
+ assert_equal ["bar"], enum.next
214
+ end
215
+ end
216
+
217
+ describe "shift nested name down in header" do
218
+ before do
219
+ @arr = ["content": [{"i": {"j":"bar"}}, {"i": {"j": "foo"}}]]
220
+ end
221
+
222
+ it "shifts j out for line multiplication" do
223
+ header = JSON22d.run(@arr, ["content": ["i SHIFT": %w(j)]]).next
224
+ assert_equal ["content.i"], header
225
+ end
226
+
227
+ it "shifts j out for column multiplication" do
228
+ header = JSON22d.run(@arr, ["content": ["i[] SHIFT": %w(j)]]).next
229
+ assert_equal ["content.i[0]"], header
230
+ end
231
+
232
+ it "shifts i out for line multiplication" do
233
+ header = JSON22d.run(@arr, ["content[] SHIFT": ["i": %w(j)]]).next
234
+ assert_equal ["content[0].j", "content[1].j"], header
235
+ end
236
+
237
+ it "shifts i out for column multiplication but keeps brackets" do
238
+ header = JSON22d.run(@arr, ["content[] SHIFT": ["i[]": %w(j)]]).next
239
+ assert_equal ["content[0][0].j", "content[1][0].j"], header
240
+ end
241
+ end
242
+
243
+ describe "shift nested name up in header" do
244
+ before do
245
+ @arr = ["content": [{"i": {"j":"bar"}}, {"i": {"j": "foo"}}]]
246
+ end
247
+
248
+ it "shifts content out for line multiplication" do
249
+ header = JSON22d.run(@arr, ["content UNSHIFT": ["i": %w(j)]]).next
250
+ assert_equal ["i.j"], header
251
+ end
252
+
253
+ it "shifts content out for column multiplication" do
254
+ header = JSON22d.run(@arr, ["content UNSHIFT": ["i[]": %w(j)]]).next
255
+ assert_equal ["i[0].j"], header
256
+ end
257
+
258
+ it "shifts j out for line multiplication" do
259
+ header = JSON22d.run(@arr, ["content[]": ["i UNSHIFT": %w(j)]]).next
260
+ assert_equal ["content[0].j", "content[1].j"], header
261
+ end
262
+
263
+ it "shifts j out for column multiplication but keeps brackets" do
264
+ header = JSON22d.run(@arr, ["content[]": ["i[] UNSHIFT": %w(j)]]).next
265
+ assert_equal ["content[0].j[0]", "content[1].j[0]"], header
266
+ end
267
+ end
268
+
269
+ {
270
+ min: 1,
271
+ max: 3,
272
+ first: 2
273
+ }.each do |op, val|
274
+ describe "extract value by aggregator" do
275
+ before do
276
+ @arr = [{"content": [{"i": 2}, {"i": 3}, {"i": 1}]}]
277
+ end
278
+
279
+ it "generates the correct header for #{op}" do
280
+ header = JSON22d.run(@arr, ["content.#{op}": ["i"]]).next
281
+ assert_equal ["content.#{op}_i"], header
282
+ end
283
+
284
+ it "extracts the #{op} value from i" do
285
+ enum = JSON22d.run(@arr, ["content.#{op}": ["i"]])
286
+ enum.next # throw away header
287
+ assert_equal [val], enum.next
288
+ end
289
+ end
290
+ end
291
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: json22d
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.5'
5
+ platform: ruby
6
+ authors:
7
+ - Matthias Geier
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-01-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">"
18
+ - !ruby/object:Gem::Version
19
+ version: '3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">"
25
+ - !ruby/object:Gem::Version
26
+ version: '3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5'
41
+ description: Create CSV/XSLX formats and many others with this transpiler
42
+ email:
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - lib/json22d.rb
48
+ - spec/json22d_spec.rb
49
+ homepage: https://github.com/metoda/json22d
50
+ licenses:
51
+ - BSD-2-Clause
52
+ metadata: {}
53
+ post_install_message:
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubyforge_project:
69
+ rubygems_version: 2.5.1
70
+ signing_key:
71
+ specification_version: 4
72
+ summary: Transpile JSON into a flat structure
73
+ test_files:
74
+ - spec/json22d_spec.rb