Skip to content

Commit fac4620

Browse files
Add piecing it together practice exercise (#2961)
1 parent fe71fa3 commit fac4620

File tree

16 files changed

+1072
-0
lines changed

16 files changed

+1072
-0
lines changed

config.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1307,6 +1307,18 @@
13071307
],
13081308
"difficulty": 6
13091309
},
1310+
{
1311+
"slug": "piecing-it-together",
1312+
"name": "Piecing It Together",
1313+
"uuid": "be303729-ad8a-4f4c-a235-6828a6734f05",
1314+
"practices": [],
1315+
"prerequisites": [
1316+
"exceptions",
1317+
"for-loops",
1318+
"if-else-statements"
1319+
],
1320+
"difficulty": 6
1321+
},
13101322
{
13111323
"slug": "queen-attack",
13121324
"name": "Queen Attack",
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Instructions
2+
3+
Given partial information about a jigsaw puzzle, add the missing pieces.
4+
5+
If the information is insufficient to complete the details, or if given parts are in contradiction, the user should be notified.
6+
7+
The full information about the jigsaw puzzle contains the following parts:
8+
9+
- `pieces`: Total number of pieces
10+
- `border`: Number of border pieces
11+
- `inside`: Number of inside (non-border) pieces
12+
- `rows`: Number of rows
13+
- `columns`: Number of columns
14+
- `aspectRatio`: Aspect ratio of columns to rows
15+
- `format`: Puzzle format, which can be `portrait`, `square`, or `landscape`
16+
17+
For this exercise, you may assume square pieces, so that the format can be derived from the aspect ratio:
18+
19+
- If the aspect ratio is less than 1, it's `portrait`
20+
- If it is equal to 1, it's `square`
21+
- If it is greater than 1, it's `landscape`
22+
23+
## Three examples
24+
25+
### Portrait
26+
27+
A portrait jigsaw puzzle with 6 pieces, all of which are border pieces and none are inside pieces. It has 3 rows and 2 columns. The aspect ratio is 1.5 (3/2).
28+
29+
![A 2 by 3 jigsaw puzzle](https://assets.exercism.org/images/exercises/piecing-it-together/jigsaw-puzzle-2x3.svg)
30+
31+
### Square
32+
33+
A square jigsaw puzzle with 9 pieces, all of which are border pieces except for the one in the center, which is an inside piece. It has 3 rows and 3 columns. The aspect ratio is 1 (3/3).
34+
35+
![A 3 by 3 jigsaw puzzle](https://assets.exercism.org/images/exercises/piecing-it-together/jigsaw-puzzle-3x3.svg)
36+
37+
### Landscape
38+
39+
A landscape jigsaw puzzle with 12 pieces, 10 of which are border pieces and 2 are inside pieces. It has 3 rows and 4 columns. The aspect ratio is 1.333333... (4/3).
40+
41+
![A 4 by 3 jigsaw puzzle](https://assets.exercism.org/images/exercises/piecing-it-together/jigsaw-puzzle-4x3.svg)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Introduction
2+
3+
Your best friend has started collecting jigsaw puzzles and wants to build a detailed catalog of their collection — recording information such as the total number of pieces, the number of rows and columns, the number of border and inside pieces, aspect ratio, and puzzle format.
4+
Even with your powers combined, it takes multiple hours to solve a single jigsaw puzzle with one thousand pieces — and then you still need to count the rows and columns and calculate the remaining numbers manually.
5+
The even larger puzzles with thousands of pieces look quite daunting now.
6+
"There has to be a better way!" you exclaim and sit down in front of your computer to solve the problem.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"authors": [
3+
"zamora-carlos"
4+
],
5+
"files": {
6+
"solution": [
7+
"src/main/java/PiecingItTogether.java"
8+
],
9+
"test": [
10+
"src/test/java/PiecingItTogetherTest.java"
11+
],
12+
"example": [
13+
".meta/src/reference/java/PiecingItTogether.java"
14+
],
15+
"editor": [
16+
"src/main/java/JigsawInfo.java"
17+
]
18+
},
19+
"blurb": "Fill in missing jigsaw puzzle details from partial data",
20+
"source": "atk just started another 1000-pieces jigsaw puzzle when this idea hit him",
21+
"source_url": "https://github.com/exercism/problem-specifications/pull/2554"
22+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import java.util.Objects;
2+
import java.util.Optional;
3+
import java.util.OptionalDouble;
4+
import java.util.OptionalInt;
5+
6+
/**
7+
* Represents partial or complete information about a jigsaw puzzle,
8+
*
9+
* NOTE: There is no need to change this file and is treated as read only by the Exercism test runners.
10+
*/
11+
public class JigsawInfo {
12+
private final OptionalInt pieces;
13+
private final OptionalInt border;
14+
private final OptionalInt inside;
15+
private final OptionalInt rows;
16+
private final OptionalInt columns;
17+
private final OptionalDouble aspectRatio;
18+
private final Optional<String> format;
19+
20+
private JigsawInfo(Builder builder) {
21+
this.pieces = builder.pieces;
22+
this.border = builder.border;
23+
this.inside = builder.inside;
24+
this.rows = builder.rows;
25+
this.columns = builder.columns;
26+
this.aspectRatio = builder.aspectRatio;
27+
this.format = builder.format;
28+
}
29+
30+
public static class Builder {
31+
private OptionalInt pieces = OptionalInt.empty();
32+
private OptionalInt border = OptionalInt.empty();
33+
private OptionalInt inside = OptionalInt.empty();
34+
private OptionalInt rows = OptionalInt.empty();
35+
private OptionalInt columns = OptionalInt.empty();
36+
private OptionalDouble aspectRatio = OptionalDouble.empty();
37+
private Optional<String> format = Optional.empty();
38+
39+
public Builder pieces(int pieces) {
40+
this.pieces = OptionalInt.of(pieces);
41+
return this;
42+
}
43+
44+
public Builder border(int border) {
45+
this.border = OptionalInt.of(border);
46+
return this;
47+
}
48+
49+
public Builder inside(int inside) {
50+
this.inside = OptionalInt.of(inside);
51+
return this;
52+
}
53+
54+
public Builder rows(int rows) {
55+
this.rows = OptionalInt.of(rows);
56+
return this;
57+
}
58+
59+
public Builder columns(int columns) {
60+
this.columns = OptionalInt.of(columns);
61+
return this;
62+
}
63+
64+
public Builder aspectRatio(double aspectRatio) {
65+
this.aspectRatio = OptionalDouble.of(aspectRatio);
66+
return this;
67+
}
68+
69+
public Builder format(String format) {
70+
this.format = Optional.of(format);
71+
return this;
72+
}
73+
74+
public JigsawInfo build() {
75+
return new JigsawInfo(this);
76+
}
77+
}
78+
79+
public OptionalInt getPieces() {
80+
return pieces;
81+
}
82+
83+
public OptionalInt getBorder() {
84+
return border;
85+
}
86+
87+
public OptionalInt getInside() {
88+
return inside;
89+
}
90+
91+
public OptionalInt getRows() {
92+
return rows;
93+
}
94+
95+
public OptionalInt getColumns() {
96+
return columns;
97+
}
98+
99+
public OptionalDouble getAspectRatio() {
100+
return aspectRatio;
101+
}
102+
103+
public Optional<String> getFormat() {
104+
return format;
105+
}
106+
107+
@Override
108+
public boolean equals(Object o) {
109+
if (o == null || getClass() != o.getClass()) {
110+
return false;
111+
}
112+
113+
JigsawInfo that = (JigsawInfo) o;
114+
return Objects.equals(pieces, that.pieces)
115+
&& Objects.equals(border, that.border)
116+
&& Objects.equals(inside, that.inside)
117+
&& Objects.equals(rows, that.rows)
118+
&& Objects.equals(columns, that.columns)
119+
&& Objects.equals(aspectRatio, that.aspectRatio)
120+
&& Objects.equals(format, that.format);
121+
}
122+
123+
@Override
124+
public int hashCode() {
125+
return Objects.hash(pieces, border, inside, rows, columns, aspectRatio, format);
126+
}
127+
128+
@Override
129+
public String toString() {
130+
return "JigsawInfo{" +
131+
"pieces=" + pieces +
132+
", border=" + border +
133+
", inside=" + inside +
134+
", rows=" + rows +
135+
", columns=" + columns +
136+
", aspectRatio=" + aspectRatio +
137+
", format=" + format +
138+
'}';
139+
}
140+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import java.util.*;
2+
3+
public class PiecingItTogether {
4+
private static final double DOUBLE_EQUALITY_TOLERANCE = 1e-9;
5+
private static final int MAX_DIMENSION = 1000;
6+
7+
public static JigsawInfo getCompleteInformation(JigsawInfo input) {
8+
List<JigsawInfo> validGuesses = new ArrayList<>();
9+
10+
if (input.getPieces().isPresent()) {
11+
// If pieces is known, we only test divisors of pieces
12+
int pieces = input.getPieces().getAsInt();
13+
for (int rows = 1; rows <= pieces; rows++) {
14+
if (pieces % rows != 0) {
15+
continue;
16+
}
17+
int columns = pieces / rows;
18+
createValidJigsaw(rows, columns, input).ifPresent(validGuesses::add);
19+
}
20+
} else if (input.getInside().isPresent() && input.getInside().getAsInt() > 0) {
21+
// If inside pieces is non-zero, we only test divisors of inside
22+
int inside = input.getInside().getAsInt();
23+
for (int innerRows = 1; innerRows <= inside; innerRows++) {
24+
if (inside % innerRows != 0) {
25+
continue;
26+
}
27+
int innerColumns = inside / innerRows;
28+
createValidJigsaw(innerRows + 2, innerColumns + 2, input).ifPresent(validGuesses::add);
29+
}
30+
} else {
31+
// Brute force using border constraint if available
32+
int maxDimension = input.getBorder().isPresent()
33+
? Math.min(input.getBorder().getAsInt(), MAX_DIMENSION)
34+
: MAX_DIMENSION;
35+
36+
for (int rows = 1; rows <= maxDimension; rows++) {
37+
for (int columns = 1; columns <= maxDimension; columns++) {
38+
createValidJigsaw(rows, columns, input).ifPresent(validGuesses::add);
39+
}
40+
}
41+
}
42+
43+
if (validGuesses.size() == 1) {
44+
return validGuesses.get(0);
45+
} else if (validGuesses.size() > 1) {
46+
throw new IllegalArgumentException("Insufficient data");
47+
} else {
48+
throw new IllegalArgumentException("Contradictory data");
49+
}
50+
}
51+
52+
private static String getFormatFromAspect(double aspectRatio) {
53+
if (Math.abs(aspectRatio - 1.0) < DOUBLE_EQUALITY_TOLERANCE) {
54+
return "square";
55+
} else if (aspectRatio < 1.0) {
56+
return "portrait";
57+
} else {
58+
return "landscape";
59+
}
60+
}
61+
62+
private static JigsawInfo fromRowsAndCols(int rows, int columns) {
63+
int pieces = rows * columns;
64+
int border = (rows == 1 || columns == 1) ? pieces : 2 * (rows + columns - 2);
65+
int inside = pieces - border;
66+
double aspectRatio = (double) columns / rows;
67+
String format = getFormatFromAspect(aspectRatio);
68+
69+
return new JigsawInfo.Builder()
70+
.pieces(pieces)
71+
.border(border)
72+
.inside(inside)
73+
.rows(rows)
74+
.columns(columns)
75+
.aspectRatio(aspectRatio)
76+
.format(format)
77+
.build();
78+
}
79+
80+
/**
81+
* Verifies that all known values in the input match those in the computed result.
82+
* Returns false if any known value conflicts, true if all values are consistent.
83+
*
84+
* @param computed the fully inferred jigsaw information
85+
* @param input the original partial input with possibly empty values
86+
* @return true if all values are consistent, false if any conflict exists
87+
*/
88+
private static boolean isConsistent(JigsawInfo computed, JigsawInfo input) {
89+
return valuesMatch(computed.getPieces(), input.getPieces()) &&
90+
valuesMatch(computed.getBorder(), input.getBorder()) &&
91+
valuesMatch(computed.getInside(), input.getInside()) &&
92+
valuesMatch(computed.getRows(), input.getRows()) &&
93+
valuesMatch(computed.getColumns(), input.getColumns()) &&
94+
valuesMatch(computed.getAspectRatio(), input.getAspectRatio()) &&
95+
valuesMatch(computed.getFormat(), input.getFormat());
96+
}
97+
98+
/**
99+
* Attempts to construct a valid jigsaw configuration using the specified number of rows and columns.
100+
* Returns a valid result only if the configuration is consistent with the input.
101+
*
102+
* @param rows number of rows to try
103+
* @param columns number of columns to try
104+
* @param input the original input to check for consistency
105+
* @return an Optional containing a valid configuration, or empty if inconsistent
106+
*/
107+
private static Optional<JigsawInfo> createValidJigsaw(int rows, int columns, JigsawInfo input) {
108+
JigsawInfo candidate = fromRowsAndCols(rows, columns);
109+
return isConsistent(candidate, input) ? Optional.of(candidate) : Optional.empty();
110+
}
111+
112+
private static <T> boolean valuesMatch(Optional<T> a, Optional<T> b) {
113+
if (a.isPresent() && b.isPresent()) {
114+
return Objects.equals(a.get(), b.get());
115+
}
116+
117+
return true;
118+
}
119+
120+
private static boolean valuesMatch(OptionalInt a, OptionalInt b) {
121+
if (a.isPresent() && b.isPresent()) {
122+
return a.getAsInt() == b.getAsInt();
123+
}
124+
125+
return true;
126+
}
127+
128+
private static boolean valuesMatch(OptionalDouble a, OptionalDouble b) {
129+
if (a.isPresent() && b.isPresent()) {
130+
return Double.compare(a.getAsDouble(), b.getAsDouble()) == 0;
131+
}
132+
133+
return true;
134+
}
135+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# This is an auto-generated file.
2+
#
3+
# Regenerating this file via `configlet sync` will:
4+
# - Recreate every `description` key/value pair
5+
# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications
6+
# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion)
7+
# - Preserve any other key/value pair
8+
#
9+
# As user-added comments (using the # character) will be removed when this file
10+
# is regenerated, comments can be added via a `comment` key.
11+
12+
[ad626f23-09a2-4f5f-ba22-eec0671fa2a9]
13+
description = "1000 pieces puzzle with 1.6 aspect ratio"
14+
15+
[3e0c5919-3561-42f5-b9ed-26d70c20214e]
16+
description = "square puzzle with 32 rows"
17+
18+
[1126f160-b094-4dc2-bf37-13e36e394867]
19+
description = "400 pieces square puzzle with only inside pieces and aspect ratio"
20+
21+
[a9743178-5642-4cc0-8fdb-00d6b031c3a0]
22+
description = "1500 pieces landscape puzzle with 30 rows and 1.6 aspect ratio"
23+
24+
[f6378369-989c-497f-a6e2-f30b1fa76cba]
25+
description = "300 pieces portrait puzzle with 70 border pieces"
26+
27+
[f53f82ba-5663-4c7e-9e86-57fdbb3e53d2]
28+
description = "puzzle with insufficient data"
29+
30+
[a3d5c31a-cc74-44bf-b4fc-9e4d65f1ac1a]
31+
description = "puzzle with contradictory data"

0 commit comments

Comments
 (0)