полнота точки — множество ключей, победитель — функция множества точек
- отношение победы было нетранзитивным: полнота (частичный порядок) плюс тай-брейк (тотальный) в попарной свёртке давали цикл, из-за которого одна и та же доставка меняла содержимое объекта при каждой пересборке - надмножество побеждает только при совпадении значений общих содержательных ключей: иначе точка без единого измерения вытесняла измерение - Less стал тотальным, isEmpty не материализует значение, имя метрики в координате столкновения обрезается, отпечаток витрины включает units и sealed - на живом архиве строгий no-op: 1737 объектов, содержимое совпало побайтово
This commit is contained in:
+242
-29
@@ -107,34 +107,194 @@ func Equal(a, b []byte) bool {
|
||||
return bytes.Equal(fa, fb)
|
||||
}
|
||||
|
||||
// Completeness — мера полноты точки: сколько значащих полей она несёт.
|
||||
// Fullness — отношение полноты двух точек: несёт ли одна всё, что несёт
|
||||
// другая, и сверх того.
|
||||
//
|
||||
// Нужна правилу разрешения столкновений: по одним координатам приезжают точки
|
||||
// с разным НАБОРОМ полей при одинаковом значении (0.66% координат), и правило
|
||||
// «последняя победила» стирало бы у сохранённой точки поля, которых новая не
|
||||
// несёт.
|
||||
// Полнота — частичный порядок, а не число. Счётчик значащих полей сравним
|
||||
// всегда и потому отвечает там, где ответа нет: точка с пятью полями без
|
||||
// содержания «полнее» настоящего измерения с двумя. Измерено (находка 49):
|
||||
// настоящих столкновений 0.65% координат, из них 981 различаются набором
|
||||
// полей — это и есть область правила, — а несравнимых наборов ноль.
|
||||
//
|
||||
// Поля с пустым значением не считаются: точка с `context: null` не полнее
|
||||
// точки без `context`. Поле source в счёт не идёт — оно нестабильно и
|
||||
// переписывается задним числом, так что его наличие ничего не говорит о
|
||||
// полноте измерения.
|
||||
func Completeness(raw []byte) int {
|
||||
// Нумерация с единицы: нулевое значение не означает ничего. Незаполненное поле
|
||||
// или ранний возврат не должны выглядеть как «множества равны» — это
|
||||
// сегодняшний исход по умолчанию, и отказ маскировался бы под успех.
|
||||
type Fullness int
|
||||
|
||||
const (
|
||||
// FullnessEqual — множества ключей совпадают.
|
||||
FullnessEqual Fullness = iota + 1
|
||||
// FullnessSuperset — a несёт всё, что b, и сверх того.
|
||||
FullnessSuperset
|
||||
// FullnessSubset — b несёт всё, что a, и сверх того.
|
||||
FullnessSubset
|
||||
// FullnessIncomparable — у каждой точки есть ключ, которого нет у другой.
|
||||
FullnessIncomparable
|
||||
)
|
||||
|
||||
func (f Fullness) String() string {
|
||||
switch f {
|
||||
case FullnessEqual:
|
||||
return "equal"
|
||||
case FullnessSuperset:
|
||||
return "superset"
|
||||
case FullnessSubset:
|
||||
return "subset"
|
||||
case FullnessIncomparable:
|
||||
return "incomparable"
|
||||
default:
|
||||
return "unknown(" + strconv.Itoa(int(f)) + ")"
|
||||
}
|
||||
}
|
||||
|
||||
// Fields — ключи точки, разобранные ОДИН раз: множество всех и значения тех,
|
||||
// что несут содержание.
|
||||
//
|
||||
// Разбор вынесен в отдельный тип не ради красоты. Победитель столкновения
|
||||
// выбирается из множества кандидатов, а не парой (см. store), и сравнений там
|
||||
// квадратично по числу кандидатов. Разбирай их RelateFullness каждый раз —
|
||||
// доставка с сотней точек на одной координате разобрала бы каждую сотню раз.
|
||||
type Fields struct {
|
||||
// full — ключ с непустым значением → его исходные байты. Значения нужны
|
||||
// целиком: полнота требует не только наличия ключа, но и совпадения
|
||||
// содержания (см. Relate).
|
||||
full map[string]json.RawMessage
|
||||
// all — все ключи, включая те, чьё значение пусто.
|
||||
all map[string]struct{}
|
||||
}
|
||||
|
||||
// Analyze разбирает точку на множества ключей.
|
||||
//
|
||||
// Поле source не входит ни в одно из множеств: оно нестабильно и
|
||||
// переписывается задним числом (находка 36), так что о полноте измерения
|
||||
// ничего не говорит.
|
||||
//
|
||||
// Содержимое, которое не разбирается как JSON-объект, даёт пустые множества:
|
||||
// так оно проигрывает любой точке с содержанием и не загрязняет наблюдение о
|
||||
// несравнимых наборах.
|
||||
func Analyze(raw []byte) Fields {
|
||||
f := Fields{
|
||||
full: make(map[string]json.RawMessage),
|
||||
all: make(map[string]struct{}),
|
||||
}
|
||||
|
||||
var obj map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw, &obj); err != nil {
|
||||
return 0
|
||||
return f
|
||||
}
|
||||
|
||||
n := 0
|
||||
for k, v := range obj {
|
||||
if k == "source" {
|
||||
continue
|
||||
}
|
||||
if isEmpty(v) {
|
||||
f.all[k] = struct{}{}
|
||||
if !isEmpty(v) {
|
||||
f.full[k] = v
|
||||
}
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// RelateFullness сравнивает полноту двух точек.
|
||||
//
|
||||
// Обёртка над Analyze и Relate для одиночного сравнения; в слиянии разбор
|
||||
// переиспользуется.
|
||||
func RelateFullness(a, b []byte) Fullness {
|
||||
return Analyze(a).Relate(Analyze(b))
|
||||
}
|
||||
|
||||
// Relate сравнивает полноту: несёт ли одна точка всё, что несёт другая, и
|
||||
// сверх того.
|
||||
//
|
||||
// Разрядов сравнения два. Сперва ключи с НЕПУСТЫМ значением: точка с
|
||||
// `context: null` не полнее точки без `context`. Если они совпали — все ключи:
|
||||
// иначе `{date, qty, Min:0, Max:0}` и `{date, qty}` неразличимы, и `Min` с
|
||||
// `Max` исчезли бы из витрины по жребию тай-брейка.
|
||||
//
|
||||
// «Несёт всё, что несёт другая» — про СОДЕРЖАНИЕ, а не про имена ключей. Если
|
||||
// значения общих содержательных ключей разошлись, точки несут разные
|
||||
// измерения, и надмножество имён о полноте не говорит ничего: иначе
|
||||
// `{qty: 0.001, p1: null, p2: null}` оказывалось бы полнее `{qty: 72.5}` и
|
||||
// стирало настоящее измерение — ровно то, ради отрицания чего правило и
|
||||
// переписано. Такая пара уходит в тай-брейк как равнополная.
|
||||
//
|
||||
// Несравнимость — исход ТОЛЬКО первого разряда: у каждой точки есть
|
||||
// содержательный ключ, которого нет у другой, и объединять там было бы что.
|
||||
// Во втором разряде лишние ключи заведомо пусты, объединять в них нечего, и
|
||||
// счётчик, ради которого объединение полей отложено, не должен считать это
|
||||
// событие (иначе замер «0 из 2 897», снятый по содержательным ключам, теряет
|
||||
// сопоставимость с тем, что считает код).
|
||||
//
|
||||
// Отношение — частичный порядок: антисимметрично по построению и транзитивно
|
||||
// (если A ⊇ B и B ⊇ C, то ключи C ⊆ ключей B ⊆ ключей A, а согласие значений
|
||||
// переносится через B). На это опирается выбор победителя из множества.
|
||||
func (f Fields) Relate(g Fields) Fullness {
|
||||
rel := relateKeys(keysOf(f.full), keysOf(g.full))
|
||||
if rel == FullnessIncomparable {
|
||||
return rel
|
||||
}
|
||||
if !agreeOnShared(f.full, g.full) {
|
||||
return FullnessEqual
|
||||
}
|
||||
if rel != FullnessEqual {
|
||||
return rel
|
||||
}
|
||||
|
||||
if rel2 := relateKeys(f.all, g.all); rel2 == FullnessSuperset || rel2 == FullnessSubset {
|
||||
return rel2
|
||||
}
|
||||
return FullnessEqual
|
||||
}
|
||||
|
||||
// agreeOnShared говорит, совпадают ли значения ключей, содержательных у обеих
|
||||
// точек. Сравнение каноническое: порядок ключей и дребезг последнего разряда
|
||||
// расхождением не считаются.
|
||||
func agreeOnShared(a, b map[string]json.RawMessage) bool {
|
||||
for k, va := range a {
|
||||
vb, ok := b[k]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
n++
|
||||
if !Equal(va, vb) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return n
|
||||
return true
|
||||
}
|
||||
|
||||
func keysOf(m map[string]json.RawMessage) map[string]struct{} {
|
||||
out := make(map[string]struct{}, len(m))
|
||||
for k := range m {
|
||||
out[k] = struct{}{}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// relateKeys сравнивает два множества ключей по включению.
|
||||
func relateKeys(a, b map[string]struct{}) Fullness {
|
||||
aExtra := hasExtra(a, b)
|
||||
bExtra := hasExtra(b, a)
|
||||
|
||||
switch {
|
||||
case aExtra && bExtra:
|
||||
return FullnessIncomparable
|
||||
case aExtra:
|
||||
return FullnessSuperset
|
||||
case bExtra:
|
||||
return FullnessSubset
|
||||
default:
|
||||
return FullnessEqual
|
||||
}
|
||||
}
|
||||
|
||||
// hasExtra говорит, есть ли в a ключ, которого нет в b.
|
||||
func hasExtra(a, b map[string]struct{}) bool {
|
||||
for k := range a {
|
||||
if _, ok := b[k]; !ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Less задаёт детерминированный порядок на точках равной полноты.
|
||||
@@ -144,28 +304,81 @@ func Completeness(raw []byte) int {
|
||||
// ВНУТРИ себя, где время приёма общее. Порядок канонических форм зависит
|
||||
// только от самих значений, поэтому свёртка по журналу даёт то же состояние,
|
||||
// что приём в реальном времени.
|
||||
//
|
||||
// Порядок ТОТАЛЬНЫЙ, включая вход, который не канонизируется: иначе на паре
|
||||
// из двух неразбираемых значений Less(a,b) и Less(b,a) оба давали бы false,
|
||||
// победителем оказывался бы просто второй аргумент, и пересборка журнала
|
||||
// разошлась бы с живым приёмом. Сегодня такой вход недостижим — hae отсеивает
|
||||
// точки, не разбирающиеся в объект, — но станет достижимым со вторым
|
||||
// источником точек (импорт родного экспорта Apple).
|
||||
func Less(a, b []byte) bool {
|
||||
fa, err := Form(a)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
fb, err := Form(b)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
return bytes.Compare(fa, fb) < 0
|
||||
return bytes.Compare(SortKey(a), SortKey(b)) < 0
|
||||
}
|
||||
|
||||
// SortKey возвращает то, по чему точки упорядочиваются: каноническую форму,
|
||||
// а для содержимого, которое не канонизируется, — исходные байты.
|
||||
//
|
||||
// Вынесено наружу, чтобы слияние считало форму один раз на точку, а не по разу
|
||||
// на каждое сравнение.
|
||||
func SortKey(raw []byte) []byte {
|
||||
form, err := Form(raw)
|
||||
if err != nil {
|
||||
return raw
|
||||
}
|
||||
return form
|
||||
}
|
||||
|
||||
// isEmpty говорит, несёт ли поле содержание.
|
||||
//
|
||||
// Пусто — `null`, пустая строка, число, равное нулю, пустой объект и пустой
|
||||
// массив. Набор совпадает с `omitempty` из encoding/json минус `false` плюс
|
||||
// пустой объект, и оба отклонения сознательны:
|
||||
//
|
||||
// - `false` пустотой НЕ считается: для булева поля это одно из двух значений,
|
||||
// а не отсутствие сведений (`isIndoor: false` — тренировка на улице).
|
||||
// - Ноль считается: точка, где все значения нулевые, не должна вытеснять
|
||||
// настоящее измерение. Цена названа вслух — при столкновении нулевого
|
||||
// значения с ненулевым по одним координатам выиграет ненулевое, хотя ноль
|
||||
// бывает и настоящим измерением. Речь именно о столкновении, где одно из
|
||||
// двух содержимых заведомо неверно; одиночная нулевая точка хранится как
|
||||
// пришла.
|
||||
//
|
||||
// Значение НЕ материализуется: решение принимается по литералу. Разбор
|
||||
// значения целиком стоил бы разворачивания heartbeatSeries в []any на каждое
|
||||
// сравнение — ровно той формы, от которой разбор тела намеренно отказался
|
||||
// (197 МиБ кучи против 54 МиБ на теле 42 МиБ). При этом `0.0`, `0e0`, `-0` и
|
||||
// `{ }` обязаны считаться пустыми, поэтому по байтам сравнивать тоже нельзя:
|
||||
// число проверяется strconv, скобки — на пробельное содержимое.
|
||||
func isEmpty(v json.RawMessage) bool {
|
||||
t := bytes.TrimSpace(v)
|
||||
switch {
|
||||
case len(t) == 0, bytes.Equal(t, []byte("null")):
|
||||
return true
|
||||
case bytes.Equal(t, []byte(`""`)):
|
||||
lit := bytes.TrimSpace(v)
|
||||
if len(lit) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
switch lit[0] {
|
||||
case 'n': // null
|
||||
return bytes.Equal(lit, []byte("null"))
|
||||
case '"':
|
||||
return bytes.Equal(lit, []byte(`""`))
|
||||
case '{':
|
||||
return emptyBracketed(lit, '{', '}')
|
||||
case '[':
|
||||
return emptyBracketed(lit, '[', ']')
|
||||
case 't', 'f':
|
||||
// Булево — одно из двух значений, а не отсутствие сведений.
|
||||
return false
|
||||
default:
|
||||
f, err := strconv.ParseFloat(string(lit), 64)
|
||||
return err == nil && f == 0
|
||||
}
|
||||
}
|
||||
|
||||
// emptyBracketed говорит, что между скобками нет ничего, кроме пробелов.
|
||||
func emptyBracketed(lit []byte, open, close byte) bool {
|
||||
if len(lit) < 2 || lit[0] != open || lit[len(lit)-1] != close {
|
||||
return false
|
||||
}
|
||||
return len(bytes.TrimSpace(lit[1:len(lit)-1])) == 0
|
||||
}
|
||||
|
||||
// decode разбирает значение с числами в виде json.Number: строковый литерал
|
||||
|
||||
Reference in New Issue
Block a user