diff --git a/lib/src/main/java/de/siegmar/fastcsv/reader/CsvRecord.java b/lib/src/main/java/de/siegmar/fastcsv/reader/CsvRecord.java index 8d6d6a78..75e71255 100644 --- a/lib/src/main/java/de/siegmar/fastcsv/reader/CsvRecord.java +++ b/lib/src/main/java/de/siegmar/fastcsv/reader/CsvRecord.java @@ -6,7 +6,14 @@ import java.util.StringJoiner; /** - * Index based CSV-record. + * Represents an immutable CSV record with unnamed (indexed) fields. + *

+ * The field values are never {@code null}. Empty fields are represented as empty strings. + *

+ * CSV records are created by {@link CsvReader} or {@link IndexedCsvReader}. + * + * @see CsvReader + * @see IndexedCsvReader */ @SuppressWarnings("PMD.ArrayIsStoredDirectly") public class CsvRecord { @@ -56,18 +63,34 @@ public class CsvRecord { } /** - * Returns the starting line number (starting with 1). On multi-line records this is the starting - * line number. - * Empty lines could be skipped via {@link CsvReader.CsvReaderBuilder#skipEmptyLines(boolean)}. + * Provides the line number at which this record originated, starting from 1. + *

+ * This information is particularly valuable in scenarios involving CSV files containing empty lines as well as + * multi-line or commented records, where the record number may deviate from the line number. + *

+ * Example: + *

+     * 1 foo,bar
+     * 2 foo,"multi
+     * 3 line bar"
+     * 4                    (empty, potentially skipped)
+     * 5 #commented record  (potentially skipped)
+     * 6 "latest
+     * 7 record"
+     * 
+ * The last record (containing the multi-line field "latest\nrecord") would have a starting line number of 6, + * no matter if empty lines or commented records are skipped or not. + *

+ * A starting offset of 1 is used to be consistent with the line numbers shown of most text editors. * - * @return the starting line number + * @return the starting line number of this record, starting from 1 */ public long getStartingLineNumber() { return startingLineNumber; } /** - * Gets a field value by its index (starting with 0). + * Retrieves the value of a field based on its index, with indexing starting from 0. * * @param index index of the field to return * @return field value, never {@code null} @@ -78,7 +101,7 @@ public String getField(final int index) { } /** - * Gets all fields of this record as an unmodifiable list. + * Retrieves all fields of this record as an unmodifiable list. * * @return all fields of this record, never {@code null} */ @@ -87,7 +110,7 @@ public List getFields() { } /** - * Gets the number of fields of this record. + * Obtains the count of fields in this record. * * @return the number of fields of this record * @see CsvReader.CsvReaderBuilder#ignoreDifferentFieldCount(boolean) @@ -97,7 +120,7 @@ public int getFieldCount() { } /** - * Provides the information if the record is a commented record. + * Indicates whether the record is a commented record. * * @return {@code true} if the record is a commented record * @see CsvReader.CsvReaderBuilder#commentStrategy(CommentStrategy) @@ -107,7 +130,8 @@ public boolean isComment() { } /** - * Provides the information if the record is an empty record. + * Indicates whether the record is considered empty, signifying that it contains only a single field with an empty + * string. * * @return {@code true} if the record is an empty record * @see CsvReader.CsvReaderBuilder#skipEmptyLines(boolean) diff --git a/lib/src/main/java/de/siegmar/fastcsv/reader/NamedCsvRecord.java b/lib/src/main/java/de/siegmar/fastcsv/reader/NamedCsvRecord.java index 74dc6546..00af23e5 100644 --- a/lib/src/main/java/de/siegmar/fastcsv/reader/NamedCsvRecord.java +++ b/lib/src/main/java/de/siegmar/fastcsv/reader/NamedCsvRecord.java @@ -13,7 +13,14 @@ import java.util.StringJoiner; /** - * Name (header) based CSV-record. + * Represents an immutable CSV record with named (and indexed) fields. + *

+ * The field values are never {@code null}. Empty fields are represented as empty strings. + *

+ * Named CSV records are created by {@link NamedCsvReader}. + * + * @see NamedCsvReader + * @see CsvReader */ @SuppressWarnings("PMD.ArrayIsStoredDirectly") public final class NamedCsvRecord extends CsvRecord { @@ -26,9 +33,13 @@ public final class NamedCsvRecord extends CsvRecord { } /** - * Gets a field value by its name (first occurrence if duplicates exists). + * Retrieves the value of a field by its case-sensitive name, considering the first occurrence in case of + * duplicates. + *

+ * This method is equivalent to {@code findField(name).orElseThrow(NoSuchElementException::new)} although a + * more explanatory exception message is provided. * - * @param name field name + * @param name case-sensitive name of the field to be retrieved * @return field value, never {@code null} * @throws NoSuchElementException if this record has no such field * @throws NullPointerException if name is {@code null} @@ -37,17 +48,23 @@ public final class NamedCsvRecord extends CsvRecord { */ public String getField(final String name) { final int fieldPos = findHeader(name); + + // Check if the field position is valid if (fieldPos == -1) { throw new NoSuchElementException(MessageFormat.format( "Header does not contain a field ''{0}''. Valid names are: {1}", name, Arrays.toString(header))); - } else if (fieldPos >= fields.length) { + } + if (fieldPos >= fields.length) { throw new NoSuchElementException(MessageFormat.format( "Field ''{0}'' is on position {1}, but current record only contains {2} fields", name, fieldPos + 1, fields.length)); } + + // Return the value of the field return fields[fieldPos]; } + // Finds the position of the first occurrence of the given header name (case-sensitive); returns -1 if not found private int findHeader(final String name) { for (int i = 0; i < header.length; i++) { final String h = header[i]; @@ -60,25 +77,33 @@ private int findHeader(final String name) { } /** - * Finds a field value by its name (first occurrence if duplicates exists). + * Retrieves the value of a field by its case-sensitive name, considering the first occurrence in case of + * duplicates. + *

+ * This method is equivalent to {@code findFields(name).stream().findFirst()} but more performant. * - * @param name field name - * @return the field value ({@link Optional#empty()} if record doesn't contain that field), never {@code null} + * @param name case-sensitive name of the field to be retrieved + * @return An {@code Optional} containing the value of the field if found, + * or an empty {@code Optional} if the field is not present. Never returns {@code null}. * @throws NullPointerException if name is {@code null} * @see #findFields(String) */ public Optional findField(final String name) { final int fieldPos = findHeader(name); + + // Check if the field position is valid if (fieldPos == -1 || fieldPos >= fields.length) { return Optional.empty(); } + + // Return the value of the field wrapped in an Optional return Optional.of(fields[fieldPos]); } /** - * Builds a list of field values found by its name. + * Collects all field values with the given name (case-sensitive) in the order they appear in the header. * - * @param name field name + * @param name case-sensitive name of the field to collect values for * @return the field values (empty list if record doesn't contain that field), never {@code null} * @throws NullPointerException if name is {@code null} */ @@ -94,13 +119,16 @@ public List findFields(final String name) { } /** - * Builds an ordered map of header names and field values (first occurrence if duplicates exists) - * of this record. + * Constructs an ordered map, associating header names with corresponding field values of this record, + * considering the first occurrence in case of duplicates. *

- * The map will contain only entries for fields that have a key and a value – - * no map entry will have a {@code null} key or value. + * The constructed map will only contain entries for fields that have a key and a value. No map entry will have a + * {@code null} key or value. + *

+ * If you need to collect all fields with the same name (duplicate header), use {@link #getFieldsAsMapList()}. * * @return an ordered map of header names and field values of this record, never {@code null} + * @see #getFieldsAsMapList() */ public Map getFieldsAsMap() { final int bound = commonSize(); @@ -112,12 +140,16 @@ public Map getFieldsAsMap() { } /** - * Builds an unordered map of header names and field values of this record. + * Constructs an unordered map, associating header names with an ordered list of corresponding field values of + * this record. + *

+ * The constructed map will only contain entries for fields that have a key and a value. No map entry will have a + * {@code null} key or value. *

- * The map will contain only entries for fields that have a key and a value – - * no map entry will have a {@code null} key or value. + * If you don't have to handle duplicate headers, you may simply use {@link #getFieldsAsMap()}. * * @return an unordered map of header names and field values of this record, never {@code null} + * @see #getFieldsAsMap() */ @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") public Map> getFieldsAsMapList() { @@ -135,6 +167,7 @@ public Map> getFieldsAsMapList() { return map; } + // Mappings will only be created for fields that have a key and a value – return the minimum of both sizes private int commonSize() { return Math.min(header.length, fields.length); }