Skip to content

Commit 34649f0

Browse files
authored
Merge pull request #16 from rcsb/builder
Builder Improvements: Add `addMasked(T[], ValueKind[])` and `addAll(Iterable<T>)`
2 parents da78ec1 + 5f5c020 commit 34649f0

21 files changed

+317
-55
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ This project uses semantic versioning. Furthermore, this project provides code t
1313
* `diffrn_radiation_wavelength_id` removed
1414
* `geom_bond_distance_min` renamed to `geom_min_bond_distance_cutoff`
1515

16+
### General
17+
* improved builder ergonomics
18+
* added overloads that make it easier to build columns with missing values
19+
* validate that all columns in a category are equal in size
20+
1621
ciftools-java 7.0.2 - September 2025
1722
-------------
1823
### Bug fixes

pom.xml

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,15 @@
5858
<dependency>
5959
<groupId>org.junit.jupiter</groupId>
6060
<artifactId>junit-jupiter-api</artifactId>
61-
<version>5.14.0</version>
61+
<version>6.0.2</version>
62+
<scope>test</scope>
63+
</dependency>
64+
<dependency>
65+
<groupId>org.junit.jupiter</groupId>
66+
<artifactId>junit-jupiter-engine</artifactId>
67+
<version>6.0.2</version>
6268
<scope>test</scope>
6369
</dependency>
64-
6570
</dependencies>
6671

6772
<properties>
@@ -85,6 +90,9 @@
8590
<groupId>org.apache.maven.plugins</groupId>
8691
<artifactId>maven-surefire-plugin</artifactId>
8792
<version>3.5.4</version>
93+
<configuration>
94+
<failIfNoTests>true</failIfNoTests>
95+
</configuration>
8896
</plugin>
8997
</plugins>
9098
</build>
@@ -98,7 +106,7 @@
98106
<plugin>
99107
<groupId>org.sonatype.central</groupId>
100108
<artifactId>central-publishing-maven-plugin</artifactId>
101-
<version>0.9.0</version>
109+
<version>0.10.0</version>
102110
<extensions>true</extensions>
103111
<configuration>
104112
<publishingServerId>central</publishingServerId>
@@ -137,7 +145,7 @@
137145
<plugin>
138146
<groupId>org.apache.maven.plugins</groupId>
139147
<artifactId>maven-source-plugin</artifactId>
140-
<version>3.3.1</version>
148+
<version>3.4.0</version>
141149
<executions>
142150
<execution>
143151
<id>attach-sources</id>

