Данни във файлове
Проблем
Текст, а трябват числа
Складът изнася продуктите си във файл. Всеки ред е един продукт: име, цена и наличност, разделени със запетаи. Първият ред казва какво е във всяка колона:
Име,Цена,НаличностХляб,1.20,30Мляко,2.10,18Кафе на зърна,18.90,12Вече можеш да прочетеш файла. Но за програмата редът е само текст:
String line = "Хляб,1.20,30";System.out.println(line.length());System.out.println(line.charAt(5));121Тя не знае, че 1.20 е цена, а 30 е брой. За да сметнеш колко струва стоката, трябва да превърнеш всеки ред в нещо с полета: в обект Product.
Идеята
CSV: ред към обект
Такъв файл се казва CSV (comma-separated values, „стойности, разделени със запетая“). Чете се от Excel, от бази данни, от всяка програма. Един ред става обект в три стъпки: split, превръщане на всяка част в нужния тип, конструктор.
class Product { String name; double price; int stock;
Product(String name, double price, int stock) { this.name = name; this.price = price; this.stock = stock; }
static Product fromCsv(String line) { String[] f = line.split(","); if (f.length != 3) { throw new IllegalArgumentException("очаквам 3 колони, а са " + f.length); } return new Product(f[0].strip(), Double.parseDouble(f[1].strip()), Integer.parseInt(f[2].strip())); }
String toCsv() { return name + "," + price + "," + stock; }}Product p = Product.fromCsv("Кафе на зърна,18.90,12");System.out.printf("%s: %.2f € x %d = %.2f €%n", p.name, p.price, p.stock, p.price * p.stock);Кафе на зърна: 18.90 € x 12 = 226.80 €fromCsv е единственото място, което знае как изглежда редът. Ако форматът се промени, променяш само него. Проверката за броя на колоните хвърля IllegalArgumentException с ясно съобщение, вместо програмата да спре по-късно с индекс извън масива.
Какво прави fromCsv с реда, стъпка по стъпка, с тялото му, разписано в main:
String line = "Кафе на зърна,18.90,12";String[] f = line.split(",");String name = f[0];double price = Double.parseDouble(f[1]);int stock = Integer.parseInt(f[2]);Product p = new Product(name, price, stock);System.out.println(p.name + ": " + p.price + " € x " + p.stock);System.out.printf("%.2f €%n", p.price * p.stock);Стъпка по стъпка, с думи
- Редът от файла е един текст. За програмата 18.90 в него е само четири знака.
- split реже реда при всяка запетая: масив от три текста.
- Името си остава текст.
- parseDouble превръща текста "18.90" в числото 18.9. С него вече може да се смята.
- parseInt прави от "12" цялото число 12.
- Конструкторът слага трите стойности в полетата на нов Product, а p сочи към него.
- p.price се печата като 18.9: нулата в края беше само в текста.
- Цената по наличността: вече е сметка с числа.
Виж го
Целият файл
Path file = Path.of("products.csv");try { Files.writeString(file, "Име,Цена,Наличност\nХляб,1.20,30\nМляко,2.10,18\nКафе на зърна,18.90,12\n"); List<String> lines = Files.readAllLines(file); Product[] products = new Product[lines.size() - 1]; int count = 0; boolean header = true; for (String line : lines) { if (header) { header = false; continue; } products[count] = Product.fromCsv(line); count++; } double value = 0; for (int i = 0; i < count; i++) { value += products[i].price * products[i].stock; } System.out.printf("%d продукта, стока за %.2f €%n", count, value);} catch (IOException e) { System.out.println("Не мога да прочета " + file + ": " + e.getMessage());}3 продукта, стока за 300.60 €- Масивът е с един елемент по-малко от редовете, защото първият ред е заглавен.
headerеtrueсамо за първото завъртане. Тогава редът се пропуска сcontinue.countброи колко продукта са сложени в масива досега и е индексът за следващия.
В модул „Колекции“ ще видиш списък, който расте сам, и броенето с count няма да ти трябва.
Идеята
Грешни редове
Файловете ги пишат хора и други програми. Рано или късно идва ред без колона, дума вместо число или празен ред:
Име,Цена,НаличностХляб,1.20,30Мляко,2.10Кафе,седем,4
Чай,3.50,25Една грешка не трябва да спира зареждането на всичко останало. Всеки ред е в свой try, а съобщението казва номера на реда, за да може някой да го поправи:
Path file = Path.of("products.csv");try { Files.writeString(file, "Име,Цена,Наличност\nХляб,1.20,30\nМляко,2.10\nКафе,седем,4\n\nЧай,3.50,25\n"); List<String> lines = Files.readAllLines(file); Product[] products = new Product[lines.size()]; int count = 0; int row = 0; for (String line : lines) { row++; if (row == 1 || line.isBlank()) { continue; } try { products[count] = Product.fromCsv(line); count++; } catch (NumberFormatException e) { System.out.println("Ред " + row + ": грешно число (" + e.getMessage() + ")"); } catch (IllegalArgumentException e) { System.out.println("Ред " + row + ": " + e.getMessage()); } } System.out.println("Заредени: " + count);} catch (IOException e) { System.out.println("Не мога да прочета " + file + ": " + e.getMessage());}Ред 3: очаквам 3 колони, а са 2Ред 4: грешно число (For input string: "седем")Заредени: 2- Празните редове се пропускат с
isBlank(). NumberFormatExceptionе наследник наIllegalArgumentException, затова е пръв. Иначе общиятcatchби хванал и него.- Програмата казва „Заредени: 2“ и продължава с тях.
Пробвай
Празни колони
Какво ще отпечата програмата?
System.out.println("Кафе,,4".split(",").length);System.out.println("Кафе,7.50,".split(",").length);System.out.println("Кафе,7.50,".split(",", -1).length);,,) остава като "". Празните части в края split ги маха, затова "Кафе,7.50," дава само 2 части и f[2] би хвърлил изключение. С втори аргумент -1 празните части в края остават. Във fromCsv това значи: ред с празна последна колона ще се отчете като ред с 2 колони.Идеята
Обекти към CSV
Обратната посока е метод, който сглобява реда:
String toCsv() { return name + "," + price + "," + stock;}Записваш заглавния ред и после по един ред за всеки обект:
Product[] stock = { new Product("Хляб", 1.2, 30), new Product("Мляко", 2.1, 18) };Path file = Path.of("stock.csv");try { try (BufferedWriter out = Files.newBufferedWriter(file)) { out.write("Име,Цена,Наличност"); out.newLine(); for (Product p : stock) { out.write(p.toCsv()); out.newLine(); } } System.out.print(Files.readString(file));} catch (IOException e) { System.out.println("Не мога да запиша " + file + ": " + e.getMessage());}Име,Цена,НаличностХляб,1.2,30Мляко,2.1,18fromCsv може да прочете този файл обратно. Записът и четенето трябва да ползват един и същ формат.
Внимавай, ако записваш цените със String.format. Той следва езиковите настройки на компютъра, а на български десетичният знак е запетая:
String bg = String.format(Locale.of("bg"), "%s,%.2f,%d", "Хляб", 1.2, 30);System.out.println(bg + " -> " + bg.split(",").length + " колони");String root = String.format(Locale.ROOT, "%s,%.2f,%d", "Хляб", 1.2, 30);System.out.println(root + " -> " + root.split(",").length + " колони");Хляб,1,20,30 -> 4 колониХляб,1.20,30 -> 3 колони1,20 става две колони и редовете се разместват. За файлове, които друга програма ще чете, подай Locale.ROOT (java.util.Locale). Той дава точка винаги, на всеки компютър.
Пробвай
Намери грешката
Програмата трябва да зареди трите продукта от products.csv, а спира още на първия ред:
Exception in thread "main" java.lang.NumberFormatException: For input string: "Цена"Избери реда с грешката.
row става 1 още в първото завъртане, преди проверката, затова row == 0 никога не е вярно. Заглавният ред не се пропуска и fromCsv се опитва да превърне думата „Цена“ в число. Поправката е row == 1.
Идеята
Колони по име
Друга програма изнася същите данни, но с колоните в друг ред и с една повече. Ако пишеш f[1] за цената, при нов формат ще четеш грешна колона и няма да разбереш. По-сигурно е да намериш колоната по името ѝ в заглавния ред:
static int column(String[] header, String name) { for (int i = 0; i < header.length; i++) { if (header[i].strip().equalsIgnoreCase(name)) { return i; } } throw new IllegalArgumentException("Няма колона " + name);}
Path file = Path.of("export.csv");try { Files.writeString(file, "Наличност,Име,Код,Цена\n30,Хляб,1001,1.20\n18,Мляко,1002,2.10\n"); int nameCol = -1; int priceCol = -1; for (String line : Files.readAllLines(file)) { String[] f = line.split(","); if (nameCol == -1) { nameCol = column(f, "име"); priceCol = column(f, "цена"); continue; } System.out.println(f[nameCol] + ": " + f[priceCol] + " €"); }} catch (IOException e) { System.out.println(e.getMessage());}Хляб: 1.20 €Мляко: 2.10 €Първото завъртане само намира къде са колоните. След това всеки ред се чете по тези индекси. Ако колоната я няма, column хвърля изключение веднага, вместо да чете грешни данни.
По-точно, за любопитните
- Запетая в стойността. „Сирене, краве“ съдържа запетая. В CSV такава стойност се огражда с кавички:
"Сирене, краве",9.80,5, а кавичка вътре се пише двойна:"". Тогаваsplit(",")вече не стига. В истински проекти се ползва библиотека като OpenCSV или Apache Commons CSV, която знае всички правила. - Друг разделител. Excel на компютър с български настройки често записва CSV с
;, именно защото запетаята е десетичният знак. - Сериализация. Java може да запише цял обект във файл с
ObjectOutputStream, ако класът имплементираSerializable. Файлът тогава е двоичен: не се чете от човек и се чупи при промяна на класа. Текстовите формати като CSV и JSON са по-предвидими и затова се ползват повече.
Упражнение
Данни, които оживяват
Реши задачите в редактора си. Програмата първо сама създава входния файл.
1. Анализ на CSV. Създай people.csv със съдържанието по-долу. Отпечатай средната възраст (целочислено деление), кой е най-възрастен и колко души има от всеки град, в реда, в който градовете се появяват. Градовете пази в масив String[], а броевете в int[] със същия размер.
Име,Възраст,ГрадИван,34,СофияМария,28,ПловдивГеорги,41,СофияЕлена,25,ВарнаСредна възраст: 32Най-възрастен: Георги, 41Брой хора от София: 2Брой хора от Пловдив: 1Брой хора от Варна: 12. Запис на героя. Клас Hero с име, ниво, живот и злато. Метод save(Path file) записва героя като редове ключ=стойност, а статичен метод load(Path file) създава нов Hero от такъв файл. Създай „Арин“ с ниво 6, 100 живот и 120 злато, вдигни му нивото с 1, махни 15 живот, дай му 220 злато, запиши го, отпечатай файла и го зареди обратно.
name=Аринlevel=7health=85gold=340Заредих: Арин, ниво 7, живот 85, злато 3403. Дублиран текст. Създай text.txt с текста по-долу. Раздели го на изречения по точката (и интервалите след нея) и отпечатай всяко изречение, което се среща повече от веднъж, по веднъж.
Това е текст. Това също е текст. Това е текст.Дублиран текст: "Това е текст."Още задачи
- Най-скъпият продукт. Зареди
products.csvот урока и отпечатай продукта с най-висока цена и колко продукта имат наличност под 20. - Поправени редове. Зареди файла с грешните редове и запиши верните в
products-clean.csv, а грешните, с номера и причината, вrejected.txt. - Отчет за продажбите. Файл
sales.csvс колониДата,Продукт,Брой,Цена. Редовете са подредени по дата. Отпечатай оборота за всеки ден със String.format в колони.
Тест