json22d 0.5

Sign up to get free protection for your applications and to get access to all the features.
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