properties) {
+ PropertyDeserializer propertyDeserializer = new PropertyDeserializer();
+ propertyDeserializer.registerPreprocessor(name -> {
+ Object value = properties.get(name);
+ return value != null ? value.toString() : null;
+ });
+ return propertyDeserializer;
+ }
+
+ /**
+ * Returns a {@link PropertyDeserializer} that acts as a live view for
+ * parsing values from the specified CSV record.
+ *
+ * @throws IllegalStateException if the CSV does not include any column
+ * information.
+ */
+ public static PropertyDeserializer fromCSV(CSVRecord record) {
+ Preconditions.checkState(record.hasColumnInformation(),
+ "CSV does not include column information");
+
+ PropertyDeserializer propertyDeserializer = new PropertyDeserializer();
+ propertyDeserializer.registerPreprocessor(record::get);
+ return propertyDeserializer;
+ }
}
diff --git a/source/nl/colorize/util/Subscribable.java b/source/nl/colorize/util/Subscribable.java
index ea9907f..fb55e8f 100644
--- a/source/nl/colorize/util/Subscribable.java
+++ b/source/nl/colorize/util/Subscribable.java
@@ -24,12 +24,12 @@
* asynchronous) operation, allowing for publish/subscribe workflows.
* Subscribers can be notified for events, for errors, or for both.
*
- * Such workflows can also be created in other ways, most commonly using
- * {@code java.util.concurrent} and its Flow API. However, the Flow API is
- * not yet supported on all platforms that are supported by this library,
- * making this class a more portable alternative.
+ * This type of workflow can also be created in other ways, most commonly
+ * using {@code java.util.concurrent} and its Flow API. However, the Flow
+ * API is not yet available on all platforms that are supported by this
+ * library, making this class a more portable alternative.
*
- * On platforms that do support concurrency, {@link Subscribable} instances
+ * On platforms that support concurrency, {@link Subscribable} instances
* are thread-safe and can be accessed from multiple threads.
*
* @param The type of event that can be subscribed to.
@@ -43,16 +43,15 @@ public final class Subscribable {
private static final Logger LOGGER = LogHelper.getLogger(Subscribable.class);
public Subscribable() {
- this.subscribers = prepareList();
- this.history = prepareList();
+ this.subscribers = new ArrayList<>();
+ this.history = new ArrayList<>();
this.completed = false;
- }
- private List prepareList() {
- if (Platform.isTeaVM()) {
- return new ArrayList<>();
- } else {
- return new CopyOnWriteArrayList<>();
+ // This needs to be in a block so this class can be
+ // compiled and run within TeaVM.
+ if (!Platform.isTeaVM()) {
+ subscribers = new CopyOnWriteArrayList<>();
+ history = new CopyOnWriteArrayList<>();
}
}
@@ -62,17 +61,13 @@ private List prepareList() {
* completed.
*/
public void next(T event) {
- if (completed) {
- return;
- }
-
- for (Subscriber subscriber : subscribers) {
- if (subscriber.onEvent != null) {
- subscriber.onEvent.accept(event);
+ if (!completed) {
+ for (Subscriber subscriber : subscribers) {
+ subscriber.onEvent(event);
}
- }
- history.add(event);
+ history.add(event);
+ }
}
/**
@@ -83,24 +78,13 @@ public void next(T event) {
* marked as completed.
*/
public void nextError(Exception error) {
- if (completed) {
- return;
- }
-
- boolean handled = false;
-
- for (Subscriber subscriber : subscribers) {
- if (subscriber.onError != null) {
- subscriber.onError.accept(error);
- handled = true;
+ if (!completed) {
+ for (Subscriber subscriber : subscribers) {
+ subscriber.onError(error);
}
- }
- if (!handled) {
- LOGGER.warning("Unhandled error: " + error.getMessage());
+ history.add(error);
}
-
- history.add(error);
}
/**
@@ -112,15 +96,13 @@ public void nextError(Exception error) {
* completed.
*/
public void next(Callable operation) {
- if (completed) {
- return;
- }
-
- try {
- T event = operation.call();
- next(event);
- } catch (Exception e) {
- nextError(e);
+ if (!completed) {
+ try {
+ T event = operation.call();
+ next(event);
+ } catch (Exception e) {
+ nextError(e);
+ }
}
}
@@ -177,22 +159,23 @@ public void retry(Callable operation, int attempts, long delay) {
}
/**
- * Registers the specified callback functions as event and error
- * subscribers. The subscribers will immediately be notified of previously
- * published events and/or errors. The {@code id} parameter can be used to
- * identify the subscriber.
+ * Registers the specified subscriber to receive published events and
+ * errors. The subscriber will immediately be notified of previously
+ * published events.
*
* @return This {@link Subscribable}, for method chaining.
*/
- public Subscribable subscribe(UUID id, Consumer onEvent, Consumer onError) {
- Subscriber subscriber = new Subscriber<>(id, onEvent, onError);
+ @SuppressWarnings("unchecked")
+ public Subscribable subscribe(Subscriber subscriber) {
+ Preconditions.checkNotNull(subscriber, "Null subscriber");
+
subscribers.add(subscriber);
for (Object historicEvent : history) {
- if (onError != null && historicEvent instanceof Exception error) {
- onError.accept(error);
- } else if (onEvent != null) {
- onEvent.accept((T) historicEvent);
+ if (historicEvent instanceof Exception error) {
+ subscriber.onError(error);
+ } else {
+ subscriber.onEvent((T) historicEvent);
}
}
@@ -201,23 +184,23 @@ public Subscribable subscribe(UUID id, Consumer onEvent, Consumer subscribe(Consumer onEvent, Consumer onError) {
- return subscribe(UUID.randomUUID(), onEvent, onError);
- }
+ public Subscribable subscribe(Consumer eventFn, Consumer errorFn) {
+ return subscribe(new Subscriber() {
+ @Override
+ public void onEvent(T event) {
+ eventFn.accept(event);
+ }
- /**
- * Registers the specified callback function as an event subscriber. The
- * subscriber will immediately be notified of previously published events.
- *
- * @return This {@link Subscribable}, for method chaining.
- */
- public Subscribable subscribe(Consumer onEvent) {
- return subscribe(onEvent, null);
+ @Override
+ public void onError(Exception error) {
+ errorFn.accept(error);
+ }
+ });
}
/**
@@ -227,7 +210,8 @@ public Subscribable subscribe(Consumer onEvent) {
* @return This {@link Subscribable}, for method chaining.
*/
public Subscribable subscribeErrors(Consumer onError) {
- return subscribe(null, onError);
+ Consumer onEvent = event -> {};
+ return subscribe(onEvent, onError);
}
/**
@@ -243,12 +227,12 @@ public Subscribable subscribe(Subscribable subscriber) {
}
/**
- * Removes a previously registered subscriber, identifying the subscriber
- * by its ID. This method does nothing if none of the current subscribers
- * match the ID.
+ * Removes a previously registered subscriber. If the subscriber is not
+ * currently registered with this {@link Subscribable}, this method does
+ * nothing.
*/
- public void unsubscribe(UUID id) {
- subscribers.removeIf(subscriber -> subscriber.id.equals(id));
+ public void unsubscribe(Subscriber subscriber) {
+ subscribers.remove(subscriber);
}
/**
@@ -257,7 +241,13 @@ public void unsubscribe(UUID id) {
* might still be published when additional subscribers are registered.
*/
public void complete() {
- completed = true;
+ if (!completed) {
+ completed = true;
+
+ for (Subscriber subscriber : subscribers) {
+ subscriber.onComplete();
+ }
+ }
}
/**
@@ -350,12 +340,4 @@ public static Subscribable runBackground(Callable operation) {
return subscribable;
}
-
- /**
- * Internal data structure that creates a subscriber objects from callback
- * methods. Callback methods can be {@code null} if the subscriber is not
- * interested in certain events.
- */
- private record Subscriber(UUID id, Consumer onEvent, Consumer onError) {
- }
}
diff --git a/source/nl/colorize/util/Subscriber.java b/source/nl/colorize/util/Subscriber.java
new file mode 100644
index 0000000..a09aeaa
--- /dev/null
+++ b/source/nl/colorize/util/Subscriber.java
@@ -0,0 +1,42 @@
+//-----------------------------------------------------------------------------
+// Colorize Java Commons
+// Copyright 2007-2024 Colorize
+// Apache license (http://www.apache.org/licenses/LICENSE-2.0)
+//-----------------------------------------------------------------------------
+
+package nl.colorize.util;
+
+import java.util.logging.Logger;
+
+/**
+ * Interface for subscribing to (possibly asynchronous) events that are
+ * published by a {@link Subscribable}.
+ *
+ * @param The type of event that can be subscribed to.
+ */
+@FunctionalInterface
+public interface Subscriber {
+
+ /**
+ * Invoked by the {@link Subscribable} when an event has been received.
+ */
+ public void onEvent(T event);
+
+ /**
+ * Invoked by the {@link Subscribable} when an error has occurred while
+ * publishing an event. This is an optional method, the default
+ * implementation will log the error.
+ */
+ default void onError(Exception error) {
+ Logger logger = LogHelper.getLogger(getClass());
+ logger.warning("Sunscriber:");
+ }
+
+ /**
+ * Invoked when the {@link Subscribable} is marked as completed, after
+ * which this {@link Subscriber} will no longer receive events. This is
+ * an optional method, the default implementation does nothing.
+ */
+ default void onComplete() {
+ }
+}
diff --git a/source/nl/colorize/util/TextUtils.java b/source/nl/colorize/util/TextUtils.java
index 8a721d3..86adfe6 100644
--- a/source/nl/colorize/util/TextUtils.java
+++ b/source/nl/colorize/util/TextUtils.java
@@ -39,10 +39,12 @@ public final class TextUtils {
public static final Splitter LINE_SPLITTER = Splitter.on("\n");
public static final Joiner LINE_JOINER = Joiner.on("\n");
+
private static final Pattern WORD_SEPARATOR = Pattern.compile("[ _]");
private static final Splitter WORD_SPLITTER = Splitter.on(WORD_SEPARATOR).omitEmptyStrings();
- private static final CharMatcher TEXT_MATCHER = CharMatcher.forPredicate(Character::isLetterOrDigit)
+ private static final CharMatcher TEXT_MATCHER = CharMatcher
+ .forPredicate(Character::isLetterOrDigit)
.or(CharMatcher.whitespace());
private TextUtils() {
diff --git a/source/nl/colorize/util/http/URLLoader.java b/source/nl/colorize/util/http/URLLoader.java
index 520b8d8..9ef27e8 100644
--- a/source/nl/colorize/util/http/URLLoader.java
+++ b/source/nl/colorize/util/http/URLLoader.java
@@ -331,8 +331,8 @@ private URI getFullURL() {
* will be thrown.
*/
public URLResponse send() throws IOException {
- boolean force = System.getProperty(FORCE_LEGACY_HTTP_CLIENT_SYSTEM_PROPERTY, "").equals("true");
- boolean classicHttpClient = Platform.isTeaVM() || force;
+ boolean classicHttpClient = Platform.isTeaVM() ||
+ System.getProperty(FORCE_LEGACY_HTTP_CLIENT_SYSTEM_PROPERTY, "").equals("true");
if (classicHttpClient) {
URI fullURL = getFullURL();
diff --git a/source/nl/colorize/util/http/URLResponse.java b/source/nl/colorize/util/http/URLResponse.java
index 61c68d5..10293ba 100644
--- a/source/nl/colorize/util/http/URLResponse.java
+++ b/source/nl/colorize/util/http/URLResponse.java
@@ -22,6 +22,8 @@
import java.util.List;
import java.util.Optional;
+import static lombok.AccessLevel.PROTECTED;
+
/**
* Represents an HTTP response that is returned after sending an HTTP request
* to a URL. Used in conjunction with {@link URLLoader}, which supports/uses
@@ -33,7 +35,7 @@ public class URLResponse implements Resource {
private int status;
private Headers headers;
private byte[] body;
- @Setter(AccessLevel.PROTECTED) private SSLSession sslSession;
+ @Setter(PROTECTED) private SSLSession sslSession;
public URLResponse(int status, Headers headers, byte[] body) {
this.status = status;
diff --git a/source/nl/colorize/util/stats/Aggregate.java b/source/nl/colorize/util/stats/Aggregate.java
index 6cd5642..01b9728 100644
--- a/source/nl/colorize/util/stats/Aggregate.java
+++ b/source/nl/colorize/util/stats/Aggregate.java
@@ -87,6 +87,10 @@ public static float percentage(float value, float total) {
return value * 100f / total;
}
+ public static float multiplyPercentage(float percentageA, float percentageB) {
+ return ((percentageA / 100f) * (percentageB / 100f)) * 100f;
+ }
+
/**
* Calculates the value of the Nth percentile for the specified data set.
* This method will interpolate between values if none of the values in
diff --git a/source/nl/colorize/util/stats/CSVFormat.java b/source/nl/colorize/util/stats/CSVFormat.java
new file mode 100644
index 0000000..7796fcc
--- /dev/null
+++ b/source/nl/colorize/util/stats/CSVFormat.java
@@ -0,0 +1,156 @@
+//-----------------------------------------------------------------------------
+// Colorize Java Commons
+// Copyright 2007-2024 Colorize
+// Apache license (http://www.apache.org/licenses/LICENSE-2.0)
+//-----------------------------------------------------------------------------
+
+package nl.colorize.util.stats;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Describes the format of a CSV file, which is necessary since the CSV file
+ * format is not fully standardized. Instances of the format can then be used
+ * to parse CSV files into {@link CSVRecord}s, or to serialize records into
+ * CSV files.
+ *
+ * Note: This class is intended for basic CSV support.
+ * When working with CSV files produced by other applications, prefer using
+ * the FastCSV library.
+ */
+public class CSVFormat {
+
+ private boolean headers;
+ private char delimiter;
+ private String lineSeparator;
+ private boolean quotes;
+
+ public static final CSVFormat COMMA = withHeaders(',');
+ public static final CSVFormat TAB = withHeaders('\t');
+ public static final CSVFormat SEMICOLON = withHeaders(';');
+
+ private CSVFormat(boolean headers, char delimiter, String lineSeparator, boolean quotes) {
+ this.headers = headers;
+ this.delimiter = delimiter;
+ this.lineSeparator = lineSeparator;
+ this.quotes = quotes;
+ }
+
+ public CSVFormat withQuotes() {
+ return new CSVFormat(headers, delimiter, lineSeparator, true);
+ }
+
+ public CSVFormat withLineSeparator(String lineSeparator) {
+ return new CSVFormat(headers, delimiter, lineSeparator, quotes);
+ }
+
+ /**
+ * Parses the specified CSV file using this {@link CSVFormat}, and returns
+ * the resulting records. If this CSV format includes header information,
+ * the first record in the file is assumed to contain the headers.
+ */
+ public List parseCSV(String csv) {
+ List records = new ArrayList<>();
+ List columns = null;
+
+ for (String line : Splitter.on(lineSeparator).omitEmptyStrings().split(csv)) {
+ List cells = parseLine(line);
+
+ if (headers && columns == null) {
+ columns = cells;
+ } else {
+ records.add(new CSVRecord(columns, cells, this));
+ }
+ }
+
+ return records;
+ }
+
+ private List parseLine(String line) {
+ if (quotes) {
+ return parseQuotedLine(line);
+ } else {
+ return Splitter.on(delimiter).splitToList(line);
+ }
+ }
+
+ private List parseQuotedLine(String line) {
+ List cells = new ArrayList<>();
+ StringBuilder buffer = new StringBuilder();
+ boolean quoting = false;
+
+ for (int i = 0; i < line.length(); i++) {
+ if (line.charAt(i) == delimiter && !quoting) {
+ cells.add(buffer.toString());
+ buffer.delete(0, buffer.length());
+ } else if (line.charAt(i) == '\"') {
+ quoting = !quoting;
+ } else {
+ buffer.append(line.charAt(i));
+ }
+ }
+
+ if (!buffer.isEmpty()) {
+ cells.add(buffer.toString());
+ }
+
+ return cells;
+ }
+
+ /**
+ * Serializes the specified record using this CSV format. The serialized
+ * line will end with a trailing line separator. This method can be used
+ * for both rows and headers, since CSV files do not differentiate
+ * between the two apart from their location within the file.
+ *
+ * @throws IllegalArgumentException if the record does not contain at
+ * least one cell.
+ */
+ public String toCSV(List cells) {
+ Preconditions.checkArgument(!cells.isEmpty(), "Empty CSV record");
+
+ CharMatcher illegalCharacters = CharMatcher.anyOf("\r\n\"" + delimiter);
+
+ return cells.stream()
+ .map(illegalCharacters::removeFrom)
+ .collect(Collectors.joining(String.valueOf(delimiter))) + lineSeparator;
+ }
+
+ /**
+ * Serializes the specified record using this CSV format. The serialized
+ * line will end with a trailing line separator. This method can be used
+ * for both rows and headers, since CSV files do not differentiate
+ * between the two apart from their location within the file.
+ *
+ * @throws IllegalArgumentException if the record does not contain at
+ * least one cell.
+ */
+ public String toCSV(String... cells) {
+ return toCSV(List.of(cells));
+ }
+
+ /**
+ * Serializes the specified record using this CSV format. The serialized
+ * line will end with a trailing line separator.
+ *
+ * @throws IllegalArgumentException if the record does not contain at
+ * least one cell.
+ */
+ public String toCSV(CSVRecord record) {
+ return toCSV(record.getCells());
+ }
+
+ public static CSVFormat withHeaders(char delimiter) {
+ return new CSVFormat(true, delimiter, System.lineSeparator(), false);
+ }
+
+ public static CSVFormat withoutHeaders(char delimiter) {
+ return new CSVFormat(false, delimiter, System.lineSeparator(), false);
+ }
+}
diff --git a/source/nl/colorize/util/stats/CSVRecord.java b/source/nl/colorize/util/stats/CSVRecord.java
new file mode 100644
index 0000000..ccb1f1a
--- /dev/null
+++ b/source/nl/colorize/util/stats/CSVRecord.java
@@ -0,0 +1,114 @@
+//-----------------------------------------------------------------------------
+// Colorize Java Commons
+// Copyright 2007-2024 Colorize
+// Apache license (http://www.apache.org/licenses/LICENSE-2.0)
+//-----------------------------------------------------------------------------
+
+package nl.colorize.util.stats;
+
+import com.google.common.base.Preconditions;
+import lombok.Getter;
+
+import java.util.List;
+
+/**
+ * One of the records witin a CSV file. Records are usually parsed from a
+ * CSV file using {@link CSVFormat}, but can also be created programmatically.
+ * Fields within the record can be retrieved by index or by column name.
+ * The latter is only available if the CSV contains column information.
+ *
+ * Note: This class is intended for basic CSV support.
+ * When working with CSV files produced by other applications, prefer using
+ * the FastCSV library.
+ */
+@Getter
+public class CSVRecord {
+
+ private List columns;
+ private List cells;
+ private CSVFormat format;
+
+ /**
+ * Creates a new CSV record from the specified cells. The column
+ * information is optional: {@code null} indicates that no column
+ * information is available, meaning that cells can only be retrieved
+ * by index.
+ *
+ * {@link CSVRecord} instances are normally by obtained by parsing CSV
+ * files using {@link CSVFormat}. There is normally no need for
+ * applications to use this constructor directly.
+ */
+ protected CSVRecord(List columns, List cells, CSVFormat format) {
+ Preconditions.checkArgument(!cells.isEmpty(), "Empty CSV record");
+ Preconditions.checkArgument(columns == null || columns.size() == cells.size(),
+ "Invalid number of columns");
+
+ this.columns = columns == null ? null : List.copyOf(columns);
+ this.cells = List.copyOf(cells);
+ this.format = format;
+ }
+
+ /**
+ * Returns the value of the cell with the specified index. This method is
+ * always available, even if the CSV file does not include any column
+ * information.
+ *
+ * @throws IllegalArgumentException if the column index is invalid.
+ */
+ public String get(int index) {
+ Preconditions.checkArgument(index >= 0 && index < cells.size(),
+ "Invalid column index: " + index);
+ return cells.get(index);
+ }
+
+ /**
+ * Returns true if this CSV record included column information. If false,
+ * cells can only be accessed by index, not by column name.
+ */
+ public boolean hasColumnInformation() {
+ return columns != null;
+ }
+
+ /**
+ * Returns the value for the cell with the specified column name.
+ *
+ * @throws IllegalStateException if no column name information is available.
+ * @throws IllegalArgumentException if no column with that name exists.
+ */
+ public String get(String column) {
+ Preconditions.checkState(columns != null, "No column name information available");
+ Preconditions.checkArgument(columns.contains(column), "No such column: " + column);
+
+ int columnIndex = columns.indexOf(column);
+ return cells.get(columnIndex);
+ }
+
+ /**
+ * Returns a list of all named columns in this CSV record. The columns will
+ * be sorted in the same order as they appear in the CSV.
+ *
+ * @throws IllegalStateException if no column name information is available.
+ */
+ public List getColumns() {
+ Preconditions.checkState(columns != null, "No column name information available");
+ return columns;
+ }
+
+ /**
+ * Returns a list of column values for all cells in this CSV record. Each
+ * tuple in the list consists of the column name and corresponding cell
+ * value. The list will be sorted to match the order in which the columns
+ * appear in the CSV.
+ *
+ * @throws IllegalStateException if no column name information is available.
+ */
+ public TupleList getColumnValues() {
+ Preconditions.checkState(!columns.isEmpty(), "No column name information available");
+ return TupleList.combine(columns, cells);
+ }
+
+ @Override
+ public String toString() {
+ return format.toCSV(this);
+ }
+}
diff --git a/test/nl/colorize/util/CSVRecordTest.java b/test/nl/colorize/util/CSVRecordTest.java
deleted file mode 100644
index 7d48faa..0000000
--- a/test/nl/colorize/util/CSVRecordTest.java
+++ /dev/null
@@ -1,150 +0,0 @@
-//-----------------------------------------------------------------------------
-// Colorize Java Commons
-// Copyright 2007-2024 Colorize
-// Apache license (http://www.apache.org/licenses/LICENSE-2.0)
-//-----------------------------------------------------------------------------
-
-package nl.colorize.util;
-
-import org.junit.jupiter.api.Test;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-
-public class CSVRecordTest {
-
- @Test
- public void testParse() {
- CSVRecord record = CSVRecord.parseRecord("a;2;3.4;false", ";");
-
- assertEquals("a", record.get(0));
- assertEquals("2", record.get(1));
- assertEquals("3.4", record.get(2));
- assertEquals("false", record.get(3));
- }
-
- @Test
- public void testSerialize() {
- CSVRecord record = CSVRecord.parseRecord("a;2", ";");
-
- assertEquals("a;2", record.toString());
- }
-
- @Test
- public void testSerializeMultiple() {
- CSVRecord first = CSVRecord.create(Arrays.asList("a", "2"));
- CSVRecord second = CSVRecord.create(Arrays.asList("b", "3"));
-
- assertEquals("a;2\nb;3", CSVRecord.toCSV(List.of(first, second), ";", false));
- }
-
- @Test
- public void testEscape() {
- CSVRecord record = CSVRecord.create(Arrays.asList("a", "b;c", "d\ne"));
-
- assertEquals("a;bc;de", record.toString());
- }
-
- @Test
- public void testParseMultipleWithHeader() {
- List records = CSVRecord.parseCSV("h1;h2\na;2\nb;3", ";", true);
-
- assertEquals(2, records.size());
- assertEquals("a", records.get(0).get(0));
- assertEquals("b", records.get(1).get(0));
- }
-
- @Test
- void returnEmptyStringIfNoRecords() {
- assertEquals("", CSVRecord.toCSV(Collections.emptyList(), ";", false));
- }
-
- @Test
- void getColumnByName() {
- String csv = """
- Name;Age
- John;38
- """;
-
- List records = CSVRecord.parseCSV(csv, ";", true);
-
- assertEquals("John", records.get(0).get("Name"));
- assertEquals("38", records.get(0).get("Age"));
- }
-
- @Test
- void parseWithoutHeaders() {
- String csv = """
- Name;Age
- John;38
- Jim;26
- """;
-
- List records = CSVRecord.parseCSV(csv, ";", false);
-
- assertEquals(3, records.size());
- assertEquals("Name", records.get(0).get(0));
- assertEquals("John", records.get(1).get(0));
- assertEquals("Jim", records.get(2).get(0));
- }
-
- @Test
- void serializeWithHeaders() {
- CSVRecord record = CSVRecord.create(List.of("Name", "Age"), List.of("John", "38"));
- String csv = CSVRecord.toCSV(List.of(record), ";", true);
-
- String expected = """
- Name;Age
- John;38""";
-
- assertEquals(expected, csv);
- }
-
- @Test
- void serializeWithoutHeaders() {
- CSVRecord record = CSVRecord.create(List.of("Name", "Age"), List.of("John", "38"));
- String csv = CSVRecord.toCSV(List.of(record), ";", false);
-
- assertEquals("John;38", csv);
- }
-
- @Test
- void exceptionIfHeadersAreRequestedButNotAvailable() {
- CSVRecord record = CSVRecord.create(List.of("John", "38"));
-
- assertThrows(IllegalStateException.class,
- () -> CSVRecord.toCSV(List.of(record), ";", true));
- }
-
- @Test
- void exceptionForInconsistentHeaders() {
- CSVRecord recordA = CSVRecord.create(List.of("Name", "Age"), List.of("John", "38"));
- CSVRecord recordB = CSVRecord.create(List.of("Name", "Other"), List.of("Jim", "26"));
-
- assertThrows(IllegalStateException.class,
- () -> CSVRecord.toCSV(List.of(recordA, recordB), ";", true));
- }
-
- @Test
- void getColumns() {
- String csv = """
- Name;Age
- John;38
- """;
-
- List records = CSVRecord.parseCSV(csv, ";", true);
-
- assertEquals(1, records.size());
- assertEquals(List.of("Name", "Age"), records.get(0).getColumns());
- assertEquals("[(Name, John), (Age, 38)]", records.get(0).getColumnValues().toString());
- }
-
- @Test
- void parseEmptyCSV() {
- assertEquals(0, CSVRecord.parseCSV("", ";", true).size());
- }
-}
diff --git a/test/nl/colorize/util/PropertyDeserializerTest.java b/test/nl/colorize/util/PropertyDeserializerTest.java
index cb8aa51..ce52b28 100644
--- a/test/nl/colorize/util/PropertyDeserializerTest.java
+++ b/test/nl/colorize/util/PropertyDeserializerTest.java
@@ -7,6 +7,8 @@
package nl.colorize.util;
import com.google.common.base.Splitter;
+import nl.colorize.util.stats.CSVFormat;
+import nl.colorize.util.stats.CSVRecord;
import nl.colorize.util.stats.DateRange;
import org.junit.jupiter.api.Test;
@@ -16,6 +18,7 @@
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
+import java.util.Map;
import java.util.Properties;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -138,4 +141,23 @@ void convertDateAndTime() {
assertEquals(LocalDate.of(2018, 3, 18), date);
assertEquals(LocalDateTime.of(2018, 3, 18, 15, 30), datetime);
}
+
+ @Test
+ void fromMap() {
+ Map properties = Map.of("a", "b", "c", 2);
+ PropertyDeserializer propertyDeserializer = PropertyDeserializer.fromMap(properties);
+
+ assertEquals("b", propertyDeserializer.parseString("a", "?"));
+ assertEquals(2, propertyDeserializer.parseInt("c", -1));
+ assertEquals(-1, propertyDeserializer.parseInt("d", -1));
+ }
+
+ @Test
+ void fromCSV() {
+ CSVRecord record = CSVFormat.SEMICOLON.parseCSV("name;age\njohn;38").getFirst();
+ PropertyDeserializer propertyDeserializer = PropertyDeserializer.fromCSV(record);
+
+ assertEquals("john", propertyDeserializer.parseString("name", ""));
+ assertEquals(38, propertyDeserializer.parseInt("age", -1));
+ }
}
diff --git a/test/nl/colorize/util/SubscribableTest.java b/test/nl/colorize/util/SubscribableTest.java
index 4c11d9a..16eddeb 100644
--- a/test/nl/colorize/util/SubscribableTest.java
+++ b/test/nl/colorize/util/SubscribableTest.java
@@ -11,9 +11,8 @@
import java.util.ArrayList;
import java.util.List;
-import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.function.Consumer;
+import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -34,7 +33,7 @@ void subscribe() {
void dontReceiveAfterDispose() {
List received = new ArrayList<>();
Subscribable subject = new Subscribable<>();
- Consumer subscriber = received::add;
+ Subscriber subscriber = received::add;
subject.subscribe(subscriber);
subject.next("a");
@@ -189,16 +188,16 @@ void subscribeOther() {
@Test
void unsubscribe() {
- UUID idA = UUID.randomUUID();
- UUID idB = UUID.randomUUID();
List received = new ArrayList<>();
+ Subscriber a = event -> received.add("a" + event);
+ Subscriber b = event -> received.add("b" + event);
Subscribable subject = new Subscribable<>();
- subject.subscribe(idA, event -> received.add("a" + event), null);
- subject.subscribe(idB, event -> received.add("b" + event), null);
+ subject.subscribe(a);
+ subject.subscribe(b);
subject.next("1");
subject.next("2");
- subject.unsubscribe(idA);
+ subject.unsubscribe(a);
subject.next("3");
assertEquals("[a1, b1, a2, b2, b3]", received.toString());
@@ -261,4 +260,45 @@ void retryWithDelay() {
assertEquals(1, result.size());
assertEquals(0, errors.size());
}
+
+ @Test
+ void completedEvent() {
+ AtomicInteger counter = new AtomicInteger(0);
+
+ Subscribable subject = new Subscribable<>();
+ subject.subscribe(new Subscriber<>() {
+ @Override
+ public void onEvent(String event) {
+ }
+
+ @Override
+ public void onComplete() {
+ counter.incrementAndGet();
+ }
+ });
+ subject.complete();
+
+ assertEquals(1, counter.get());
+ }
+
+ @Test
+ void doNotSendCompletedEventTwice() {
+ AtomicInteger counter = new AtomicInteger(0);
+
+ Subscribable subject = new Subscribable<>();
+ subject.subscribe(new Subscriber<>() {
+ @Override
+ public void onEvent(String event) {
+ }
+
+ @Override
+ public void onComplete() {
+ counter.incrementAndGet();
+ }
+ });
+ subject.complete();
+ subject.complete();
+
+ assertEquals(1, counter.get());
+ }
}
diff --git a/test/nl/colorize/util/stats/AggregateTest.java b/test/nl/colorize/util/stats/AggregateTest.java
index 080c603..fb1f0fb 100644
--- a/test/nl/colorize/util/stats/AggregateTest.java
+++ b/test/nl/colorize/util/stats/AggregateTest.java
@@ -86,4 +86,13 @@ void correlation() {
assertEquals(-0.90f, Aggregate.correlation(original, differentOrder), EPSILON);
assertEquals(0.13f, Aggregate.correlation(original, dissimilar), EPSILON);
}
+
+ @Test
+ void multiplyPercentage() {
+ assertEquals(0f, Aggregate.multiplyPercentage(0f, 0f), EPSILON);
+ assertEquals(0f, Aggregate.multiplyPercentage(0f, 50f), EPSILON);
+ assertEquals(100f, Aggregate.multiplyPercentage(100f, 100f), EPSILON);
+ assertEquals(50f, Aggregate.multiplyPercentage(100f, 50f), EPSILON);
+ assertEquals(25f, Aggregate.multiplyPercentage(50f, 50f), EPSILON);
+ }
}
diff --git a/test/nl/colorize/util/stats/CSVFormatTest.java b/test/nl/colorize/util/stats/CSVFormatTest.java
new file mode 100644
index 0000000..fc750de
--- /dev/null
+++ b/test/nl/colorize/util/stats/CSVFormatTest.java
@@ -0,0 +1,90 @@
+//-----------------------------------------------------------------------------
+// Colorize Java Commons
+// Copyright 2007-2024 Colorize
+// Apache license (http://www.apache.org/licenses/LICENSE-2.0)
+//-----------------------------------------------------------------------------
+
+package nl.colorize.util.stats;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class CSVFormatTest {
+
+ @Test
+ void parseCSV() {
+ String csv = """
+ name;age
+ john;38
+ jim;40
+ """;
+
+ List records = CSVFormat.SEMICOLON.parseCSV(csv);
+
+ assertEquals(2, records.size());
+ assertEquals("john", records.get(0).get(0));
+ assertEquals("38", records.get(0).get(1));
+ assertEquals("jim", records.get(1).get(0));
+ assertEquals("40", records.get(1).get(1));
+ }
+
+ @Test
+ void parseWithoutHeaders() {
+ String csv = """
+ john;38
+ jim;40
+ """;
+
+ List records = CSVFormat.withoutHeaders(';').parseCSV(csv);
+
+ assertEquals(2, records.size());
+ assertEquals("john", records.get(0).get(0));
+ assertEquals("38", records.get(0).get(1));
+ assertEquals("jim", records.get(1).get(0));
+ assertEquals("40", records.get(1).get(1));
+ }
+
+ @Test
+ void parseEmptyCSV() {
+ List records = CSVFormat.withHeaders(';').parseCSV("");
+
+ assertEquals(0, records.size());
+ }
+
+ @Test
+ void parseHeadersWithoutRows() {
+ List records = CSVFormat.withHeaders(';').parseCSV("name;age");
+
+ assertEquals(0, records.size());
+ }
+
+ @Test
+ void parseQuotes() {
+ String csv = """
+ name;age
+ "jo;hn";38
+ jim;"old"
+ """;
+
+ List records = CSVFormat.SEMICOLON.withQuotes().parseCSV(csv);
+
+ assertEquals(2, records.size());
+ assertEquals("jo;hn", records.get(0).get(0));
+ assertEquals("38", records.get(0).get(1));
+ assertEquals("jim", records.get(1).get(0));
+ assertEquals("old", records.get(1).get(1));
+ }
+
+ @Test
+ void serializeRecords() {
+ assertEquals("john;38\n", CSVFormat.SEMICOLON.toCSV("john", "38"));
+ }
+
+ @Test
+ void escapeDelimiters() {
+ assertEquals("john;38\n", CSVFormat.SEMICOLON.toCSV("jo;hn", "38"));
+ }
+}
diff --git a/test/nl/colorize/util/stats/CSVRecordTest.java b/test/nl/colorize/util/stats/CSVRecordTest.java
new file mode 100644
index 0000000..aada7de
--- /dev/null
+++ b/test/nl/colorize/util/stats/CSVRecordTest.java
@@ -0,0 +1,73 @@
+//-----------------------------------------------------------------------------
+// Colorize Java Commons
+// Copyright 2007-2024 Colorize
+// Apache license (http://www.apache.org/licenses/LICENSE-2.0)
+//-----------------------------------------------------------------------------
+
+package nl.colorize.util.stats;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class CSVRecordTest {
+
+ @Test
+ public void testParseMultipleWithHeader() {
+ List records = CSVFormat.SEMICOLON.parseCSV("h1;h2\na;2\nb;3");
+
+ assertEquals(2, records.size());
+ assertEquals("a", records.get(0).get(0));
+ assertEquals("b", records.get(1).get(0));
+ }
+
+ @Test
+ void getColumnByName() {
+ String csv = """
+ Name;Age
+ John;38
+ """;
+
+ List records = CSVFormat.SEMICOLON.parseCSV(csv);
+
+ assertEquals("John", records.get(0).get("Name"));
+ assertEquals("38", records.get(0).get("Age"));
+ }
+
+ @Test
+ void parseWithoutHeaders() {
+ String csv = """
+ Name;Age
+ John;38
+ Jim;26
+ """;
+
+ List records = CSVFormat.withoutHeaders(';').parseCSV(csv);
+
+ assertEquals(3, records.size());
+ assertEquals("Name", records.get(0).get(0));
+ assertEquals("John", records.get(1).get(0));
+ assertEquals("Jim", records.get(2).get(0));
+ }
+
+ @Test
+ void getColumns() {
+ String csv = """
+ Name;Age
+ John;38
+ """;
+
+ List records = CSVFormat.SEMICOLON.parseCSV(csv);
+
+ assertEquals(1, records.size());
+ assertEquals(List.of("Name", "Age"), records.get(0).getColumns());
+ assertEquals("[(Name, John), (Age, 38)]", records.get(0).getColumnValues().toString());
+ }
+
+ @Test
+ void parseEmptyCSV() {
+ assertEquals(0, CSVFormat.SEMICOLON.parseCSV("").size());
+ }
+}
diff --git a/test/nl/colorize/util/uitest/ImageViewerUIT.java b/test/nl/colorize/util/uitest/ImageViewerUIT.java
index 017725e..abe2bf9 100644
--- a/test/nl/colorize/util/uitest/ImageViewerUIT.java
+++ b/test/nl/colorize/util/uitest/ImageViewerUIT.java
@@ -44,7 +44,7 @@ public class ImageViewerUIT {
private ImageViewer imageViewer;
private Table imageList;
- private static final int IMAGE_CACHE_SIZE = 1100;
+ private static final int IMAGE_CACHE_SIZE = 1000;
public static void main(String[] args) {
SwingUtils.initializeSwing();