, PP exten * @return this builder instance */ FloatColumnBuilder
add(double... values); + + /** + * Add new values with fine-grained control. + *
+ * For {@link ValueKind#PRESENT}, the corresponding entry from {@code values} is appended. + * For {@link ValueKind#NOT_PRESENT} and {@link ValueKind#UNKNOWN}, this method delegates to + * {@link #markNextNotPresent()} and {@link #markNextUnknown()} respectively. + *
+ * @param values array of double values + * @param mask array of {@link ValueKind}, must be the same length as {@code values} + * @return this builder instance + * @throws IllegalArgumentException if arrays differ in size + * @throws NullPointerException if {@code values}, {@code mask}, or any {@code mask[i]} is null + */ + default FloatColumnBuilderaddMasked(double[] values, ValueKind[] mask) { + Objects.requireNonNull(values, "values"); + Objects.requireNonNull(mask, "mask"); + if (values.length != mask.length) { + throw new IllegalArgumentException("values.length (" + values.length + ") must equal mask.length (" + mask.length + ")"); + } + + for (int i = 0; i < values.length; i++) { + ValueKind k = Objects.requireNonNull(mask[i], "mask[" + i + "]"); + switch (k) { + case PRESENT: + add(values[i]); + break; + case NOT_PRESENT: + markNextNotPresent(); + break; + case UNKNOWN: + markNextUnknown(); + break; + default: + throw new IllegalStateException("Unhandled ValueKind: " + k); + } + } + return this; + } + + /** + * Add values from an Iterable. + * @param values Double values, null is mapped to ValueKind.NOT_PRESENT (".") + * @return this builder instance + */ + default FloatColumnBuilder
addNullable(Iterable , PP extends
* @return this builder instance
*/
IntColumnBuilder add(int... values);
+
+ /**
+ * Add new values with fine-grained control.
+ *
+ * For {@link ValueKind#PRESENT}, the corresponding entry from {@code values} is appended.
+ * For {@link ValueKind#NOT_PRESENT} and {@link ValueKind#UNKNOWN}, this method delegates to
+ * {@link #markNextNotPresent()} and {@link #markNextUnknown()} respectively.
+ * addMasked(int[] values, ValueKind[] mask) {
+ Objects.requireNonNull(values, "values");
+ Objects.requireNonNull(mask, "mask");
+ if (values.length != mask.length) {
+ throw new IllegalArgumentException("values.length (" + values.length + ") must equal mask.length (" + mask.length + ")");
+ }
+
+ for (int i = 0; i < values.length; i++) {
+ ValueKind k = Objects.requireNonNull(mask[i], "mask[" + i + "]");
+ switch (k) {
+ case PRESENT:
+ add(values[i]);
+ break;
+ case NOT_PRESENT:
+ markNextNotPresent();
+ break;
+ case UNKNOWN:
+ markNextUnknown();
+ break;
+ default:
+ throw new IllegalStateException("Unhandled ValueKind: " + k);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Add values from an Iterable.
+ * @param values Integer values, null is mapped to ValueKind.NOT_PRESENT (".")
+ * @return this builder instance
+ */
+ default IntColumnBuilder addNullable(Iterable , PP extends
* @return this builder instance
*/
StrColumnBuilder add(String... values);
+
+ /**
+ * Add new values with fine-grained control.
+ *
+ * For {@link ValueKind#PRESENT}, the corresponding entry from {@code values} is appended.
+ * For {@link ValueKind#NOT_PRESENT} and {@link ValueKind#UNKNOWN}, this method delegates to
+ * {@link #markNextNotPresent()} and {@link #markNextUnknown()} respectively.
+ * addMasked(String[] values, ValueKind[] mask) {
+ Objects.requireNonNull(values, "values");
+ Objects.requireNonNull(mask, "mask");
+ if (values.length != mask.length) {
+ throw new IllegalArgumentException("values.length (" + values.length + ") must equal mask.length (" + mask.length + ")");
+ }
+
+ for (int i = 0; i < values.length; i++) {
+ ValueKind k = Objects.requireNonNull(mask[i], "mask[" + i + "]");
+ if (k == ValueKind.PRESENT && (values[i] == null || ValueKind.isValueKindToken(values[i]))) {
+ throw new IllegalArgumentException("PRESENT value must not be null, '.' or '?': values[" + i + "]");
+ }
+ switch (k) {
+ case PRESENT:
+ add(values[i]);
+ break;
+ case NOT_PRESENT:
+ markNextNotPresent();
+ break;
+ case UNKNOWN:
+ markNextUnknown();
+ break;
+ default:
+ throw new IllegalStateException("Unhandled ValueKind: " + k);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Add values from an Iterable.
+ * @param values String values, null is mapped to NOT_PRESENT ("."); "." and "?" are interpreted as CIF tokens.
+ * @return this builder instance
+ */
+ default StrColumnBuilder addNullable(Iterable , PP extends CifFileB
protected final P parent;
private final List , P, PP>) child);
} else if (child instanceof FloatColumnBuilder) {
diff --git a/src/main/java/org/rcsb/cif/model/builder/ColumnBuilderImpl.java b/src/main/java/org/rcsb/cif/model/builder/ColumnBuilderImpl.java
index c34fc03ff..50473e470 100644
--- a/src/main/java/org/rcsb/cif/model/builder/ColumnBuilderImpl.java
+++ b/src/main/java/org/rcsb/cif/model/builder/ColumnBuilderImpl.java
@@ -12,7 +12,7 @@
public abstract class ColumnBuilderImpl , PP extends BlockBuilder {
private final String categoryName;
private final String columnName;
- final List , PP extends BlockBuilder implements FloatColumnBuilder {
- private final List markNextNotPresent() {
- values.add(0.0);
+ values.add(null);
mask.add(ValueKind.NOT_PRESENT);
return this;
}
@Override
public FloatColumnBuilder markNextUnknown() {
- values.add(0.0);
+ values.add(null);
mask.add(ValueKind.UNKNOWN);
return this;
}
@@ -48,9 +46,14 @@ public FloatColumn build() {
}
@Override
- public FloatColumnBuilder add(double... value) {
- DoubleStream.of(value).forEach(values::add);
- IntStream.range(0, value.length).mapToObj(i -> ValueKind.PRESENT).forEach(mask::add);
+ public FloatColumnBuilder add(double... values) {
+ this.values.ensureCapacity(this.values.size() + values.length);
+ this.mask.ensureCapacity(this.mask.size() + values.length);
+
+ for (double v : values) {
+ this.values.add(v);
+ this.mask.add(ValueKind.PRESENT);
+ }
return this;
}
diff --git a/src/main/java/org/rcsb/cif/model/builder/IntColumnBuilderImpl.java b/src/main/java/org/rcsb/cif/model/builder/IntColumnBuilderImpl.java
index 63a3097e8..5c62162e9 100644
--- a/src/main/java/org/rcsb/cif/model/builder/IntColumnBuilderImpl.java
+++ b/src/main/java/org/rcsb/cif/model/builder/IntColumnBuilderImpl.java
@@ -9,13 +9,12 @@
import java.util.ArrayList;
import java.util.List;
-import java.util.stream.IntStream;
import static org.rcsb.cif.model.CategoryBuilder.createColumnText;
public class IntColumnBuilderImpl , PP extends BlockBuilder implements IntColumnBuilder {
- private final List markNextNotPresent() {
- values.add(0);
+ values.add(null);
mask.add(ValueKind.NOT_PRESENT);
return this;
}
@Override
public IntColumnBuilder markNextUnknown() {
- values.add(0);
+ values.add(null);
mask.add(ValueKind.UNKNOWN);
return this;
}
@@ -48,8 +47,13 @@ public IntColumn build() {
@Override
public IntColumnBuilder add(int... values) {
- IntStream.of(values).forEach(this.values::add);
- IntStream.range(0, values.length).mapToObj(i -> ValueKind.PRESENT).forEach(mask::add);
+ this.values.ensureCapacity(this.values.size() + values.length);
+ this.mask.ensureCapacity(this.mask.size() + values.length);
+
+ for (int v : values) {
+ this.values.add(v);
+ this.mask.add(ValueKind.PRESENT);
+ }
return this;
}
diff --git a/src/main/java/org/rcsb/cif/model/builder/StrColumnBuilderImpl.java b/src/main/java/org/rcsb/cif/model/builder/StrColumnBuilderImpl.java
index 3772a19d3..fef75df02 100644
--- a/src/main/java/org/rcsb/cif/model/builder/StrColumnBuilderImpl.java
+++ b/src/main/java/org/rcsb/cif/model/builder/StrColumnBuilderImpl.java
@@ -14,7 +14,7 @@
public class StrColumnBuilderImpl , PP extends BlockBuilder implements StrColumnBuilder {
- private final List markNextNotPresent() {
- values.add(".");
+ values.add(null);
mask.add(ValueKind.NOT_PRESENT);
return this;
}
@Override
public StrColumnBuilder markNextUnknown() {
- values.add("?");
+ values.add(null);
mask.add(ValueKind.UNKNOWN);
return this;
}
@@ -45,16 +45,45 @@ public StrColumn build() {
return createColumnText(getColumnName(), values, mask, StrColumn.class);
}
+ /**
+ * Add one or more string values to this column.
+ *
+ * CIF has two special tokens for missing data: {@code "."} (not present) and {@code "?"}
+ * (unknown). This method treats those tokens (and {@code null}) as missingness indicators
+ * rather than literal payload:
+ *
+ * Note: this means you cannot write a literal value that is exactly {@code "."} or {@code "?"}
+ * via this overload. If you need explicit control over missingness vs. payload, prefer the
+ * forthcoming masked overload that accepts {@link ValueKind} alongside values (e.g. {@code addMasked(...)}).
+ *
+ * @param values string values to append; {@code null}, {@code "."}, and {@code "?"} are treated specially
+ * @return this builder instance
+ */
@Override
public StrColumnBuilder add(String... values) {
+ this.values.ensureCapacity(this.values.size() + values.length);
+ this.mask.ensureCapacity(this.mask.size() + values.length);
+
for (String s : values) {
- if (".".equals(s)) {
- markNextNotPresent();
- } else if ("?".equals(s)) {
- markNextUnknown();
- } else {
- this.values.add(s);
- mask.add(ValueKind.PRESENT);
+ ValueKind kind = ValueKind.fromCifToken(s);
+ switch (kind) {
+ case NOT_PRESENT:
+ markNextNotPresent();
+ break;
+ case UNKNOWN:
+ markNextUnknown();
+ break;
+ case PRESENT:
+ this.values.add(s);
+ this.mask.add(ValueKind.PRESENT);
+ break;
+ default:
+ throw new IllegalStateException("Unhandled ValueKind: " + kind);
}
}
return this;
diff --git a/src/main/java/org/rcsb/cif/model/text/TextColumn.java b/src/main/java/org/rcsb/cif/model/text/TextColumn.java
index c0e2e86dc..10f857e08 100644
--- a/src/main/java/org/rcsb/cif/model/text/TextColumn.java
+++ b/src/main/java/org/rcsb/cif/model/text/TextColumn.java
@@ -36,19 +36,13 @@ public String getStringData(int row) {
}
private String honorValueKind(String value) {
- return (".".equals(value) || "?".equals(value)) ? "" : value;
+ return ValueKind.isValueKindToken(value) ? "" : value;
}
@Override
public ValueKind getValueKind(int row) {
String value = textData.substring(startToken[row], endToken[row]);
- if (value.isEmpty() || ".".equals(value)) {
- return ValueKind.NOT_PRESENT;
- } else if ("?".equals(value)) {
- return ValueKind.UNKNOWN;
- } else {
- return ValueKind.PRESENT;
- }
+ return ValueKind.fromCifToken(value);
}
/**
diff --git a/src/test/java/org/rcsb/cif/CifOptionsTest.java b/src/test/java/org/rcsb/cif/CifOptionsTest.java
index b6ab2bf42..a3aacab44 100644
--- a/src/test/java/org/rcsb/cif/CifOptionsTest.java
+++ b/src/test/java/org/rcsb/cif/CifOptionsTest.java
@@ -81,7 +81,7 @@ void testFetchUrlText() throws IOException {
// by switching to RCSB cif files, the implementation type should be text
CifFile cifFile = CifIO.readById("1acj", CifOptions.builder()
.fetchUrl("https://files.rcsb.org/download/%s.cif").build());
- assertTrue(cifFile instanceof TextFile);
+ assertInstanceOf(TextFile.class, cifFile);
}
@Test
@@ -89,7 +89,7 @@ void testFetchUrlBinary() throws IOException {
// by switching to RCSB bcif files, the implementation type should be binary
CifFile cifFile = CifIO.readById("1acj", CifOptions.builder()
.fetchUrl("https://models.rcsb.org/%s.bcif").build());
- assertTrue(cifFile instanceof BinaryFile);
+ assertInstanceOf(BinaryFile.class, cifFile);
}
@Test
diff --git a/src/test/java/org/rcsb/cif/IntegrationTest.java b/src/test/java/org/rcsb/cif/IntegrationTest.java
index 027777f07..082889fb9 100644
--- a/src/test/java/org/rcsb/cif/IntegrationTest.java
+++ b/src/test/java/org/rcsb/cif/IntegrationTest.java
@@ -98,12 +98,12 @@ void testDelegationBehavior() throws IOException {
MmCifFile textCifFile = CifIO.readFromInputStream(TestHelper.getInputStream("cif/1acj.cif")).as(StandardSchemata.MMCIF);
textCifFile.getFirstBlock()
.categories()
- .forEach(category -> assertTrue(category instanceof DelegatingCategory, "no delegation for text after schema was imposed for " + category.getCategoryName()));
+ .forEach(category -> assertInstanceOf(DelegatingCategory.class, category, "no delegation for text after schema was imposed for " + category.getCategoryName()));
MmCifFile binaryCifFile = CifIO.readFromInputStream(TestHelper.getInputStream("bcif/1acj.bcif")).as(StandardSchemata.MMCIF);
binaryCifFile.getFirstBlock()
.categories()
- .forEach(category -> assertTrue(category instanceof DelegatingCategory, "no delegation for binary after schema was imposed for " + category.getCategoryName()));
+ .forEach(category -> assertInstanceOf(DelegatingCategory.class, category, "no delegation for binary after schema was imposed for " + category.getCategoryName()));
}
@Test
diff --git a/src/test/java/org/rcsb/cif/TestHelper.java b/src/test/java/org/rcsb/cif/TestHelper.java
index a35d35d08..a027d7104 100644
--- a/src/test/java/org/rcsb/cif/TestHelper.java
+++ b/src/test/java/org/rcsb/cif/TestHelper.java
@@ -21,7 +21,7 @@
* Origin of files:
* - bcif created by Mol* encoder
* - cif created by Mol* encoder
- *
+ *
* All tests ensure that the behavior of the reference implementation (i.e. Mol*) is recreated rather than that output
* is in perfect agreement with e.g. PDB files.
*/
diff --git a/src/test/java/org/rcsb/cif/WriterTest.java b/src/test/java/org/rcsb/cif/WriterTest.java
index 7aa6917d5..773875a57 100644
--- a/src/test/java/org/rcsb/cif/WriterTest.java
+++ b/src/test/java/org/rcsb/cif/WriterTest.java
@@ -116,11 +116,11 @@ void testClassInferenceOfBuiltMmCifFile() {
.leaveBlock()
.leaveFile();
MmCifBlock block = cifFile.getFirstBlock();
- assertTrue(block.getCategory("atom_site") instanceof org.rcsb.cif.schema.mm.AtomSite);
- assertTrue(block.getCategory("atom_site").getColumn("B_iso_or_equiv") instanceof FloatColumn);
+ assertInstanceOf(org.rcsb.cif.schema.mm.AtomSite.class, block.getCategory("atom_site"));
+ assertInstanceOf(FloatColumn.class, block.getCategory("atom_site").getColumn("B_iso_or_equiv"));
Category atom_site = new CategoryBuilderImpl<>("atom_site", null).build();
- assertTrue(atom_site instanceof TextCategory);
+ assertInstanceOf(TextCategory.class, atom_site);
FloatColumn cartnX = new FloatColumnBuilderImpl<>("atom_site", "Cartn_x", null).build();
assertNotNull(cartnX);
@@ -144,7 +144,7 @@ void testClassInferenceOfBuiltCifCoreFile() {
assertTrue(columnBySchema.isDefined());
Column> columnByName = block.getColumn("atom_site_B_iso_or_equiv");
assertTrue(columnByName.isDefined());
- assertTrue(columnByName instanceof FloatColumn);
+ assertInstanceOf(FloatColumn.class, columnByName);
AtomSite categoryBySchema = block.getAtomSite();
assertTrue(categoryBySchema.isDefined());
@@ -154,7 +154,7 @@ void testClassInferenceOfBuiltCifCoreFile() {
assertFalse(categoryByName.isDefined());
Category atom_site = new CategoryBuilderImpl<>("atom_site", null).build();
- assertTrue(atom_site instanceof TextCategory);
+ assertInstanceOf(TextCategory.class, atom_site);
FloatColumn cartnX = new FloatColumnBuilderImpl<>("atom_site", "Cartn_x", null).build();
assertNotNull(cartnX);
diff --git a/src/test/java/org/rcsb/cif/binary/codec/MessagePackCodecTest.java b/src/test/java/org/rcsb/cif/binary/codec/MessagePackCodecTest.java
index 68cb56f66..2fb3b9560 100644
--- a/src/test/java/org/rcsb/cif/binary/codec/MessagePackCodecTest.java
+++ b/src/test/java/org/rcsb/cif/binary/codec/MessagePackCodecTest.java
@@ -15,7 +15,7 @@
import static org.rcsb.cif.TestHelper.convertToIntArray;
/**
- * - Obtain MessagePacked data at: https://msgpack.org/
+ * - Obtain MessagePacked data at: msgpack.com
* - Always use a sorted Map implementation (e.g. LinkedHashMap) as order matters.
*/
class MessagePackCodecTest {
? in CIF. String values will be empty, number values will be 0.
*/
- UNKNOWN
+ UNKNOWN;
+
+ public static final String CIF_NOT_PRESENT = ".";
+ public static final String CIF_UNKNOWN = "?";
+
+ /**
+ * Checks whether a String matches "?" or ".", sequences with special meaning in CIF.
+ * @param s payload to evaluate
+ * @return true if this String indicates missing or undefined values
+ */
+ public static boolean isValueKindToken(String s) {
+ return CIF_NOT_PRESENT.equals(s) || CIF_UNKNOWN.equals(s);
+ }
+
+ /**
+ * Transforms a String into a ValueKind.
+ * @param s payload to evaluate
+ * @return appropriate ValueKind for "?" and ".", otherwise marked as PRESENT
+ */
+ public static ValueKind fromCifToken(String s) {
+ if (s == null || CIF_NOT_PRESENT.equals(s)) return NOT_PRESENT;
+ if (CIF_UNKNOWN.equals(s)) return UNKNOWN;
+ return PRESENT;
+ }
}
diff --git a/src/main/java/org/rcsb/cif/model/binary/BinaryStrColumn.java b/src/main/java/org/rcsb/cif/model/binary/BinaryStrColumn.java
index 2b543170f..3e2bdf688 100644
--- a/src/main/java/org/rcsb/cif/model/binary/BinaryStrColumn.java
+++ b/src/main/java/org/rcsb/cif/model/binary/BinaryStrColumn.java
@@ -1,6 +1,7 @@
package org.rcsb.cif.model.binary;
import org.rcsb.cif.model.StrColumn;
+import org.rcsb.cif.model.ValueKind;
public class BinaryStrColumn extends BinaryColumn
+ *
+ *