Строка выгрузки: поля и ошибки
Выгрузка заказов — строки вида TK-101;Болт М8;12;1500: артикул, название, количество в штуках и цена в копейках. Часть строк битая: поле потерялось, в количестве 12шт, в цене 9,90. Здесь строка режется на поля, а parseLine возвращает значения или ошибку.
Разрезать и почистить
Ситуация. Из строки нужны отдельные поля, а вокруг названия стоят лишние пробелы.
package main
import (
"fmt"
"strings"
)
func main() {
line := "TK-101; Болт М8 ;12;1500"
parts := strings.Split(line, ";")
fmt.Println(len(parts))
fmt.Printf("[%s]\n", parts[1])
fmt.Printf("[%s]\n", strings.TrimSpace(parts[1]))
fmt.Println(strings.Fields(" Болт М8 "))
fmt.Println(strings.ToLower("БОЛТ М8"))
fmt.Println(strings.Contains("Болт М8", "М8"))
fmt.Println(strings.HasPrefix("TK-101", "TK-"))
fmt.Println(strings.Join([]string{"Болт", "М8"}, " / "))
}Что увидите.
4
[ Болт М8 ]
[Болт М8]
[Болт М8]
болт м8
true
true
Болт / М8Как это читать. Квадратные скобки вокруг %s поставлены, чтобы увидеть пробелы по краям.
| Функция | Что делает | В примере |
|---|---|---|
strings.Split(s, ";") |
режет строку по разделителю и возвращает срез строк; разделитель пропадает. Частей на одну больше, чем разделителей | 4 части |
strings.TrimSpace(s) |
убирает пробелы, табуляции и переводы строки по краям, внутри не трогает | [ Болт М8 ] → [Болт М8] |
strings.Fields(s) |
режет по пробелам любой длины; пустых частей не бывает | срез из двух строк в квадратных скобках |
strings.ToLower(s) |
все буквы маленькие, русские тоже | болт м8 |
strings.Contains(s, part) |
есть ли кусок внутри строки | true |
strings.HasPrefix(s, start) |
начинается ли строка с куска | true |
strings.Join(parts, sep) |
склеивает срез строк через разделитель | Болт / М8 |
Функции strings понимают UTF-8: ToLower знает русские буквы, Split не разрежет букву. Кириллицу ломает только наша арифметика с байтами.
parseLine: значения или ошибка
Ситуация. Каждую строку выгрузки нужно превратить в название, количество и цену — или понять, что строка битая, и сказать чем.
package main
import (
"errors"
"fmt"
"strconv"
"strings"
)
func parseLine(line string) (string, int, int, error) {
if strings.TrimSpace(line) == "" {
return "", 0, 0, errors.New("пустая строка")
}
parts := strings.Split(line, ";")
if len(parts) != 4 {
return "", 0, 0, fmt.Errorf("нужно 4 поля, а их %d", len(parts))
}
name := strings.TrimSpace(parts[1])
qty, err := strconv.Atoi(strings.TrimSpace(parts[2]))
if err != nil {
return "", 0, 0, fmt.Errorf("количество: %v", err)
}
price, err := strconv.Atoi(strings.TrimSpace(parts[3]))
if err != nil {
return "", 0, 0, fmt.Errorf("цена: %v", err)
}
return name, qty, price, nil
}
func main() {
fmt.Println(parseLine("TK-101; Болт М8 ;12;1500"))
fmt.Println(parseLine(" "))
fmt.Println(parseLine("TK-102;Шайба;100"))
fmt.Println(parseLine("TK-103;Гайка М10;12шт;350"))
fmt.Println(parseLine("TK-104;Скоба;5;9,90"))
}Что увидите.
Болт М8 12 1500 <nil>
0 0 пустая строка
0 0 нужно 4 поля, а их 3
0 0 количество: strconv.Atoi: parsing "12шт": invalid syntax
0 0 цена: strconv.Atoi: parsing "9,90": invalid syntaxКак это читать. Четыре результата, ошибка — последним, при ошибке остальные нулевые — правило дня 12. У битых строк вывод начинается с пробела: название пустое.
Порядок проверок важен:
- Пустая строка — первой, иначе сообщение было бы «нужно 4 поля, а их 1»:
Splitпустой строки даёт одну пустую часть. - Число полей — до
parts[3], иначе строка из трёх полей роняет программу паникой — «Что может пойти не так». - Каждое поле — через
TrimSpace, иначе" 12"не станет числом. - Текст ошибки
Atoiсохраняется:%vвставляет его целиком.
Попробуйте сейчас: строка на поля.
Цель:
parseLineразбирает верную строку и называет, что не так с битыми.1. Наберите программу из раздела выше и запустите:
✎ Наберите в файлscratch/main.gopackage main import ( "errors" "fmt" "strconv" "strings" ) func parseLine(line string) (string, int, int, error) { if strings.TrimSpace(line) == "" { return "", 0, 0, errors.New("пустая строка") } parts := strings.Split(line, ";") if len(parts) != 4 { return "", 0, 0, fmt.Errorf("нужно 4 поля, а их %d", len(parts)) } name := strings.TrimSpace(parts[1]) qty, err := strconv.Atoi(strings.TrimSpace(parts[2])) if err != nil { return "", 0, 0, fmt.Errorf("количество: %v", err) } price, err := strconv.Atoi(strings.TrimSpace(parts[3])) if err != nil { return "", 0, 0, fmt.Errorf("цена: %v", err) } return name, qty, price, nil } func main() { fmt.Println(parseLine("TK-101; Болт М8 ;12;1500")) fmt.Println(parseLine(" ")) fmt.Println(parseLine("TK-102;Шайба;100")) fmt.Println(parseLine("TK-103;Гайка М10;12шт;350")) fmt.Println(parseLine("TK-104;Скоба;5;9,90")) }▶ Выполнитеcd ~/gocourse/day19/scratch go run .2. Допишите в
mainсвой случай — например, пустое количество:"TK-105;Скоба;;90". Запустите и прочитайте сообщение.Готово, когда: пять строк вывода совпали с выводом выше, а на ваш случай пришла понятная ошибка. Проверка дня опыт не сверяет.
Тест к двум функциям
parseLine и fitName со страницы 3 нужны отчёту. Они поедут в файл line.go каталога report, тест — рядом, в line_test.go, пакет у всех файлов main.
Табличный тест теперь читается целиком. cases := []struct{…}{…} — срез из дня 16; каждый элемент — запись с полями in, width, want (struct разберём в блоке 5). Новый случай — ещё одна строка {…},, цикл не меняется. Ниже — по одному случаю на функцию:
package main
import "testing"
func TestFitName(t *testing.T) {
cases := []struct {
in string
width int
want string
}{
{"Bolt-M8", 10, "Bolt-M8..."},
}
for _, c := range cases {
got := fitName(c.in, c.width)
if got != c.want {
t.Errorf("fitName(%q, %d) = %q, хотели %q", c.in, c.width, got, c.want)
}
}
}
func TestParseLineGood(t *testing.T) {
name, qty, price, err := parseLine("TK-101; Болт М8 ;12;1500")
if err != nil {
t.Fatalf("верная строка, а ошибка: %v", err)
}
if name != "Болт М8" || qty != 12 || price != 1500 {
t.Errorf("parseLine = %q, %d, %d", name, qty, price)
}
}%q печатает строку в кавычках — так видны точки и пробелы на краях. t.Fatalf останавливает тест сразу: при ошибке сравнивать поля незачем.
Что увидите.
stagiaire@lab:~/gocourse/day19/report$ go test -v .
=== RUN TestFitName
--- PASS: TestFitName (0.00s)
=== RUN TestParseLineGood
--- PASS: TestParseLineGood (0.00s)
PASS
ok day19/report 0.005sОдного случая мало. Тот же тест на неверных функциях: fitName считает длину через len и режет name[:width-1], как fix1 до починки, а parseLine на строке с неверным числом полей возвращает nil:
stagiaire@lab:~/gocourse/day19/report$ go test .
ok day19/report 0.005sУ Bolt-M8 длина в байтах и в буквах одна, а в верной строке четыре поля — ошибок на этих случаях не видно. Проверка дня подменяет line.go неверными версиями и ждёт, что ваш тест упадёт. Какой случай что ловит:
| Ошибка в функции | Случай, на котором она видна |
|---|---|
| точки дописываются по числу байтов | русское название короче ширины: точек меньше |
| длинное название режется по байтам | русское название длиннее ширины: последняя буква разрезана, %q покажет её обрубок вроде \xd1 |
| строка с неверным числом полей проходит | строка из трёх или пяти полей, а в тесте t.Errorf, если err == nil |
Ожидаемое want считайте руками, в буквах: тест, списавший ответ у функции, повторит её ошибку.
Попробуйте сейчас: line.go и тест к нему.
Цель: в
~/gocourse/day19/reportлежатline.goс двумя функциями иline_test.goс вашими случаями; тест зелёный и падает на неверных версиях функций.1. Прочитайте условие; ширина колонки в
line.goне нужна — её передают параметром.▶ Выполнитеcd ~/gocourse/day19/report cat TASK.txt2. Наберите
line.go— только эти две функции:✎ Наберите в файлreport/line.gopackage main import ( "errors" "fmt" "strconv" "strings" "unicode/utf8" ) func parseLine(line string) (string, int, int, error) { if strings.TrimSpace(line) == "" { return "", 0, 0, errors.New("пустая строка") } parts := strings.Split(line, ";") if len(parts) != 4 { return "", 0, 0, fmt.Errorf("нужно 4 поля, а их %d", len(parts)) } name := strings.TrimSpace(parts[1]) qty, err := strconv.Atoi(strings.TrimSpace(parts[2])) if err != nil { return "", 0, 0, fmt.Errorf("количество: %v", err) } price, err := strconv.Atoi(strings.TrimSpace(parts[3])) if err != nil { return "", 0, 0, fmt.Errorf("цена: %v", err) } return name, qty, price, nil } func fitName(name string, width int) string { n := utf8.RuneCountInString(name) if n > width { runes := []rune(name) return string(runes[:width-1]) + "~" } return name + strings.Repeat(".", width-n) }3. Наберите заготовку теста из раздела выше — по одному случаю на функцию:
✎ Наберите в файлreport/line_test.gopackage main import "testing" func TestFitName(t *testing.T) { cases := []struct { in string width int want string }{ {"Bolt-M8", 10, "Bolt-M8..."}, } for _, c := range cases { got := fitName(c.in, c.width) if got != c.want { t.Errorf("fitName(%q, %d) = %q, хотели %q", c.in, c.width, got, c.want) } } } func TestParseLineGood(t *testing.T) { name, qty, price, err := parseLine("TK-101; Болт М8 ;12;1500") if err != nil { t.Fatalf("верная строка, а ошибка: %v", err) } if name != "Болт М8" || qty != 12 || price != 1500 { t.Errorf("parseLine = %q, %d, %d", name, qty, price) } }▶ Выполнитеgo test -v .4. Допишите случаи сами — названия, ширину и строки выбирайте свои:
- в таблицу
TestFitName— русское название короче ширины и русское название длиннее ширины;- новой функцией
TestParseLine…— строку, где полей не четыре:errобязан быть неnil.▶ Выполнитеgo test -v .5. Проверьте силу теста приёмом из дня 14: испортите
line.go, запуститеgo test ., верните как было. Порча первая — вfitNameдве строки внутриifзамените однойreturn name[:width-1] + "~": упасть обязан случай с длинным русским названием. Порча вторая — вparseLineвместоfmt.Errorf("нужно 4 поля, а их %d", len(parts))поставьтеnil: упасть обязан случай с неверным числом полей.6. Закоммитьте:
▶ Выполнитеcd ~/gocourse/day19 git add . git commit -m "report: parseLine, fitName и тест"Готово, когда: в тесте есть русские названия короче и длиннее ширины и строка с неверным числом полей;
go test .печатаетok, на каждой порче —FAIL; коммит вgit log --oneline(пунктыtestfitиtestparse).
Что может пойти не так
В программе из раздела parseLine забыли три строки проверки числа полей — на строке TK-102;Шайба;100 нет parts[3]:
stagiaire@lab:~/gocourse/day19/scratch$ go run .
Болт М8 12 1500 <nil>
0 0 пустая строка
panic: runtime error: index out of range [3] with length 3
goroutine 1 [running]:
main.parseLine({0x4a2d30, 0x15})
/home/stagiaire/gocourse/day19/scratch/main.go:20 +0x258
main.main()
/home/stagiaire/gocourse/day19/scratch/main.go:30 +0x20a
exit status 2Стек читается снизу вверх: main на строке 30 вызвал parseLine, а она упала на строке 20.
Функция объявлена и в line.go, и в main.go — два сообщения на одну ошибку:
stagiaire@lab:~/gocourse/day19/report$ go run .
# day19/report
./main.go:5:6: fitName redeclared in this block
./line.go:31:6: other declaration of fitName| Что видите | Что это значит | Что делать |
|---|---|---|
panic: runtime error: index out of range [3] with length 3 |
взяли поле, которого нет: полей после Split три |
проверять len(parts) до обращения к полям |
fitName redeclared in this block и other declaration of fitName |
одна функция объявлена в двух файлах пакета | оставить её только в line.go |
runtime.main_main·f: function main is undeclared in the main package |
go run . в report, где ещё нет main.go. go test . при этом работает |
программу пишем на шестой странице, до неё — только go test . |
нужно 4 поля, а их 1 на пустой строке |
проверка пустой строки стоит после Split |
пустую строку проверять первой |
strconv.Atoi: parsing " 12": invalid syntax |
поле не очищено от пробелов | strings.TrimSpace перед Atoi |
| проверка: «Тест проходит и на заведомо неверной программе» | в тесте нет случая, на котором видна ошибка подмены | подсказка под пунктом называет случай; таблица «Одного случая мало» |
Дальше: как прочитать выгрузку из файла по имени — course next