src/main/java/org/rcsb/cif/model/CategoryBuilder.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,9 @@ static <C extends Column<?>> C createColumnText(String columnName, List<?> value
112112
startToken[i] = builder.length();
113113
String value = String.valueOf(values.get(i));
114114
if (mask.get(i) == ValueKind.NOT_PRESENT) {
115-
value = ".";
115+
value = ValueKind.CIF_NOT_PRESENT;
116116
} else if (mask.get(i) == ValueKind.UNKNOWN) {
117-
value = "?";
117+
value = ValueKind.CIF_UNKNOWN;
118118
}
119119
builder.append(value);
120120
endToken[i] = builder.length();

src/main/java/org/rcsb/cif/model/FloatColumn.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ default DoubleStream values() {
2929
* @return a double
3030
*/
3131
static double parseFloat(String text) {
32-
if (text.isEmpty() || ".".equals(text) || "?".equals(text)) {
32+
if (text.isEmpty() || ValueKind.isValueKindToken(text)) {
3333
return 0;
3434
}
3535
// some formats specify uncertain decimal places like: 0.00012(3) - ignore them (in agreement with Mol*)

src/main/java/org/rcsb/cif/model/FloatColumnBuilder.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.rcsb.cif.model;
22

33
import java.util.List;
4+
import java.util.Objects;
45

56
/**
67
* A builder instance for {@link FloatColumn} instances.
@@ -27,4 +28,60 @@ public interface FloatColumnBuilder<P extends CategoryBuilder<PP, PPP>, PP exten
2728
* @return this builder instance
2829
*/
2930
FloatColumnBuilder<P, PP, PPP> add(double... values);
31+
32+
/**
33+
* Add new values with fine-grained control.
34+
* <p>
35+
* For {@link ValueKind#PRESENT}, the corresponding entry from {@code values} is appended.
36+
* For {@link ValueKind#NOT_PRESENT} and {@link ValueKind#UNKNOWN}, this method delegates to
37+
* {@link #markNextNotPresent()} and {@link #markNextUnknown()} respectively.
38+
* </p>
39+
* @param values array of double values
40+
* @param mask array of {@link ValueKind}, must be the same length as {@code values}
41+
* @return this builder instance
42+
* @throws IllegalArgumentException if arrays differ in size
43+
* @throws NullPointerException if {@code values}, {@code mask}, or any {@code mask[i]} is null
44+
*/
45+
default FloatColumnBuilder<P,PP,PPP> addMasked(double[] values, ValueKind[] mask) {
46+
Objects.requireNonNull(values, "values");
47+
Objects.requireNonNull(mask, "mask");
48+
if (values.length != mask.length) {
49+
throw new IllegalArgumentException("values.length (" + values.length + ") must equal mask.length (" + mask.length + ")");
50+
}
51+
52+
for (int i = 0; i < values.length; i++) {
53+
ValueKind k = Objects.requireNonNull(mask[i], "mask[" + i + "]");
54+
switch (k) {
55+
case PRESENT:
56+
add(values[i]);
57+
break;
58+
case NOT_PRESENT:
59+
markNextNotPresent();
60+
break;
61+
case UNKNOWN:
62+
markNextUnknown();
63+
break;
64+
default:
65+
throw new IllegalStateException("Unhandled ValueKind: " + k);
66+
}
67+
}
68+
return this;
69+
}
70+
71+
/**
72+
* Add values from an Iterable.
73+
* @param values Double values, null is mapped to ValueKind.NOT_PRESENT (".")
74+
* @return this builder instance
75+
*/
76+
default FloatColumnBuilder<P,PP,PPP> addNullable(Iterable<Double> values) {
77+
Objects.requireNonNull(values, "values");
78+
for (Double v : values) {
79+
if (v == null) {
80+
markNextNotPresent();
81+
} else {
82+
add(v);
83+
}
84+
}
85+
return this;
86+
}
3087
}

src/main/java/org/rcsb/cif/model/IntColumn.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ default IntStream values() {
2929
* @return an int
3030
*/
3131
static int parseInt(String text) {
32-
if (text.isEmpty() || ".".equals(text) || "?".equals(text)) {
32+
if (text.isEmpty() || ValueKind.isValueKindToken(text)) {
3333
return 0;
3434
}
3535
// some floats may omit decimal places and can be parsed as int: 88. - ignore the dot (in agreement with Mol*)

src/main/java/org/rcsb/cif/model/IntColumnBuilder.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.rcsb.cif.model;
22

33
import java.util.List;
4+
import java.util.Objects;
45

56
/**
67
* A builder instance for {@link IntColumn} instances.
@@ -27,4 +28,60 @@ public interface IntColumnBuilder<P extends CategoryBuilder<PP, PPP>, PP extends
2728
* @return this builder instance
2829
*/
2930
IntColumnBuilder<P, PP, PPP> add(int... values);
31+
32+
/**
33+
* Add new values with fine-grained control.
34+
* <p>
35+
* For {@link ValueKind#PRESENT}, the corresponding entry from {@code values} is appended.
36+
* For {@link ValueKind#NOT_PRESENT} and {@link ValueKind#UNKNOWN}, this method delegates to
37+
* {@link #markNextNotPresent()} and {@link #markNextUnknown()} respectively.
38+
* </p>
39+
* @param values array of int values
40+
* @param mask array of {@link ValueKind}, must be the same length as {@code values}
41+
* @return this builder instance
42+
* @throws IllegalArgumentException if arrays differ in size
43+
* @throws NullPointerException if {@code values}, {@code mask}, or any {@code mask[i]} is null
44+
*/
45+
default IntColumnBuilder<P,PP,PPP> addMasked(int[] values, ValueKind[] mask) {
46+
Objects.requireNonNull(values, "values");
47+
Objects.requireNonNull(mask, "mask");
48+
if (values.length != mask.length) {
49+
throw new IllegalArgumentException("values.length (" + values.length + ") must equal mask.length (" + mask.length + ")");
50+
}
51+
52+
for (int i = 0; i < values.length; i++) {
53+
ValueKind k = Objects.requireNonNull(mask[i], "mask[" + i + "]");
54+
switch (k) {
55+
case PRESENT:
56+
add(values[i]);
57+
break;
58+
case NOT_PRESENT:
59+
markNextNotPresent();
60+
break;
61+
case UNKNOWN:
62+
markNextUnknown();
63+
break;
64+
default:
65+
throw new IllegalStateException("Unhandled ValueKind: " + k);
66+
}
67+
}
68+
return this;
69+
}
70+
71+
/**
72+
* Add values from an Iterable.
73+
* @param values Integer values, null is mapped to ValueKind.NOT_PRESENT (".")
74+
* @return this builder instance
75+
*/
76+
default IntColumnBuilder<P,PP,PPP> addNullable(Iterable<Integer> values) {
77+
Objects.requireNonNull(values, "values");
78+
for (Integer v : values) {
79+
if (v == null) {
80+
markNextNotPresent();
81+
} else {
82+
add(v);
83+
}
84+
}
85+
return this;
86+
}
3087
}

src/main/java/org/rcsb/cif/model/StrColumnBuilder.java

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.rcsb.cif.model;
22

33
import java.util.List;
4+
import java.util.Objects;
45

56
/**
67
* A builder instance for {@link StrColumn} instances.
@@ -27,4 +28,72 @@ public interface StrColumnBuilder<P extends CategoryBuilder<PP, PPP>, PP extends
2728
* @return this builder instance
2829
*/
2930
StrColumnBuilder<P, PP, PPP> add(String... values);
31+
32+
/**
33+
* Add new values with fine-grained control.
34+
* <p>
35+
* For {@link ValueKind#PRESENT}, the corresponding entry from {@code values} is appended.
36+
* For {@link ValueKind#NOT_PRESENT} and {@link ValueKind#UNKNOWN}, this method delegates to
37+
* {@link #markNextNotPresent()} and {@link #markNextUnknown()} respectively.
38+
* </p>
39+
* @param values array of String values
40+
* @param mask array of {@link ValueKind}, must be the same length as {@code values}
41+
* @return this builder instance
42+
* @throws IllegalArgumentException if arrays differ in size
43+
* @throws NullPointerException if {@code values}, {@code mask}, or any {@code mask[i]} is null
44+
*/
45+
default StrColumnBuilder<P,PP,PPP> addMasked(String[] values, ValueKind[] mask) {
46+
Objects.requireNonNull(values, "values");
47+
Objects.requireNonNull(mask, "mask");
48+
if (values.length != mask.length) {
49+
throw new IllegalArgumentException("values.length (" + values.length + ") must equal mask.length (" + mask.length + ")");
50+
}
51+
52+
for (int i = 0; i < values.length; i++) {
53+
ValueKind k = Objects.requireNonNull(mask[i], "mask[" + i + "]");
54+
if (k == ValueKind.PRESENT && (values[i] == null || ValueKind.isValueKindToken(values[i]))) {
55+
throw new IllegalArgumentException("PRESENT value must not be null, '.' or '?': values[" + i + "]");
56+
}
57+
switch (k) {
58+
case PRESENT:
59+
add(values[i]);
60+
break;
61+
case NOT_PRESENT:
62+
markNextNotPresent();
63+
break;
64+
case UNKNOWN:
65+
markNextUnknown();
66+
break;
67+
default:
68+
throw new IllegalStateException("Unhandled ValueKind: " + k);
69+
}
70+
}
71+
return this;
72+
}
73+
74+
/**
75+
* Add values from an Iterable.
76+
* @param values String values, null is mapped to NOT_PRESENT ("."); "." and "?" are interpreted as CIF tokens.
77+
* @return this builder instance
78+
*/
79+
default StrColumnBuilder<P,PP,PPP> addNullable(Iterable<String> values) {
80+
Objects.requireNonNull(values, "values");
81+
for (String v : values) {
82+
ValueKind valueKind = ValueKind.fromCifToken(v);
83+
switch (valueKind) {
84+
case PRESENT:
85+
add(v);
86+
break;
87+
case NOT_PRESENT:
88+
markNextNotPresent();
89+
break;
90+
case UNKNOWN:
91+
markNextUnknown();
92+
break;
93+
default:
94+
throw new IllegalStateException("Unhandled ValueKind: " + valueKind);
95+
}
96+
}
97+
return this;
98+
}
3099
}

src/main/java/org/rcsb/cif/model/ValueKind.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,28 @@ public enum ValueKind {
1616
/**
1717
* The value is unknown - <code>?</code> in CIF. String values will be empty, number values will be 0.
1818
*/
19-
UNKNOWN
19+
UNKNOWN;
20+
21+
public static final String CIF_NOT_PRESENT = ".";
22+
public static final String CIF_UNKNOWN = "?";
23+
24+
/**
25+
* Checks whether a String matches "?" or ".", sequences with special meaning in CIF.
26+
* @param s payload to evaluate
27+
* @return true if this String indicates missing or undefined values
28+
*/
29+
public static boolean isValueKindToken(String s) {
30+
return CIF_NOT_PRESENT.equals(s) || CIF_UNKNOWN.equals(s);
31+
}
32+
33+
/**
34+
* Transforms a String into a ValueKind.
35+
* @param s payload to evaluate
36+
* @return appropriate ValueKind for "?" and ".", otherwise marked as PRESENT
37+
*/
38+
public static ValueKind fromCifToken(String s) {
39+
if (s == null || CIF_NOT_PRESENT.equals(s)) return NOT_PRESENT;
40+
if (CIF_UNKNOWN.equals(s)) return UNKNOWN;
41+
return PRESENT;
42+
}
2043
}

src/main/java/org/rcsb/cif/model/binary/BinaryStrColumn.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.rcsb.cif.model.binary;
22

33
import org.rcsb.cif.model.StrColumn;
4+
import org.rcsb.cif.model.ValueKind;
45

56
public class BinaryStrColumn extends BinaryColumn<String[]> implements StrColumn {
67
private final String[] data;
@@ -21,7 +22,7 @@ public String getStringData(int row) {
2122
}
2223

2324
private String honorValueKind(String value) {
24-
return (".".equals(value) || "?".equals(value)) ? "" : value;
25+
return ValueKind.isValueKindToken(value) ? "" : value;
2526
}
2627

2728
@Override

0 commit comments

Comments
 (0)