Это самый «толстый» скрипт из всей бизнес-логики. Он принимает на вход input.txt, проверяет данные, считает возраст, запускает математику из calc.yab и сохраняет всё через store.yab. Здесь же живёт логика TEM-замеров (когда врач делает несколько замеров одной складки через ";").
В Yabasic нет исключений. Если пользователь введет в поле weight: abc, программа не должна молча записать это в базу или упасть с невнятным стектрейсом.
Казалось бы, для проверки на число есть встроенная функция val(). Но проблема в том, что val("abc") возвращает 0. И тут возникает вопрос: а если пользователь реально ввел 0? Как отличить корректный ноль от мусора?
Если last_name пустое, или пол не m/f, или weight не число — программа не пытается «спасти» ситуацию. Она падает через fatal() и завершает работу.
Почему так? Потому что это медицинский инструмент. Лучше заставить врача исправить опечатку в input.txt, чем записать в базу данных клиента с полом «х» и потом ловить баги при генерации отчёта.
В Yabasic нет библиотек для работы с датами. Встроенная date$ возвращает строку из 6 полей (типа 3-09-23-2026-Wed-Sep), которую надо разбивать руками.
Чтобы посчитать возраст на момент визита, я написал функцию calc_age(). Она берет дату рождения (ГГГГ-ММ-ДД) и дату визита, разбивает их через split() и вычитает года с учетом того, прошел ли уже день рождения в текущем году.
Если врач хочет оценить свою точность (TEM), он может ввести значение через точку с запятой: triceps: 6.0;6.2;5.8.
Функция in_values() разбирает такую строку. Если значение одно — возвращает его. Если несколько — считает среднее. А если значений больше 5 (или там буквы) — снова вызывает fatal().
Напомню про ограничение, о котором писал в посте про CSV. Функция line input читает строку до первого перевода строки (\n). Если врач нажмет Enter внутри поля notes (решит написать заметку в две строки), вторая строка просто потеряется. Программа не упадет, просто молча съест текст.
Код: Выделить всё
// ============================================================
// new.yab — создание клиента / добавление визита из input.txt
// ============================================================
//
// YABCT — Yet Another Body Composition Tool
//
// Author: Anton Shekhetov, Ekaterina Yakovleva
// Copyright (c) 2026 ANO DPO "NCPS" RU INN 7606135591
// This software is released under the MIT License.
// See the LICENSE file in the project root for more information
//
// Режимы:
// input.txt содержит id: <число> → визит к существующему клиенту
// input.txt без id → создание нового клиента
//
// Зависимости: cfg, calc, store, lang.
// ============================================================
import cfg
import calc
import store
import lang
// ============================================================
// ХРАНИЛИЩЕ INPUT
// ============================================================
dim in_key$(100)
dim in_val$(100)
in_count = 0
dim tmp_date$(10)
dim visit_arr$(40)
// Список TEM-точек. Порядок: складки (поз. 12–22), затем обхваты (поз. 6–11).
// См. PROJECT_STATE §3.12.
dim tem_list$(17)
dim tem_is_sf(17) // 1 = складка, 0 = обхват
// ============================================================
// ФАТАЛЬНАЯ ОШИБКА
// ============================================================
sub fatal(msg$)
print msg$
exit
end sub
// Показать ошибку store и завершить.
// Использует err.<code> из lang, если ключ есть. Иначе — err.generic.
sub fatal_store_error()
local code$, detail$, key$
code$ = store.last_error_code$()
detail$ = store.last_error_detail$()
key$ = "err." + code$
if (lang.has(key$) = 1) then
fatal(lang.subst$(key$, detail$))
end if
fatal(lang.subst$("err.generic", code$))
end sub
// ============================================================
// УТИЛИТЫ
// ============================================================
sub file_exists(fname$)
local f
f = open(fname$, "r")
if (f = 0) return 0
close f
return 1
end sub
sub strip_comment$(s$)
local i, c$
for i = 1 to len(s$)
c$ = mid$(s$, i, 1)
if (c$ = "#") return trim$(left$(s$, i - 1))
next i
return trim$(s$)
end sub
sub is_number(s$)
local i, c$, dots, result
if (len(s$) = 0) return 0
dots = 0
result = 1
for i = 1 to len(s$)
c$ = mid$(s$, i, 1)
if (c$ = ".") then
dots = dots + 1
if (dots > 1) result = 0
elsif (c$ = "-") then
if (i > 1) result = 0
elsif (c$ < "0" or c$ > "9") then
result = 0
end if
next i
return result
end sub
// ============================================================
// ПАРСЕР INPUT.TXT
// ============================================================
sub load_input(fname$)
local line$, key$, val$, rest$, p, i
if (not file_exists(fname$)) return 0
in_count = 0
open fname$ for reading as #1
while (not eof(#1))
line input #1 line$
line$ = trim$(line$)
if (len(line$) > 0 and left$(line$, 1) <> "#") then
p = 0
for i = 1 to len(line$)
if (mid$(line$, i, 1) = ":" and p = 0) p = i
next i
if (p > 0) then
key$ = trim$(left$(line$, p - 1))
rest$ = mid$(line$, p + 1)
val$ = strip_comment$(rest$)
if (len(key$) > 0) then
in_count = in_count + 1
in_key$(in_count) = key$
in_val$(in_count) = val$
end if
end if
end if
wend
close #1
return in_count
end sub
sub in_has(key$)
local i
for i = 1 to in_count
if (in_key$(i) = key$) return 1
next i
return 0
end sub
sub in_get$(key$)
local i
for i = 1 to in_count
if (in_key$(i) = key$) return in_val$(i)
next i
return ""
end sub
sub in_num(key$)
local v$
if (not in_has(key$)) return 0
v$ = in_get$(key$)
if (not is_number(v$)) return 0
return val(v$)
end sub
// ------------------------------------------------------------
// in_values(key$, arr$(), max_n)
// Разбор значения с повторами (разделитель ";") для точки с TEM.
// Возвращает k (число значений), заполняет arr$().
// Без ";" — k = 1, arr$(1) = значение.
// Нет ключа или пусто — k = 0.
// Ошибки ввода → fatal (fail-hard, §8.2 п. 11):
// > max_n, пусто, не число, отрицательное.
// ------------------------------------------------------------
sub in_values(key$, arr$(), max_n)
local raw$, parts$(10), k, i
if (not in_has(key$)) return 0
raw$ = in_get$(key$)
if (raw$ = "") return 0
// Одиночное значение
if (instr(raw$, ";") = 0) then
if (not is_number(raw$)) then
fatal(lang.t$("err.tem_invalid") + " " + key$ + ": " + lang.t$("err.tem_reason_not_number"))
end if
if (val(raw$) < 0) then
fatal(lang.t$("err.tem_invalid") + " " + key$ + ": " + lang.t$("err.tem_reason_negative"))
end if
arr$(1) = raw$
return 1
end if
// Со ";" — split
k = split(raw$, parts$(), ";")
if (k > max_n) then
fatal(lang.t$("err.tem_invalid") + " " + key$ + ": " + lang.t$("err.tem_reason_too_many"))
end if
if (k < 2) then
fatal(lang.t$("err.tem_invalid") + " " + key$ + ": " + lang.t$("err.tem_reason_empty"))
end if
for i = 1 to k
if (parts$(i) = "") then
fatal(lang.t$("err.tem_invalid") + " " + key$ + ": " + lang.t$("err.tem_reason_empty"))
end if
if (not is_number(parts$(i))) then
fatal(lang.t$("err.tem_invalid") + " " + key$ + ": " + lang.t$("err.tem_reason_not_number"))
end if
if (val(parts$(i)) < 0) then
fatal(lang.t$("err.tem_invalid") + " " + key$ + ": " + lang.t$("err.tem_reason_negative"))
end if
arr$(i) = parts$(i)
next i
return k
end sub
// ------------------------------------------------------------
// get_point_value(key$)
// Значение точки для записи в CSV.
// k = 0 → 0 (нет данных)
// k = 1 → val(raw)
// k >= 2 → calc.tem_mean (среднее по всем повторам)
// Детали TEM (k, значения) здесь не сохраняются —
// это делает блок 6 (<id>_tem.csv, tem_status).
// ------------------------------------------------------------
sub get_point_value(key$)
local vals$(5), k
k = in_values(key$, vals$(), 5)
if (k = 0) return -1
if (k = 1) return val(vals$(1))
return calc.tem_mean(vals$(), k)
end sub
// ============================================================
// РАСЧЁТ ВОЗРАСТА
// ============================================================
sub calc_age(birth$, visit$)
local by, bm, bd, vy, vm, vd, age, dummy
if (len(birth$) < 10) return 0
dummy = split(birth$, tmp_date$(), "-")
by = val(tmp_date$(1))
bm = val(tmp_date$(2))
bd = val(tmp_date$(3))
visit$ = left$(visit$, 10)
dummy = split(visit$, tmp_date$(), "-")
vy = val(tmp_date$(1))
vm = val(tmp_date$(2))
vd = val(tmp_date$(3))
age = vy - by
if (vm < bm) age = age - 1
if (vm = bm and vd < bd) age = age - 1
return age
end sub
// ============================================================
// ФОРМИРОВАНИЕ СТРОКИ ВИЗИТА
// ============================================================
sub fmt_num$(v)
if (v < 0) return ""
return trim$(str$(v))
end sub
sub fmt_field$(key$)
local v$
if (not in_has(key$)) return ""
v$ = in_get$(key$)
if (v$ = "") return ""
if (not is_number(v$)) return ""
return trim$(str$(val(v$)))
end sub
sub build_visit_row$(id$, sex$, age, visit_date$, tem_status$)
local weight, height, neck, waist, hip
local arm_circ, thigh_circ, calf_circ
local biceps, triceps, subscap, suprail
local iliac_crest, supraspinale
local chest, abdomen, thigh_sf, midax, calf_sf
local bf_dw, bf_jp7, bf_navy, bf_main
local sm_lee, sm_janssen, fat_kg, lean_kg
local bmi_v, whr_v, whtr_v, bsa_v, bf_use
local sum8_v
local conditions$, notes$
weight = in_num("weight")
height = in_num("height")
neck = get_point_value("neck")
waist = get_point_value("waist")
hip = get_point_value("hip")
arm_circ = get_point_value("arm_circ")
thigh_circ = get_point_value("thigh_circ")
calf_circ = get_point_value("calf_circ")
biceps = get_point_value("biceps")
triceps = get_point_value("triceps")
subscap = get_point_value("subscapular")
suprail = get_point_value("suprailiac")
iliac_crest = get_point_value("iliac_crest")
supraspinale = get_point_value("supraspinale")
chest = get_point_value("chest")
abdomen = get_point_value("abdomen")
thigh_sf = get_point_value("thigh")
midax = get_point_value("midaxillary")
calf_sf = get_point_value("calf_medial")
bf_dw = calc.skinfolds(sex$, biceps, triceps, subscap, suprail)
bf_navy = calc.navy(sex$, height, neck, waist, hip)
bf_jp7 = calc.jp7(sex$, age, chest, midax, triceps, subscap, abdomen, suprail, thigh_sf)
// BF% — только для возраста 18–64 (§3.11).
// Для <18 и 65+ все формулы BF% = -1.
if (age < 18 or age >= 65) then
bf_dw = -1
bf_jp7 = -1
bf_navy = -1
end if
sm_lee = calc.lee(sex$, age, height / 100, arm_circ, thigh_circ, calf_circ, triceps, thigh_sf, calf_sf)
sm_janssen = calc.janssen(sex$, age, weight, height / 100)
// Основная формула по приоритету JP7 → DW → Navy.
if (bf_jp7 >= 0) then
bf_use = bf_jp7
elsif (bf_dw >= 0) then
bf_use = bf_dw
elsif (bf_navy >= 0) then
bf_use = bf_navy
else
bf_use = -1
end if
bf_main = bf_use
// ISAK Σ8 (мм). При недостатке данных — -1 (в CSV как "").
sum8_v = calc.sum8(triceps, subscap, biceps, iliac_crest, supraspinale, abdomen, thigh_sf, calf_sf)
if (bf_use >= 0) then
fat_kg = weight * bf_use / 100
lean_kg = weight - fat_kg
else
fat_kg = -1
lean_kg = -1
end if
bmi_v = calc.bmi(weight, height)
whr_v = calc.whr(waist, hip)
whtr_v = calc.whtr(waist, height)
bsa_v = calc.bsa(weight, height)
conditions$ = in_get$("conditions_ok")
if (conditions$ = "") conditions$ = "Y"
notes$ = in_get$("visit_notes")
// 37 колонок — см. VISIT_HEADER$ в store.yab.
visit_arr$(1) = visit_date$
visit_arr$(2) = fmt_num$(age)
visit_arr$(3) = fmt_field$("weight")
visit_arr$(4) = tem_status$
visit_arr$(5) = fmt_field$("height")
visit_arr$(6) = fmt_num$(neck)
visit_arr$(7) = fmt_num$(waist)
visit_arr$(8) = fmt_num$(hip)
visit_arr$(9) = fmt_num$(arm_circ)
visit_arr$(10) = fmt_num$(thigh_circ)
visit_arr$(11) = fmt_num$(calf_circ)
visit_arr$(12) = fmt_num$(biceps)
visit_arr$(13) = fmt_num$(triceps)
visit_arr$(14) = fmt_num$(subscap)
visit_arr$(15) = fmt_num$(suprail)
visit_arr$(16) = fmt_num$(iliac_crest)
visit_arr$(17) = fmt_num$(supraspinale)
visit_arr$(18) = fmt_num$(chest)
visit_arr$(19) = fmt_num$(abdomen)
visit_arr$(20) = fmt_num$(thigh_sf)
visit_arr$(21) = fmt_num$(midax)
visit_arr$(22) = fmt_num$(calf_sf)
visit_arr$(23) = fmt_num$(bf_dw)
visit_arr$(24) = fmt_num$(bf_jp7)
visit_arr$(25) = fmt_num$(bf_navy)
visit_arr$(26) = fmt_num$(bf_main)
visit_arr$(27) = fmt_num$(sm_lee)
visit_arr$(28) = fmt_num$(sm_janssen)
visit_arr$(29) = fmt_num$(fat_kg)
visit_arr$(30) = fmt_num$(lean_kg)
visit_arr$(31) = fmt_num$(bmi_v)
visit_arr$(32) = fmt_num$(whr_v)
visit_arr$(33) = fmt_num$(whtr_v)
visit_arr$(34) = fmt_num$(bsa_v)
visit_arr$(35) = conditions$
visit_arr$(36) = notes$
visit_arr$(37) = fmt_num$(sum8_v)
return store.build_line$(visit_arr$(), 37)
end sub
// ============================================================
// TEM — СТАТУС
// ============================================================
// ------------------------------------------------------------
// compute_tem_status$()
// Возвращает "" / "high" / "ok" / "bad". См. PROJECT_STATE §3.12.
// "" — ни у одной точки нет повторов
// bad — ЛИБО агрегат, ЛИБО хотя бы одна точка > порога
// high — не bad, все складки ≤ tem_high_precision
// ok — не bad, не high
// ------------------------------------------------------------
sub compute_tem_status$()
local i, k, mean, sd, rt
local any_repeat, any_bad, sf_high_all
local vals$(5)
local sf_thr, circ_thr, sf_high
sf_thr = cfg.get_num("tem_threshold")
circ_thr = cfg.get_num("tem_circ_threshold")
sf_high = cfg.get_num("tem_high_precision")
any_repeat = 0
any_bad = 0
sf_high_all = 1
for i = 1 to 17
k = in_values(tem_list$(i), vals$(), 5)
if (k >= 2) then
any_repeat = 1
mean = calc.tem_mean(vals$(), k)
sd = calc.tem_sd(vals$(), k, mean)
rt = calc.tem_rtem(mean, sd)
if (tem_is_sf(i) = 1) then
if (rt > sf_thr) any_bad = 1
if (rt > sf_high) sf_high_all = 0
else
if (rt > circ_thr) any_bad = 1
end if
end if
next i
if (any_repeat = 0) return ""
if (any_bad = 1) return "bad"
if (sf_high_all = 1) return "high"
return "ok"
end sub
// ------------------------------------------------------------
// fmt_num2$(v)
// Число с фиксированными 2 знаками. 6.0 → "6.00", 6.02 → "6.02".
// Отрицательное → "".
// ------------------------------------------------------------
sub fmt_num2$(v)
local s$, dp
if (v < 0) return ""
s$ = trim$(str$(calc.round2(v)))
dp = instr(s$, ".")
if (dp = 0) then
return s$ + ".00"
end if
if (len(s$) - dp = 1) then
return s$ + "0"
end if
return s$
end sub
// ------------------------------------------------------------
// write_tem_log(id$, visit_date$)
// Запись <id>_tem.csv. Append + заголовок при создании.
// Только точки с k >= 2. 9 колонок (§3.12).
// ------------------------------------------------------------
sub write_tem_log(id$, visit_date$)
local i, j, k, cnt, mean, sd, rt
local vals$(5)
local fname$, f, need_header
local tl$(20) // грабля 10: local с размером, сбрасывается при вызове
fname$ = id$ + "_tem.csv"
// Проверка существования (грабля 22)
f = open(fname$, "r")
if (f = 0) then
need_header = 1
else
close f
need_header = 0
end if
// Сбор строк
cnt = 0
for i = 1 to 17
k = in_values(tem_list$(i), vals$(), 5)
if (k >= 2) then
mean = calc.tem_mean(vals$(), k)
sd = calc.tem_sd(vals$(), k, mean)
rt = calc.tem_rtem(mean, sd)
cnt = cnt + 1
tl$(cnt) = visit_date$ + "," + tem_list$(i)
for j = 1 to 5
if (j <= k) then
tl$(cnt) = tl$(cnt) + "," + vals$(j)
else
tl$(cnt) = tl$(cnt) + ","
end if
next j
tl$(cnt) = tl$(cnt) + "," + fmt_num2$(mean)
tl$(cnt) = tl$(cnt) + "," + fmt_num2$(rt)
end if
next i
if (cnt = 0) return
// Запись (append через "a", грабля 21)
if (need_header = 1) then
open fname$ for writing as #1
print #1, "visit_date,point,v1,v2,v3,v4,v5,mean,tem_pct"
else
open #1, fname$, "a"
end if
for i = 1 to cnt
print #1, tl$(i)
next i
close #1
end sub
// ============================================================
// ГЛАВНАЯ ПРОГРАММА
// ============================================================
// 0. Язык
// Список TEM-точек: складки (поз. 12–22), затем обхваты (поз. 6–11).
tem_list$(1) = "biceps"
tem_is_sf(1) = 1
tem_list$(2) = "triceps"
tem_is_sf(2) = 1
tem_list$(3) = "subscapular"
tem_is_sf(3) = 1
tem_list$(4) = "suprailiac"
tem_is_sf(4) = 1
tem_list$(5) = "iliac_crest"
tem_is_sf(5) = 1
tem_list$(6) = "supraspinale"
tem_is_sf(6) = 1
tem_list$(7) = "chest"
tem_is_sf(7) = 1
tem_list$(8) = "abdomen"
tem_is_sf(8) = 1
tem_list$(9) = "thigh"
tem_is_sf(9) = 1
tem_list$(10) = "midaxillary"
tem_is_sf(10) = 1
tem_list$(11) = "calf_medial"
tem_is_sf(11) = 1
tem_list$(12) = "neck"
tem_is_sf(12) = 0
tem_list$(13) = "waist"
tem_is_sf(13) = 0
tem_list$(14) = "hip"
tem_is_sf(14) = 0
tem_list$(15) = "arm_circ"
tem_is_sf(15) = 0
tem_list$(16) = "thigh_circ"
tem_is_sf(16) = 0
tem_list$(17) = "calf_circ"
tem_is_sf(17) = 0
n = lang.load("lang.txt")
print lang.t$("app.title")
print
// 1. Config
print lang.t$("config.loading")
n = cfg.load("config.cfg")
if (n = 0) then
fatal(lang.t$("config.err"))
end if
print lang.t$("config.ok")
print
// 2. Input
print lang.t$("input.loading")
n = load_input("input.txt")
if (n = 0) then
fatal(lang.t$("input.err"))
end if
print lang.t$("input.ok")
print
// 3. Validation
print lang.t$("validate.loading")
if (in_get$("last_name") = "") then
fatal(lang.t$("validate.err_last_name"))
end if
if (in_get$("first_name") = "") then
fatal(lang.t$("validate.err_first_name"))
end if
sex$ = lower$(in_get$("sex"))
if (sex$ <> "m" and sex$ <> "f") then
fatal(lang.t$("validate.err_sex"))
end if
weight = in_num("weight")
height = in_num("height")
if (weight <= 0 or height <= 0) then
fatal(lang.t$("validate.err_weight_height"))
end if
print lang.t$("validate.ok")
print
// 4. Store init
store.init("clients")
// 5. Определяем ID и клиента
id$ = in_get$("id")
if (id$ = "") then
// Новый клиент
id$ = store.new_id$()
print lang.subst$("client.new_id", id$)
print lang.t$("client.creating")
r = store.add_client(id$, sex$, in_get$("last_name"), in_get$("first_name"), in_get$("middle_name"), in_get$("birth_date"), in_get$("phone"), in_get$("email"), in_get$("notes"))
if (r = 0) then
fatal_store_error()
end if
print lang.t$("client.created")
else
// Существующий клиент
print lang.subst$("app.client_id", id$)
if (store.client_exists(id$) = 0) then
fatal(lang.subst$("err.client_not_found", id$))
end if
db_sex$ = lower$(store.client_field$(id$, "sex"))
if (db_sex$ = "m" or db_sex$ = "f") then
sex$ = db_sex$
end if
ln$ = store.client_field$(id$, "last_name")
fn$ = store.client_field$(id$, "first_name")
print lang.subst$("client.found", ln$ + " " + fn$)
end if
print
// 6. Возраст
age = in_num("age")
if (age = 0) then
birth$ = store.client_field$(id$, "birth_date")
vdate$ = in_get$("visit_date")
if (vdate$ = "") then
dummy = split(date$, tmp_date$(), "-")
vdate$ = tmp_date$(4) + "-" + tmp_date$(2) + "-" + tmp_date$(3)
end if
age = calc_age(birth$, vdate$)
end if
print lang.subst$("visit.age", trim$(str$(age)))
print
// 7. Расчёт и формирование строки визита
print lang.t$("visit.calc_loading")
vdate$ = in_get$("visit_date")
if (vdate$ = "") then
dummy = split(date$, tmp_date$(), "-")
v_day$ = tmp_date$(3)
v_month$ = tmp_date$(2)
v_year$ = tmp_date$(4)
dummy = split(time$, tmp_date$(), "-")
v_hour$ = tmp_date$(1)
v_min$ = tmp_date$(2)
if (len(v_day$) = 1) v_day$ = "0" + v_day$
if (len(v_month$) = 1) v_month$ = "0" + v_month$
if (len(v_hour$) = 1) v_hour$ = "0" + v_hour$
if (len(v_min$) = 1) v_min$ = "0" + v_min$
vdate$ = v_year$ + "-" + v_month$ + "-" + v_day$ + " " + v_hour$ + ":" + v_min$
end if
tem_status$ = compute_tem_status$()
row$ = build_visit_row$(id$, sex$, age, vdate$, tem_status$)
print lang.t$("visit.calc_ok")
print
// 8. Сохранение визита
print lang.t$("visit.save_loading")
r = store.add_visit(id$, row$)
if (r = 0) then
fatal_store_error()
end if
print lang.t$("visit.save_ok")
print
// TEM-лог (только при наличии повторов)
write_tem_log(id$, vdate$)
print lang.t$("app.done")
print lang.subst$("app.client_id", id$)