добавлен словарь категориальных значений HAE → коды HealthKit
- фазы сна, контекст пульса и имена тренировок попадают в реестр `category_value` (миграция 00010): строка хранится дословно, выведенный код лежит рядом отдельной записью, а не полем внутри точки - словарь и синонимы кодов живут в бинаре (`internal/healthkit`); локаль из `Accept-Language` сужает поиск, но в ключ реестра не входит — заголовков в сыром архиве нет - наблюдение входит в отпечаток витрины, выведенный код — нет: он производная от словаря, а не от журнала
This commit is contained in:
+66
-2
@@ -179,6 +179,32 @@ type Result struct {
|
||||
// по координате.
|
||||
SkippedBadEnd int
|
||||
|
||||
// Categoricals — различные наблюдённые категориальные значения доставки с
|
||||
// выведенными кодами, в детерминированном порядке. Повтор одной строки в
|
||||
// тысяче точек даёт один элемент.
|
||||
Categoricals []Categorical
|
||||
// CategoricalUnknown — сколько РАЗЛИЧНЫХ наблюдений не получило кода.
|
||||
// Считаются различные значения, а не их вхождения: счётчик отвечает на
|
||||
// вопрос «сколько строк ждёт словаря», а не «сколько точек их несло».
|
||||
//
|
||||
// В установившемся режиме он ненулевой — словарь покрывает только фазы сна,
|
||||
// а контекст пульса и имена тренировок объявлены категориальными заранее.
|
||||
// Сигналом «появилось новое» служит поэтому новая строка реестра, а не
|
||||
// ненулевой счётчик.
|
||||
CategoricalUnknown int
|
||||
// CategoricalDropped — сколько ВХОЖДЕНИЙ отброшено границами: непомерная
|
||||
// длина значения или переполнение числа различных значений.
|
||||
//
|
||||
// Единица названа вслух и отличается от соседнего счётчика намеренно.
|
||||
// Считать здесь различные значения нечем: набор ограничен при вставке (иначе
|
||||
// накопитель растёт вместе с телом, а тело контролирует отправитель), и
|
||||
// отброшенный ключ нигде не запоминается — запомнить его значило бы вернуть
|
||||
// ровно тот неограниченный рост, ради устранения которого граница и стоит на
|
||||
// вставке. Поэтому счётчик отвечает на вопрос «сколько раз сработала
|
||||
// граница», а не «сколько строк потеряно»; на второй отвечает реестр в
|
||||
// витрине.
|
||||
CategoricalDropped int
|
||||
|
||||
// Layer — слой, выведенный для доставки в целом (тот, что наследуют редкие
|
||||
// метрики). Пустой, если плотных метрик не было и наследовать было нечего.
|
||||
// Его сохраняет вызывающий, чтобы следующая доставка той же автоматизации
|
||||
@@ -204,6 +230,13 @@ type Meta struct {
|
||||
// из 89, обе с заголовком `Default`. Ищет и передаёт его вызывающий —
|
||||
// разбор остаётся чистой функцией.
|
||||
FallbackLayer Layer
|
||||
|
||||
// Locale — нормализованный языковой тег доставки из `Accept-Language`
|
||||
// (находка 32). Сужает поиск по словарю категориальных значений и НИКУДА не
|
||||
// сохраняется: заголовков в сыром архиве нет, поэтому доставка,
|
||||
// восстановленная из осиротевшего тела, приезжает без локали — и обязана
|
||||
// дать то же состояние. Пустая локаль законна.
|
||||
Locale string
|
||||
}
|
||||
|
||||
// Parse разбирает секцию metrics тела доставки в точки.
|
||||
@@ -231,18 +264,28 @@ func Parse(body []byte, meta Meta) (res Result, err error) {
|
||||
res.Uncovered = env.uncovered
|
||||
res.UncoveredDropped = env.dropped
|
||||
|
||||
cat := newCategoricals(meta.Locale)
|
||||
|
||||
res.Workouts = decodeEntities(env.workouts, workoutsSection, &res)
|
||||
res.Records = decodeEntities(env.stateOfMind, stateOfMindSection, &res)
|
||||
// Имя тренировки берётся из уже разобранного заголовка сущности. Мягкое
|
||||
// чтение превратило значение не того типа в пустую строку, а пустая строка
|
||||
// наблюдением не считается, — то есть «имени не было» и «имя приехало
|
||||
// числом» дают один исход, и он верный.
|
||||
for _, w := range res.Workouts {
|
||||
cat.add(workoutsSection, fieldName, w.Name)
|
||||
}
|
||||
|
||||
metrics := env.metrics
|
||||
res.Metrics = len(metrics)
|
||||
if len(metrics) == 0 {
|
||||
res.Categoricals, res.CategoricalUnknown, res.CategoricalDropped = cat.result()
|
||||
return res, nil
|
||||
}
|
||||
|
||||
groups := make([]group, 0, len(metrics))
|
||||
for _, m := range metrics {
|
||||
g := decodeGroup(m, &res)
|
||||
g := decodeGroup(m, &res, cat)
|
||||
if len(g.summaries) > 0 {
|
||||
groups = append(groups, group{
|
||||
metric: sleepSummaryMetric,
|
||||
@@ -258,6 +301,7 @@ func Parse(body []byte, meta Meta) (res Result, err error) {
|
||||
}
|
||||
}
|
||||
if len(groups) == 0 {
|
||||
res.Categoricals, res.CategoricalUnknown, res.CategoricalDropped = cat.result()
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -280,6 +324,11 @@ func Parse(body []byte, meta Meta) (res Result, err error) {
|
||||
}, err
|
||||
}
|
||||
|
||||
// Наблюдения отдаются только на успешном исходе: «всё или ничего» относится
|
||||
// к доставке целиком. У отказа по слою (см. ветку выше) сущности не
|
||||
// отдаются по той же причине.
|
||||
res.Categoricals, res.CategoricalUnknown, res.CategoricalDropped = cat.result()
|
||||
|
||||
total := 0
|
||||
for _, g := range groups {
|
||||
total += len(g.points)
|
||||
@@ -400,6 +449,15 @@ type pointHead struct {
|
||||
// TotalSleep различает две схемы под именем sleep_analysis: поэпизодную и
|
||||
// суточную сводку. Общих полей, кроме date и source, у них нет.
|
||||
TotalSleep *json.RawMessage `json:"totalSleep"`
|
||||
|
||||
// Value и Context — категориальные поля точки (фаза сна и контекст пульса,
|
||||
// находка 37). Сырыми сообщениями, а не строками: объяви их `string`, и
|
||||
// точка, у которой поле пришло числом, перестала бы разбираться вовсе —
|
||||
// json.Unmarshal отвечает ошибкой на несовпадение типа, а decodeGroup
|
||||
// считает такую точку не разобравшейся. Новый путь потери точки ради
|
||||
// удобства структуры недопустим.
|
||||
Value *json.RawMessage `json:"value"`
|
||||
Context *json.RawMessage `json:"context"`
|
||||
}
|
||||
|
||||
// decodeEnvelope разбирает конверт: отдаёт секцию metrics и имена секций,
|
||||
@@ -665,7 +723,7 @@ func clipSection(name string) string {
|
||||
// уже отфильтрован пропусками, и любой пропуск сдвигал бы соответствие — эпизод
|
||||
// сна уезжал бы под имя суточной сводки, а сводка под имя эпизода. Индексной
|
||||
// корреляции между двумя списками здесь не существует по построению.
|
||||
func decodeGroup(m metricEnvelope, res *Result) group {
|
||||
func decodeGroup(m metricEnvelope, res *Result, cat *categoricals) group {
|
||||
g := group{metric: m.Name, units: m.Units, points: make([]Point, 0, len(m.Data))}
|
||||
|
||||
for _, raw := range m.Data {
|
||||
@@ -716,8 +774,14 @@ func decodeGroup(m metricEnvelope, res *Result) group {
|
||||
if m.Name == sleepMetric && head.TotalSleep != nil {
|
||||
p.Metric = sleepSummaryMetric
|
||||
g.summaries = append(g.summaries, p)
|
||||
// Наблюдение снимается по ИТОГОВОМУ имени метрики, тому же, которым
|
||||
// адресуется единица хранения. У суточной сводки категориальных
|
||||
// полей нет, так что здесь это ноль работы, — но правило записано
|
||||
// один раз и не разойдётся при следующем разделении схем.
|
||||
cat.addPoint(p.Metric, &head)
|
||||
continue
|
||||
}
|
||||
cat.addPoint(p.Metric, &head)
|
||||
g.points = append(g.points, p)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user