package hae import ( "bytes" "encoding/json" ) // PointValue достаёт число точки: `qty`, а при его отсутствии — `Avg`. // // Единственное место в проекте, знающее, какое поле точки HAE несёт число. // Порядок именно такой: `qty` несут все метрики, `Avg` — только `heart_rate` // (находка 40), и без второго кандидата самая частая метрика потока не // измерялась бы вовсе. // // Это чтение, а не интерпретация: значение никуда не пишется и ничего не // подменяет. Форма точки при этом рода метрики не выдаёт — род измеряется // сверкой слоёв, а не выводится отсюда. // // **Ноль — значение.** Словарь пустоты из `canon` сюда не применяется и // применяться не должен: там ноль объявлен пустотой, чтобы точка без измерений // не вытесняла настоящее измерение при столкновении координат, — вопрос совсем // другой. Взяв его, измерение не увидело бы точки `{"qty":0}`, час выпал бы из // счётчиков ещё до правила различимости, и проверка «нулевой час свидетельством // не является» зеленела бы по неверной причине. // // **Значением считается только JSON-число.** Строка `"72.5"` — не число, хотя // `json.Number` её принимает (проверено): такой формы поток не приносил, и // прочитать её как измерение значило бы угадать за источник. Отсутствие ключа и // `null` — тоже «нет значения»; при этом `qty: null` не мешает прочитать `Avg`, // а `qty` не того типа мешает: форма точки поменялась, и догадываться не о чем. func PointValue(raw json.RawMessage) (float64, bool) { var p struct { Qty *json.RawMessage `json:"qty"` Avg *json.RawMessage `json:"Avg"` } // Указатели, а не значения: `null` обязан отличаться от нуля, и в этой форме // он приходит нулевым указателем. if err := json.Unmarshal(raw, &p); err != nil { return 0, false } if p.Qty != nil { return jsonNumber(*p.Qty) } if p.Avg != nil { return jsonNumber(*p.Avg) } return 0, false } // jsonNumber разбирает значение поля, требуя, чтобы это было JSON-число. // // Проверка первого байта нужна потому, что `json.Unmarshal` в `float64` // отвергает строку, но `json.Number` — принимает; полагаться на тип-приёмник // значило бы получить разное поведение от невидимой детали. // // Бесконечность значением не считается: `1e400` разбирается с ошибкой // диапазона, и проглоти её — `+Inf` отравил бы и сумму, и среднее всего часа, а // видно это было бы лишь тем, что род перестал определяться. Отдельной проверки // на `Inf`/`NaN` после разбора нет намеренно: их литералов в JSON не бывает, а // число вне диапазона `float64` даёт ошибку, а не бесконечность. func jsonNumber(raw json.RawMessage) (float64, bool) { b := bytes.TrimSpace(raw) if len(b) == 0 || (b[0] != '-' && (b[0] < '0' || b[0] > '9')) { return 0, false } var v float64 if err := json.Unmarshal(b, &v); err != nil { return 0, false } return v, true }