Files
verdande/internal/store/mentions.go
T

117 lines
3.4 KiB
Go
Raw Normal View History

package store
import (
"sort"
"strings"
"unicode"
)
// MentionsIn reads the people named with an @ out of a body, and returns their
// ids in the order they are written.
//
// Slået op i navnene frem for tolket ud af teksten. Et navn kan have mellemrum,
// bindestreg og punktum i sig, og et udtryk, der skulle fange dem alle, ville
// også fange sætningen bagefter: `@Sofie i morgen` ville blive til en person, der
// hedder "Sofie i morgen", og ellers til "Sofie", alt efter hvor grådigt det var
// skrevet. Her er der ikke noget at gætte — der står et navn, eller der gør ikke.
//
// Det er også dét, der gør det forsvarligt at *dele* på: en omtale giver adgang,
// og adgang må ikke afhænge af, hvor langt et regulært udtryk valgte at læse.
//
// Længste navn først, så "@Sofie Jensen" bliver Sofie Jensen og ikke Sofie med et
// efternavn hængende bagefter — også når begge findes som konti.
func MentionsIn(body string, people []Person) []string {
if body == "" || len(people) == 0 {
return nil
}
lower := strings.ToLower(body)
byLength := make([]Person, len(people))
copy(byLength, people)
sort.SliceStable(byLength, func(i, j int) bool {
return len(byLength[i].Name) > len(byLength[j].Name)
})
// Hvilke tegn der allerede er læst som en del af en omtale. Uden det ville
// "@Sofie Jensen" også tælle som en omtale af Sofie, hvis begge er konti — og
// noten ville blive delt med en, der ikke står i den.
claimed := make([]bool, len(body))
found := map[string]int{} // bruger -> hvor i teksten, så rækkefølgen kan bæres
for _, p := range byLength {
name := strings.ToLower(strings.TrimSpace(p.Name))
if name == "" || p.ID == "" {
continue
}
needle := "@" + name
for at := 0; ; {
i := strings.Index(lower[at:], needle)
if i < 0 {
break
}
i += at
at = i + len(needle)
end := i + len(needle)
if overlaps(claimed, i, end) || !isMention(body, i, end) {
continue
}
for j := i; j < end; j++ {
claimed[j] = true
}
if _, seen := found[p.ID]; !seen {
found[p.ID] = i
}
}
}
out := make([]string, 0, len(found))
for id := range found {
out = append(out, id)
}
sort.Slice(out, func(i, j int) bool { return found[out[i]] < found[out[j]] })
return out
}
func overlaps(claimed []bool, from, to int) bool {
for i := from; i < to && i < len(claimed); i++ {
if claimed[i] {
return true
}
}
return false
}
// isMention er de to kanter om navnet.
//
// Foran: begyndelsen af noten eller noget, der ikke er en bogstavstavelse — så en
// e-mailadresse ikke bliver til en omtale, fordi der tilfældigvis står et navn
// efter snabel-a'et.
//
// Bagved: ikke midt i et længere ord. "@Sofies kalender" er skrevet om Sofie og
// tæller med — ejefaldets s hører til sætningen, ikke til en anden person — men
// "@Sofienberg" er et sted og ikke en, der skal have noten delt. Grænsen er sat
// ved, at et bogstav *lige* efter navnet kun må være et ejefalds-s.
func isMention(body string, from, to int) bool {
runes := []rune(body[:from])
if len(runes) > 0 {
prev := runes[len(runes)-1]
if unicode.IsLetter(prev) || unicode.IsDigit(prev) || prev == '@' {
return false
}
}
rest := body[to:]
if rest == "" {
return true
}
next := []rune(rest)
if next[0] == 's' || next[0] == 'S' {
next = next[1:]
if len(next) == 0 {
return true
}
}
return !unicode.IsLetter(next[0]) && !unicode.IsDigit(next[0])
}