-
Notifications
You must be signed in to change notification settings - Fork 5
JDD: CSV
CSV — текстовый формат, придуманный для обмена и преобразования данных между разными программами, которые работают с табличным представлением данных (например, spreadsheet programs).
В этом формате, каждая строчка в файле — это одна строка таблицы, а разделителем (delimiter) полей (колонок, столбцов) служит чаще всего символ запятой (,), однако на практике могут использоваться и другие разделители. Если разделитель встречается в значении ячейки, то значение обрамляется двойными кавычками. Если в значении встречаются кавычки — они представляются в файле в виде двух кавычек подряд (Samsung U600 24" будет выглядеть как "Samsung U600 24""").
Рассмотрим, пример табличного представления данных, которое нужно представить в CSV:
В этом примере видим 3 строки (rows) и 4 поля (fields). Каждая строка должна иметь одинаковое количество полей. Первая строка, с точки зрения пользователя, логически представляет собой строку-заголовок (header), а две последующие строки — строки-значения (values).
В этом примере, у таблицы есть заголовок, но его может и вовсе не быть. Если вы получили файл в CSV формате, то узнать есть ли в нем заголовок или нет, можно только когда вы его откроете и сами убедитесь, или же заранее договоритесь с поставщиком CSV, о том что всякий файл в этом формате будет всегда содержать строку-заголовок. Такого понятия как строка-заголовок в CSV нет, технически это строка-значение, которая логически выполняет роль заголовка.
Итоговый CSV с использованием запятой как разделителя, будет выглядеть следующим образом:
Фамилия,Имя,Отчество,Телефон
Иванов,Иван,Иванович,"+7 906 710 56 10, 222-22-22"
Петров,Петр,Петрович,01;02
Как видите, из-за того что значение колонки с телефоном для второй строки содержит знак запятой, пришлось его обрамить в двойные кавычки. Также нужно отметить, что CSV никак не обозначает, какие типы данных он содержит. Если логически мы имеем строки, числа, даты и т.д., то в CSV это просто строки.
CSV хранятся в виде текстовых файлов, а вот работать с ними в коде может быть удобнее в виде какой-то абстрации. Так как в Java у нас все является обьектом, и принято программировать в парадигме ООП, то абстракции здесь строят через классы. Поэтому, создадим класс с названием Csv, который будет инкапсулировать в себе поля header и values, а также геттеры header() и values() для того чтобы получать эти поля:
public class Csv {
private String[] header;
private String[][] values;
public Csv(String[] header, String[][] values) {
this.header = header;
this.values = values;
}
public String[] header() {
return this.header;
}
public String[][] values() {
return this.values;
}
}
Если у вас Java 15 или выше, то код можно упростить, используя record-class
public record Csv(String[] header, String[][] values) {}
А, для того чтобы превращать Csv обьект в файл и наоборот, можно создать класс-хелпер, который будет иметь методы со следующей сигнатурой:
public class CsvHelper {
public static Csv parseFile(FileReader reader) throws FileNotFoundException {
// Передаем FileReader для какого-то CSV файла, с которого построчно читаем строку-header и строки-values и с помощью них строим CSV обьект, который потом возвращаеи
}
public static void writeCsv(FileWriter writer, Csv csv) throws IOException {
// Передаем FileWriter (с помощь. когторого будем писать в файл) и CSV обьект, header и values поля которого превращаем в строки, со значениями, разделенными запятой
}
}
Если продолжать строить абстракции дальше и попробовать реализовать очень простое подобие БД над нашим классом Csv для выполнения над ним простых операций, на подобии тех, которые существуют в языке SQL (select/insert/update/delete/join), то эти операции можно представить в виде соответсвующих классов с прибавлением суфикса, например, в нашем случае - суфиксом Request (суфикс в названии служит просто для их визуального выделения среди других классов, и может быть Operation, или какой-либо другой):
public class InsertRequest implements Request<Csv> {
public InsertRequest(Csv target, String[] line) {
// TODO
}
@Override
public Csv execute() {
// TODO
}
}
public class DeleteRequest implements Request<Csv> {
public DeleteRequest(Csv target, Selector whereSelector) {
// TODO
}
@Override
public Csv execute() {
// TODO
}
}
public class UpdateRequest implements Request<Csv> {
public UpdateRequest(Csv target, Selector whereSelector, Selector updateToSelector) {
// TODO
}
@Override
public Csv execute() {
// TODO
}
}
public class SelectRequest implements Request<Csv> {
public SelectRequest(Csv target, Selector selector, String[] columns) {
// TODO
}
@Override
public String[][] execute() {
// TODO
}
}
public class JoinRequest implements Request<Csv> {
public JoinRequest(Csv from, Csv on, String by) {
// TODO
}
@Override
public Csv execute() {
// TODO
}
}
Все эти классы-операции обьединяет то, что они должны реализовать метод execute()
интерфейса Request
, в котором у каждого класса происходит непосредственная логика их работа над Csv:
public interface Request<T> {
abstract T execute();
}
Также в некоторых классах используется вспомогательный record-класс Selector, который инкапсуляцирует название какого-то поля и его значение.
public record Selector(String fieldName, String value) {}
Принимает в конструктор target типа Csv и line типа массив строк, что логически представляют собой строку-значение. При выполнении, должен создать новый обьект типа Csv, но уже с учетом добавленой этой новой строки-значения
Дано:
target (Csv):
name, surname, age
Petr, Petrov, 18
line (String[]):
Vasya, Vasiliev, 19
Результат:
name, surname, age
Petr, Petrov, 18
Vasya, Vasiliev, 19
Принимает в конструктор target типа Csv и whereSelector типа Selector, что логически представляют собой название поля (fieldName) и его значение (value). При выполнении, удалить из target любую строку-значение, у которой значение поля совпадает с value
Дано:
target (Csv):
name, surname, age
Petr, Petrov, 18
Vasya, Vasiliev, 19
whereSelector:
fieldName: name
value Vasya
Результат:
name, surname, age
Petr, Petrov, 18
В результати из Csv удалили целую строку Vasya, Vasiliev, 19
, т.к. у нее значение поля совпало с переданным value.
Работает по похожему принципу как DeleteRequest, но только у строки, у которой значение поля совпало с переданным value обновляет поле на значения с селектора updateToSelector
Дано:
target (Csv):
name, surname, age
Petr, Petrov, 18
Vasya, Vasiliev, 19
whereSelector:
fieldName: name
value Vasya
updateToSelector:
fieldName: age
value: 31
Результат:
name, surname, age
Petr, Petrov, 18
Vasya, Vasiliev, 31
Csv target, Selector selector, String[] columns Принимает в конструктор target типа Csv и selector типа Selector, что логически представляют собой название поля (fieldName) и его значение (value), а также названия полей (полей) с именем columns в виде массива строк. При выполнении, ищет в target все строки-значения, которые удовлетвоняют селектор selector, и потом позвращает только те поля, названия которых есть в массиве аргумента columns
Пример 1.
Дано:
target (Csv):
name, surname, age
Petr, Petrov, 18
Vasya, Vasiliev, 19
selector:
fieldName: surname
value Petrov
columns:
[name, surname]
Результат:
[
[Petr, Petrov]
]
Пример 2.
Дано:
target (Csv):
name, surname, age
Petr, Petrov, 18
Vasya, Vasiliev, 18
selector:
fieldName: surname
value Petrov
columns:
[]
Результат:
[
[Petr, Petrov, 18]
]
Пример 3.
Дано:
target (Csv):
name, surname, age
Petr, Petrov, 18
Vasya, Vasiliev, 18
selector:
fieldName: age
value 18
columns:
[name]
Результат:
[
[Petr],
[Vasya]
]
Получает два Cvs и обьединяет их по полю by
Дано:
from (Csv):
name, surname, age
Petr, Petrov, 18
Vasya, Vasiliev, 18
on (Csv):
name, city, pet
Vasya, Berlin, dog
Petr, London, cat
by (String):
name
Результат:
name, surname, age, city, pet
Petr, Petrov, 18, London, cat
Vasya, Vasiliev, 18, Berlin, dog