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