Данни във файлове
Проблем
Текст, а трябват числа
Складът изнася продуктите си във файл. Всеки ред е един продукт: име, цена и наличност, разделени със запетаи. Първият ред казва какво е във всяка колона:
Име,Цена,НаличностХляб,1.20,30Мляко,2.10,18Кафе на зърна,18.90,12Вече можеш да прочетеш файла. Но за програмата редът е само текст:
const line = "Хляб,1.20,30";console.log(line.length);console.log(line[5]);121Тя не знае, че 1.20 е цена, а 30 е брой. За да сметнеш колко струва стоката, трябва да превърнеш всеки ред в нещо с полета: в обект Product.
Идеята
CSV: ред към обект
Такъв файл се казва CSV (comma-separated values, „стойности, разделени със запетая“). Чете се от Excel, от бази данни, от всяка програма. Един ред става обект в три стъпки: split, превръщане на всяка част в нужния тип, конструктор.
static) в самия клас:function toNumber(text) { const n = Number(text); if (text.trim() === "" || Number.isNaN(n)) { throw new Error(`грешно число (${text.trim()})`); } return n;}
class Product { constructor(name, price, stock) { this.name = name; this.price = price; this.stock = stock; }
static fromCsv(line) { const f = line.split(","); if (f.length !== 3) { throw new Error(`очаквам 3 колони, а са ${f.length}`); } return new Product(f[0].trim(), toNumber(f[1]), toNumber(f[2])); }
toCsv() { return `${this.name},${this.price},${this.stock}`; }}const p = Product.fromCsv("Кафе на зърна,18.90,12");console.log(`${p.name}: ${p.price.toFixed(2)} € x ${p.stock} = ${(p.price * p.stock).toFixed(2)} €`);Кафе на зърна: 18.90 € x 12 = 226.80 €fromCsv е единственото място, което знае как изглежда редът. Ако форматът се промени, променяш само него. Проверката за броя на колоните хвърля грешка с ясно съобщение, вместо програмата да спре по-късно с неясна.
Number("седем") не хвърля грешка, а дава NaN („не е число“), а Number("") дава 0. Програма, която не проверява, тихо смята с грешни числа. Затова toNumber проверява текста и хвърля сама.
Какво прави fromCsv с реда, стъпка по стъпка, с тялото му, разписано ред по ред:
const line = "Кафе на зърна,18.90,12";const f = line.split(",");const name = f[0];const price = Number(f[1]);const stock = Number(f[2]);const p = new Product(name, price, stock);console.log(`${p.name}: ${p.price} € x ${p.stock}`);console.log(`${(p.price * p.stock).toFixed(2)} €`);Стъпка по стъпка, с думи
- Редът от файла е един текст. За програмата 18.90 в него е само четири знака.
- split реже реда при всяка запетая: масив от три текста.
- Името си остава текст.
- Number превръща текста "18.90" в числото 18.9. С него вече може да се смята.
- Number прави от "12" числото 12. В JavaScript и цената, и бройката са number.
- new Product слага трите стойности в полетата на нов обект, а p сочи към него.
- p.price се печата като 18.9: нулата в края беше само в текста.
- Цената по наличността: вече е сметка с числа.
Виж го
Целият файл
import { readFileSync, writeFileSync } from "node:fs"; е най-горе и не се показва:try { writeFileSync("products.csv", "Име,Цена,Наличност\nХляб,1.20,30\nМляко,2.10,18\nКафе на зърна,18.90,12\n"); const lines = readFileSync("products.csv", "utf8").trimEnd().split("\n"); const products = []; let header = true; for (const line of lines) { if (header) { header = false; continue; } products.push(Product.fromCsv(line)); } let value = 0; for (const p of products) { value += p.price * p.stock; } console.log(`${products.length} продукта, стока за ${value.toFixed(2)} €`);} catch (e) { console.log(`Не мога да прочета products.csv: ${e.message}`);}3 продукта, стока за 300.60 €productsзапочва като празен масив, аpushдобавя всеки нов продукт в края му.products.lengthе колко са.headerеtrueсамо за първото завъртане. Тогава редът се пропуска сcontinue.
Повече за масивите, които растат, има в модул „Колекции“.
Идеята
Грешни редове
Файловете ги пишат хора и други програми. Рано или късно идва ред без колона, дума вместо число или празен ред:
Име,Цена,НаличностХляб,1.20,30Мляко,2.10Кафе,седем,4
Чай,3.50,25Една грешка не трябва да спира зареждането на всичко останало. Всеки ред е в свой try, а съобщението казва номера на реда, за да може някой да го поправи:
try { writeFileSync("products.csv", "Име,Цена,Наличност\nХляб,1.20,30\nМляко,2.10\nКафе,седем,4\n\nЧай,3.50,25\n"); const lines = readFileSync("products.csv", "utf8").trimEnd().split("\n"); const products = []; let row = 0; for (const line of lines) { row++; if (row === 1 || line.trim() === "") { continue; } try { products.push(Product.fromCsv(line)); } catch (e) { console.log(`Ред ${row}: ${e.message}`); } } console.log(`Заредени: ${products.length}`);} catch (e) { console.log(`Не мога да прочета products.csv: ${e.message}`);}Ред 3: очаквам 3 колони, а са 2Ред 4: грешно число (седем)Заредени: 2- Празните редове се пропускат с
line.trim() === "". - Един
catchхваща и двете грешки, коитоfromCsvхвърля: за броя на колоните и отtoNumber. Кое е станало, казваe.message. - Програмата казва „Заредени: 2“ и продължава с тях.
Пробвай
Празни колони
Какво ще отпечата програмата?
console.log("Кафе,,4".split(",").length);console.log("Кафе,7.50,".split(",").length);console.log(Number(""));"", и по средата (,,), и в края: "Кафе,7.50," дава 3 части (в Java щяха да са 2). Но Number("") е 0, а не NaN: празна цена би станала 0 € без никаква грешка. Затова toNumber проверява празния текст отделно.Идеята
Обекти към CSV
Обратната посока е метод, който сглобява реда:
toCsv() { return `${this.name},${this.price},${this.stock}`;}Записваш заглавния ред и после по един ред за всеки обект:
const stock = [new Product("Хляб", 1.2, 30), new Product("Мляко", 2.1, 18)];try { let text = "Име,Цена,Наличност\n"; for (const p of stock) { text += `${p.toCsv()}\n`; } writeFileSync("stock.csv", text); process.stdout.write(readFileSync("stock.csv", "utf8"));} catch (e) { console.log(`Не мога да запиша stock.csv: ${e.message}`);}Име,Цена,НаличностХляб,1.2,30Мляко,2.1,18fromCsv може да прочете този файл обратно. Записът и четенето трябва да ползват един и същ формат.
Внимавай, ако форматираш цените с toLocaleString. Той следва езиковите настройки, а на български десетичният знак е запетая:
const bg = `Хляб,${(1.2).toLocaleString("bg-BG", { minimumFractionDigits: 2 })},30`;console.log(`${bg} -> ${bg.split(",").length} колони`);const fixed = `Хляб,${(1.2).toFixed(2)},30`;console.log(`${fixed} -> ${fixed.split(",").length} колони`);Хляб,1,20,30 -> 4 колониХляб,1.20,30 -> 3 колони1,20 става две колони и редовете се разместват. За файлове, които друга програма ще чете, ползвай toFixed(2). Той дава точка винаги, на всеки компютър.
Пробвай
Намери грешката
Програмата трябва да зареди трите продукта от products.csv, а спира още на първия ред:
Error: грешно число (Цена)Избери реда с грешката.
row става 1 още в първото завъртане, преди проверката, затова row === 0 никога не е вярно. Заглавният ред не се пропуска и fromCsv се опитва да превърне думата „Цена“ в число. Поправката е row === 1.
Идеята
Колони по име
Друга програма изнася същите данни, но с колоните в друг ред и с една повече. Ако пишеш f[1] за цената, при нов формат ще четеш грешна колона и няма да разбереш. По-сигурно е да намериш колоната по името ѝ в заглавния ред:
function column(header, name) { for (let i = 0; i < header.length; i++) { if (header[i].trim().toLowerCase() === name.toLowerCase()) { return i; } } throw new Error(`Няма колона ${name}`);}
try { writeFileSync("export.csv", "Наличност,Име,Код,Цена\n30,Хляб,1001,1.20\n18,Мляко,1002,2.10\n"); let nameCol = -1; let priceCol = -1; for (const line of readFileSync("export.csv", "utf8").trimEnd().split("\n")) { const f = line.split(","); if (nameCol === -1) { nameCol = column(f, "име"); priceCol = column(f, "цена"); continue; } console.log(`${f[nameCol]}: ${f[priceCol]} €`); }} catch (e) { console.log(e.message);}Хляб: 1.20 €Мляко: 2.10 €Първото завъртане само намира къде са колоните. След това всеки ред се чете по тези индекси. Ако колоната я няма, column хвърля изключение веднага, вместо да чете грешни данни.
По-точно, за любопитните
- Запетая в стойността. „Сирене, краве“ съдържа запетая. В CSV такава стойност се огражда с кавички:
"Сирене, краве",9.80,5, а кавичка вътре се пише двойна:"". Тогаваsplit(",")вече не стига. Node няма вграден модул за CSV. В истински проекти се ползва библиотека катоcsv-parseили Papa Parse, която знае всички правила. - Друг разделител. Excel на компютър с български настройки често записва CSV с
;, именно защото запетаята е десетичният знак. Тогава редът се реже съсsplit(";"). - JSON. За данни, които само твоя програма ще чете, в JavaScript най-често се ползва JSON:
JSON.stringify(product)прави от обекта текст, аJSON.parse(text)го връща обратно.
Упражнение
Данни, които оживяват
Реши задачите в редактора си. Програмата първо сама създава входния файл.
1. Анализ на CSV. Създай people.csv със съдържанието по-долу. Отпечатай средната възраст (целочислено деление), кой е най-възрастен и колко души има от всеки град, в реда, в който градовете се появяват. Градовете пази в един масив, а броевете в друг, на същите индекси.
Име,Възраст,ГрадИван,34,СофияМария,28,ПловдивГеорги,41,СофияЕлена,25,ВарнаСредна възраст: 32Най-възрастен: Георги, 41Брой хора от София: 2Брой хора от Пловдив: 1Брой хора от Варна: 12. Запис на героя. Клас Hero с име, ниво, живот и злато. Метод save(path) записва героя като редове ключ=стойност, а статичен метод load(path) създава нов 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с колониДата,Продукт,Брой,Цена. Редовете са подредени по дата. Отпечатай оборота за всеки ден с padEnd и toFixed в колони.
Тест