
de.siegmar.fastcsv.reader.NamedCsvRecord Maven / Gradle / Ivy
Show all versions of fastcsv Show documentation
package de.siegmar.fastcsv.reader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.StringJoiner;
/**
* Represents an immutable CSV record with named (and indexed) fields.
*
* The field values are never {@code null}. Empty fields are represented as empty strings.
*
* @see CsvReader
*/
@SuppressWarnings("PMD.ArrayIsStoredDirectly")
public final class NamedCsvRecord extends CsvRecord {
private final String[] header;
@SuppressWarnings("PMD.UseVarargs")
NamedCsvRecord(final long startingLineNumber, final String[] fields, final boolean comment,
final String[] header) {
super(startingLineNumber, fields, comment);
this.header = header;
}
/**
* Retrieves the header names of this record.
*
* The header names are returned in the order they appear in the CSV file.
*
* Note that the header names are not necessarily unique.
* If you need to collect all fields with the same name (duplicate header), use {@link #getFieldsAsMapList()}.
*
* Note that records for commented lines ({@link #isComment()}) do not have an empty header.
* To retrieve the comment value, user {@link #getField(int)} with index 0.
*
* @return the header names, never {@code null}
*/
public List getHeader() {
return Collections.unmodifiableList(Arrays.asList(header));
}
/**
* 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 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}
* @see #findField(String)
* @see #findFields(String)
*/
public String getField(final String name) {
final int fieldIdx = findHeaderIndex(name);
// Check if the field index is valid
if (fieldIdx == -1) {
throw new NoSuchElementException(String.format(
"Header does not contain a field '%s'. Valid names are: %s", name, Arrays.toString(header)));
}
if (fieldIdx >= fields.length) {
throw new NoSuchElementException(String.format(
"Field '%s' is on index %d, but current record only contains %d fields",
name, fieldIdx, fields.length));
}
// Return the value of the field
return fields[fieldIdx];
}
// Finds the index for the first occurrence of the given header name (case-sensitive); returns -1 if not found
private int findHeaderIndex(final String name) {
for (int i = 0; i < header.length; i++) {
if (name.equals(header[i])) {
return i;
}
}
return -1;
}
/**
* 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 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 fieldIdx = findHeaderIndex(name);
// Check if the field index is valid
if (fieldIdx == -1 || fieldIdx >= fields.length) {
return Optional.empty();
}
// Return the value of the field wrapped in an Optional
return Optional.of(fields[fieldIdx]);
}
/**
* Collects all field values with the given name (case-sensitive) in the order they appear in the header.
*
* @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}
*/
public List findFields(final String name) {
final int bound = header.length;
final List ret = new ArrayList<>(bound);
for (int i = 0; i < bound; i++) {
if (name.equals(header[i])) {
ret.add(fields[i]);
}
}
return ret;
}
/**
* Constructs an ordered map, associating header names with corresponding field values of this record,
* considering the first occurrence in case of duplicates.
*
* 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();
final Map map = new LinkedHashMap<>(bound);
for (int i = 0; i < bound; i++) {
map.putIfAbsent(header[i], fields[i]);
}
return map;
}
/**
* Constructs an unordered map, associating header names with an ordered list of corresponding field values in
* 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.
*
* 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() {
final int bound = commonSize();
final Map> map = new HashMap<>(bound);
for (int i = 0; i < bound; i++) {
final String key = header[i];
List val = map.get(key);
if (val == null) {
val = new LinkedList<>();
map.put(key, val);
}
val.add(fields[i]);
}
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);
}
@Override
public String toString() {
return new StringJoiner(", ", NamedCsvRecord.class.getSimpleName() + "[", "]")
.add("startingLineNumber=" + startingLineNumber)
.add("fields=" + Arrays.toString(fields))
.add("comment=" + comment)
.add("header=" + Arrays.toString(header))
.toString();
}
}