Skip to content

Commit d8fb729

Browse files
committed
Implement reference solution for PiecingItTogether exercise
1 parent 44292bc commit d8fb729

File tree

1 file changed

+309
-0
lines changed

1 file changed

+309
-0
lines changed
Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
import java.util.ArrayList;
2+
import java.util.List;
3+
import java.util.Objects;
4+
import java.util.Optional;
5+
6+
public class PiecingItTogether {
7+
private final double epsilon = 1e-6;
8+
9+
public PartialJigsawInformation getCompleteInformation(PartialJigsawInformation input) {
10+
Integer rows = input.getRows().orElse(null);
11+
Integer cols = input.getColumns().orElse(null);
12+
Integer pieces = input.getPieces().orElse(null);
13+
Integer border = input.getBorder().orElse(null);
14+
Integer inside = input.getInside().orElse(null);
15+
Double aspect = input.getAspectRatio().orElse(null);
16+
String format = input.getFormat().orElse(null);
17+
18+
// Final information to be compared with the original input to detect inconsistencies
19+
PartialJigsawInformation result = null;
20+
21+
// Check format and aspect ratio don't contradict each other or set aspect if format is square
22+
if (format != null && aspect != null) {
23+
if (!format.equals(getFormatFromAspect(aspect))) {
24+
throw new IllegalArgumentException("Contradictory data");
25+
}
26+
} else if ("square".equalsIgnoreCase(format)) {
27+
aspect = 1.0;
28+
}
29+
30+
// Derive any missing value among pieces, border, and inside
31+
if (pieces != null && border != null && inside != null) {
32+
if (pieces != border + inside) {
33+
throw new IllegalArgumentException("Contradictory data");
34+
}
35+
} else if (pieces != null && border != null) {
36+
inside = pieces - border;
37+
} else if (border != null && inside != null) {
38+
pieces = border + inside;
39+
} else if (inside != null && pieces != null) {
40+
border = pieces - inside;
41+
}
42+
43+
// Try to compute missing information using the numbers of rows and cols
44+
if (rows != null && cols != null) {
45+
result = fromRowsAndCols(rows, cols);
46+
} else if (rows != null) {
47+
Optional<Integer> possibleCols = calculateOtherSide(rows, true, pieces, border, inside, aspect);
48+
49+
if (possibleCols.isPresent()) {
50+
cols = possibleCols.get();
51+
result = fromRowsAndCols(rows, cols);
52+
}
53+
} else if (cols != null) {
54+
Optional<Integer> possibleRows = calculateOtherSide(cols, false, pieces, border, inside, aspect);
55+
56+
if (possibleRows.isPresent()) {
57+
rows = possibleRows.get();
58+
result = fromRowsAndCols(rows, cols);
59+
}
60+
}
61+
62+
if (result != null) {
63+
checkConsistencyOrThrow(result, input);
64+
return result;
65+
}
66+
67+
// Try to compute missing information using the value of aspect ratio
68+
if (aspect != null) {
69+
if (pieces != null) {
70+
double actualRows = Math.sqrt(pieces / aspect);
71+
rows = roundIfClose(actualRows);
72+
73+
double actualCols = aspect * rows;
74+
cols = roundIfClose(actualCols);
75+
76+
result = fromRowsAndCols(rows, cols);
77+
checkConsistencyOrThrow(result, input);
78+
return result;
79+
} else if (inside != null && inside == 0) {
80+
List<PartialJigsawInformation> validGuesses = new ArrayList<>();
81+
82+
for (int fixed : List.of(1, 2)) {
83+
tryGuessWithFixedSide(fixed, true, aspect, input).ifPresent(validGuesses::add); // rows = fixed
84+
tryGuessWithFixedSide(fixed, false, aspect, input).ifPresent(validGuesses::add); // cols = fixed
85+
}
86+
87+
if (validGuesses.size() == 1) {
88+
return validGuesses.getFirst();
89+
} else if (validGuesses.size() > 1) {
90+
throw new IllegalArgumentException("Insufficient data");
91+
} else {
92+
throw new IllegalArgumentException("Contradictory data");
93+
}
94+
}
95+
}
96+
97+
if (pieces == null && border == null && (inside == null || inside == 0)) {
98+
throw new IllegalArgumentException("Insufficient data");
99+
}
100+
101+
// Brute force as a last resort
102+
List<PartialJigsawInformation> validGuesses = new ArrayList<>();
103+
104+
// Brute-force using pieces
105+
if (pieces != null) {
106+
for (int r = 1; r <= pieces; r++) {
107+
if (pieces % r != 0) {
108+
continue; // cols must be integer
109+
}
110+
111+
int c = pieces / r;
112+
tryGuess(r, c, input).ifPresent(validGuesses::add);
113+
}
114+
} else if (inside != null) {
115+
for (int r = 3; r <= inside + 2; r++) {
116+
int innerRows = r - 2;
117+
if (inside % innerRows != 0) {
118+
continue;
119+
}
120+
121+
int innerCols = inside / innerRows;
122+
int c = innerCols + 2;
123+
124+
tryGuess(r, c, input).ifPresent(validGuesses::add);
125+
}
126+
} else {
127+
int max = Math.min(border, 1000);
128+
129+
for (int r = 1; r <= max; r++) {
130+
for (int c = 1; c <= max; c++) {
131+
tryGuess(r, c, input).ifPresent(validGuesses::add);
132+
}
133+
}
134+
}
135+
136+
if (validGuesses.size() == 1) {
137+
return validGuesses.getFirst();
138+
} else if (validGuesses.size() > 1) {
139+
throw new IllegalArgumentException("Insufficient data");
140+
} else {
141+
throw new IllegalArgumentException("Contradictory data");
142+
}
143+
}
144+
145+
private String getFormatFromAspect(double aspectRatio) {
146+
String format;
147+
148+
if (Math.abs(aspectRatio - 1.0) < epsilon) {
149+
format = "square";
150+
} else if (aspectRatio < 1.0) {
151+
format = "portrait";
152+
} else {
153+
format = "landscape";
154+
}
155+
156+
return format;
157+
}
158+
159+
private Integer roundIfClose(double value) {
160+
double rounded = Math.round(value);
161+
162+
if (Math.abs(value - rounded) < epsilon) {
163+
return (int) rounded;
164+
}
165+
166+
throw new IllegalArgumentException("Contradictory data");
167+
}
168+
169+
private int calculateOtherSideFromPieces(int knownSide, int pieces) {
170+
if (pieces <= 0 || pieces % knownSide != 0) {
171+
throw new IllegalArgumentException("Contradictory data");
172+
}
173+
174+
return pieces / knownSide;
175+
}
176+
177+
private int calculateOtherSideFromBorder(int knownSide, int border) {
178+
if (knownSide == 1) {
179+
return border;
180+
}
181+
182+
if (knownSide == border) {
183+
return 1;
184+
}
185+
186+
if ((border + 4 <= 2 * knownSide) || border % 2 != 0) {
187+
throw new IllegalArgumentException("Contradictory data");
188+
}
189+
190+
return ((border + 4) - 2 * knownSide) / 2;
191+
}
192+
193+
private int calculateOtherSideFromInside(int knownSide, int inside) {
194+
if (knownSide <= 2 || inside % (knownSide - 2) != 0) {
195+
throw new IllegalArgumentException("Contradictory data");
196+
}
197+
198+
return (inside / (knownSide - 2)) + 2;
199+
}
200+
201+
private Optional<Integer> calculateOtherSide(int knownSide, boolean isRowKnown, Integer pieces, Integer border, Integer inside, Double aspect) {
202+
if (pieces != null) {
203+
return Optional.of(calculateOtherSideFromPieces(knownSide, pieces));
204+
} else if (border != null) {
205+
return Optional.of(calculateOtherSideFromBorder(knownSide, border));
206+
} else if (aspect != null) {
207+
double raw = isRowKnown ? knownSide * aspect : knownSide / aspect;
208+
return Optional.of(roundIfClose(raw));
209+
} else if (inside != null) {
210+
if (inside == 0) {
211+
// When inside is zero and all other values (otherSide, pieces, border, aspect) are unknown,
212+
// it only tells us that either rows or cols is 1 or 2, but provides no way to infer the rest.
213+
// Since we can't make any further assumptions, we stop here.
214+
throw new IllegalArgumentException("Insufficient data");
215+
}
216+
217+
return Optional.of(calculateOtherSideFromInside(knownSide, inside));
218+
}
219+
220+
return Optional.empty();
221+
}
222+
223+
private PartialJigsawInformation fromRowsAndCols(int rows, int cols) {
224+
int pieces = rows * cols;
225+
int border = (rows == 1 || cols == 1) ? pieces : 2 * (rows + cols - 2);
226+
int inside = pieces - border;
227+
double aspect = (double) cols / rows;
228+
String format = getFormatFromAspect(aspect);
229+
230+
return new PartialJigsawInformation(pieces, border, inside, rows, cols, aspect, format);
231+
}
232+
233+
/**
234+
* Verifies that all known values in the input match those in the computed result.
235+
* If any known value in the input contradicts the corresponding computed value,
236+
* an IllegalArgumentException is thrown.
237+
*
238+
* @param computed the fully inferred jigsaw information
239+
* @param input the original partial input with possibly known values
240+
* @throws IllegalArgumentException if any known value in the input conflicts with the computed result
241+
*/
242+
public void checkConsistencyOrThrow(PartialJigsawInformation computed, PartialJigsawInformation input) {
243+
if (!valuesMatch(computed.getPieces(), input.getPieces()) || !valuesMatch(computed.getBorder(), input.getBorder()) || !valuesMatch(computed.getInside(), input.getInside()) || !valuesMatch(computed.getRows(), input.getRows()) || !valuesMatch(computed.getColumns(), input.getColumns()) || !valuesMatch(computed.getAspectRatio(), input.getAspectRatio()) || !valuesMatch(computed.getFormat(), input.getFormat())) {
244+
throw new IllegalArgumentException("Contradictory data");
245+
}
246+
}
247+
248+
/**
249+
* Attempts to compute a valid jigsaw configuration by fixing one dimension
250+
* (either rows or columns) and inferring the other using the aspect ratio.
251+
*
252+
* @param fixed the known size of one side (either rows or columns)
253+
* @param isRowFixed true if the fixed value represents rows, false if columns
254+
* @param aspect the desired aspect ratio (cols / rows)
255+
* @param input the original input to check for consistency
256+
* @return an Optional containing a valid inferred configuration, or empty if the guess is invalid
257+
*/
258+
private Optional<PartialJigsawInformation> tryGuessWithFixedSide(int fixed, boolean isRowFixed, double aspect, PartialJigsawInformation input) {
259+
try {
260+
int other = isRowFixed ? roundIfClose(fixed * aspect) : roundIfClose(fixed / aspect);
261+
262+
int rows = isRowFixed ? fixed : other;
263+
int cols = isRowFixed ? other : fixed;
264+
265+
PartialJigsawInformation guess = fromRowsAndCols(rows, cols);
266+
checkConsistencyOrThrow(guess, input);
267+
return Optional.of(guess);
268+
} catch (IllegalArgumentException ignored) {
269+
return Optional.empty();
270+
}
271+
}
272+
273+
/**
274+
* Attempts to construct a jigsaw configuration using the specified number of rows and columns.
275+
* Returns a valid result only if the configuration is consistent with the input.
276+
*
277+
* @param rows number of rows to try
278+
* @param cols number of columns to try
279+
* @param input the original input to check for consistency
280+
* @return an Optional containing a valid configuration, or empty if inconsistent
281+
*/
282+
private Optional<PartialJigsawInformation> tryGuess(int rows, int cols, PartialJigsawInformation input) {
283+
try {
284+
PartialJigsawInformation guess = fromRowsAndCols(rows, cols);
285+
checkConsistencyOrThrow(guess, input);
286+
return Optional.of(guess);
287+
} catch (IllegalArgumentException ignored) {
288+
return Optional.empty();
289+
}
290+
}
291+
292+
/**
293+
* Compares two optional values for equality.
294+
* Returns true if either value is absent, or if both are present and equal.
295+
* Used to check consistency between inferred and input data.
296+
*
297+
* @param a the first optional value
298+
* @param b the second optional value
299+
* @param <T> the type of the contained values
300+
* @return true if the values are consistent (both missing or equal if present), false otherwise
301+
*/
302+
private <T> boolean valuesMatch(Optional<T> a, Optional<T> b) {
303+
if (a.isPresent() && b.isPresent()) {
304+
return Objects.equals(a.get(), b.get());
305+
}
306+
307+
return true;
308+
}
309+
}

0 commit comments

Comments
 (0)