Skip to content

Commit 6467919

Browse files
committed
add format-array (formats JSON arrays)
format-array expresses our per-file preferences for array formatting, in cases where the preference differs from prettier. To see that this is true, the following procedure can be used: 1. Delete or truncate .pretterignore and run prettier on JSON files. prettier will have reformatted them. 2. run format-array. It will restore the files to their original state.
1 parent dc4744a commit 6467919

File tree

1 file changed

+178
-0
lines changed

1 file changed

+178
-0
lines changed

bin/format-array.rb

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
require 'json'
2+
3+
# format-array expresses (and enacts) our per-file preferences for array formatting,
4+
# in cases where the preference differs from prettier.
5+
#
6+
# It has only one mode of operation:
7+
# Run it without any arguments to format the files.
8+
9+
# format-array was written because we want to keep files consistently formatted,
10+
# while also optimising for human readability.
11+
#
12+
# As part of optimising for human readability,
13+
# some arrays should be formatted such that each element is on its own line,
14+
# whereas some other arrays should be formatted such that the entire array is on one line.
15+
#
16+
# We could not find an existing tool that allows us to specify array formatting how we wanted.
17+
18+
# The configuration of array formatting preferences.
19+
#
20+
# format can be the following choices:
21+
#
22+
# * single_line (the entire array and all of its elements should be on a single line)
23+
# * multi_line (each element of the array is on its own line)
24+
# * multi_line_unless_single (like multi_line, except arrays with one element remain on a single line)
25+
# * multi_line_deep (multi_line, even applied to arrays within that array)
26+
# * padded (14 elements per line padded to 3 characters per element)
27+
# (TODO: padding amount could be configurable, but we haven't needed it)
28+
formats = {
29+
'book-store' => {
30+
'basket' => :single_line,
31+
},
32+
'bowling' => {
33+
'previousRolls' => :single_line,
34+
},
35+
'change' => {
36+
'expected' => :single_line,
37+
},
38+
'connect' => {
39+
'board' => :multi_line,
40+
},
41+
'diamond' => {
42+
'expected' => :multi_line,
43+
},
44+
'dominoes' => {
45+
'dominoes' => :single_line,
46+
},
47+
'flatten-array' => {
48+
'array' => :multi_line_deep,
49+
'expected' => :multi_line,
50+
},
51+
'forth' => {
52+
'instructions' => :multi_line_unless_single,
53+
},
54+
'go-counting' => {
55+
'board' => :multi_line,
56+
},
57+
'minesweeper' => {
58+
'minefield' => :multi_line_unless_single,
59+
'expected' => :multi_line_unless_single,
60+
},
61+
'ocr-numbers' => {
62+
'rows' => :multi_line,
63+
},
64+
'rectangles' => {
65+
'strings' => :multi_line_unless_single,
66+
},
67+
'saddle-points' => {
68+
'matrix' => :multi_line,
69+
},
70+
'scale-generator' => {
71+
'expected' => :single_line,
72+
},
73+
'sieve' => {
74+
'expected' => :padded,
75+
},
76+
'transpose' => {
77+
'lines' => :multi_line,
78+
'expected' => :multi_line,
79+
},
80+
'variable-length-quantity' => {
81+
'integers' => :single_line,
82+
'expected' => :single_line,
83+
},
84+
'word-search' => {
85+
'grid' => :multi_line,
86+
'wordsToSearchFor' => :multi_line,
87+
},
88+
}.each_value(&:freeze).freeze
89+
90+
def single_line_arrays(contents, key)
91+
# matches things of the form "key": [1, 2, 3]
92+
# because this is not a multi-line regex,
93+
# . does NOT match newlines.
94+
contents.scan(/^( +)"#{key}": (\[.*\],?$)/)
95+
end
96+
97+
def multi_line_arrays(contents, key)
98+
# matches things of the form
99+
# "key": [
100+
# 1,
101+
# 2,
102+
# 3
103+
# ]
104+
# because this IS a multi-line regex (note the /m),
105+
# . DOES match newlines.
106+
#
107+
# To find which closing bracket matches the opening bracket,
108+
# we find the *first* closing bracket that is both:
109+
# - the only thing on its line (except for maybe a comma afterward)
110+
# - at the same indentation level as the key
111+
#
112+
# to find the first of these, make the match non-greedy (*? instead of *)
113+
contents.scan(/^( +)"#{key}": (\[$.*?^\1\],?$)/m)
114+
end
115+
116+
formats.each { |exercise, exercise_format|
117+
filename = "#{__dir__}/../exercises/#{exercise}/canonical-data.json"
118+
contents = File.read(filename)
119+
exercise_format.each { |key, format_type|
120+
replace = ->(old, new) {
121+
# Include the key in both the search and the replacement,
122+
# to avoid accidentally replacing something we didn't mean to.
123+
# This has been observed to be important for transpose,
124+
# where ["A1"] is both an input and an output.
125+
contents.sub!(%Q("#{key}": #{old}), %Q("#{key}": #{new}))
126+
}
127+
128+
case format_type
129+
when :single_line
130+
multi_line_arrays(contents, key).each { |indent, arr|
131+
arr_lines = arr.lines
132+
raise "impossible, array doesn't start with [ but instead #{arr_lines[0]}" if arr_lines[0] != "[\n"
133+
raise "impossible, array doesn't end with ] but instead #{arr_lines[-1]}" unless arr_lines[-1].match?(/^ *\],?$/)
134+
replace[arr, "[#{arr_lines[1...-1].map(&:strip).join(' ')}]#{',' if arr[-1] == ','}"]
135+
}
136+
when :multi_line, :multi_line_unless_single
137+
single_line_arrays(contents, key).each { |indent, arr|
138+
elements = JSON.parse(arr.delete_suffix(','))
139+
next if elements.empty?
140+
next if elements.size == 1 && format_type == :multi_line_unless_single
141+
indented_elements = elements.map { |el|
142+
js = JSON.generate(el)
143+
# JSON.generate will output [1,2,3] but we want [1, 2, 3]
144+
"#{indent} #{el.is_a?(Array) ? js.gsub(',', ', ') : js}"
145+
}
146+
# all lines but the last need a trailing comma
147+
replace[arr, "[\n#{indented_elements.join(",\n")}\n#{indent}]#{',' if arr[-1] == ','}"]
148+
}
149+
when :multi_line_deep
150+
single_line_arrays(contents, key).each { |indent, arr|
151+
# pretty_generate will render an empty array as:
152+
# [
153+
#
154+
# ]
155+
# whereas we just want []
156+
pretty_lines = JSON.pretty_generate(JSON.parse(arr)).sub(/\[\s+\]/, '[]').lines
157+
if pretty_lines == ['[]']
158+
replace[arr, '[]']
159+
else
160+
raise "impossible, array doesn't start with [ but instead #{pretty_lines[0]}" if pretty_lines[0] != "[\n"
161+
raise "impossible, array doesn't end with ] but instead #{pretty_lines[-1]}" if pretty_lines[-1] != "]"
162+
replace[arr, "[\n" + pretty_lines[1...-1].map { |l| "#{indent}#{l}" }.join + "#{indent}]#{',' if arr[-1] == ','}"]
163+
end
164+
}
165+
when :padded
166+
multi_line_arrays(contents, key).each { |indent, arr|
167+
elements = JSON.parse(arr)
168+
padded_elements = elements.map { |el| '%3d' % el }
169+
new_rows = padded_elements.each_slice(14).map { |row| indent + ' ' + row.join(', ') }
170+
# all lines but the last need a trailing comma
171+
replace[arr, "[\n#{new_rows.join(",\n")}\n#{indent}]"]
172+
}
173+
else
174+
raise "unknown #{exercise} #{key} #{format_type}"
175+
end
176+
}
177+
File.write(filename, contents)
178+
}

0 commit comments

Comments
 (0)