// Copyright 2021 Molecula Corp. All rights reserved. package ingest import ( "fmt" "reflect" "strconv" "unsafe" ) // StringTable is a mapping of strings to temporary IDs. // All mapped-to IDs fall in the range [0, len). The zero value, // a nil map, instead just parses the numbers. // // We keep the array of names in creation order because we want reproducibility; // the first key we see is always key 0. Otherwise, the keys are created in // arbitrary orders. type StringTable struct { names []string values map[string]uint64 } // NewStringTable just creates a string table with a non-nil map. func NewStringTable() *StringTable { return &StringTable{values: map[string]uint64{}} } // unsafe is func pretendByteIsString(data []byte) (result string) { dH := (*reflect.SliceHeader)(unsafe.Pointer(&data)) sH := (*reflect.StringHeader)(unsafe.Pointer(&result)) sH.Data = dH.Data sH.Len = dH.Len return result } // ID returns an ID associated to the string, adding it to the table if it is not already present, // or parsing an integer if there's no table. func (tbl *StringTable) ID(in []byte) (uint64, error) { str := pretendByteIsString(in) if tbl != nil { id, ok := tbl.values[str] if !ok { id = uint64(len(tbl.values)) tbl.values[str] = id tbl.names = append(tbl.names, str) } return id, nil } return strconv.ParseUint(str, 10, 64) } // SignedID returns an ID associated to the string, adding it to the table if it is not already present, // or parsing an integer if there's no table. It yields signed values only. func (tbl *StringTable) IntID(in []byte) (int64, error) { str := pretendByteIsString(in) if tbl != nil { id, ok := tbl.values[str] if !ok { id = uint64(len(tbl.values)) tbl.values[str] = id tbl.names = append(tbl.names, str) } return int64(id), nil } return strconv.ParseInt(str, 10, 64) } // MapForStringTable, given a string table mapping strings to consecutive // integers and a translation function from strings to "real" keys, yields // a translation/lookup slice. If it cannot translate all the keys, it // returns an error. func (tbl *StringTable) MakeIDMap(keys KeyTranslator) ([]uint64, error) { lookedUp, err := keys.TranslateKeys(tbl.names...) if err != nil { return nil, err } if len(lookedUp) != len(tbl.names) { return nil, fmt.Errorf("missing keys: expected %d keys, got %d", len(tbl.values), len(lookedUp)) } out := make([]uint64, len(tbl.names)) for i, v := range tbl.names { out[i] = lookedUp[v] } return out, nil } // translateSigned replaces values from 0 to len(mapping)-1 with the // elements of mapping. It yields an error if any values aren't // mapped. func translateSigned(mapping []uint64, values []int64) error { oops := 0 for i, v := range values { if v >= int64(len(mapping)) { oops++ } else { values[i] = int64(mapping[v]) } } if oops > 0 { return fmt.Errorf("encountered %d out-of-range signed values when applying translation mapping", oops) } return nil } // translateUnsigned replaces values from 0 to len(mapping)-1 with the // elements of mapping. It yields an error if any values aren't // mapped. func translateUnsigned(mapping []uint64, values []uint64) error { oops := 0 for i, v := range values { if v >= uint64(len(mapping)) { oops++ } else { values[i] = mapping[v] } } if oops > 0 { return fmt.Errorf("encountered %d out-of-range signed values when applying translation mapping", oops) } return nil }