Repository navigation
Expand file tree
/
Copy pathdraft.go
More file actions
200 lines (187 loc) · 5.88 KB
/
Copy pathdraft.go
File metadata and controls
200 lines (187 loc) · 5.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
package main
// What posting something would cost, before it is posted.
//
// This is the part that can still change anything. Everything an account
// has already published may be sitting in an archive whatever happens to
// it now; a draft is the one thing still entirely in its author's hands.
import (
"fmt"
"math"
)
type alternative struct {
masked, plain string
people float64
}
func draftFinding(r *Report, a *Account, before *Lineup, text string, reveal bool) *Lineup {
post := Post{Text: own(text), Kind: Draft, When: a.Now, Where: "the draft"}
post.Lang = detect(post.Text)
index := len(a.Posts)
rd := &reader{text: post.Text, post: index, when: a.Now, lang: post.Lang}
rd.readIn(post.Lang)
added := dedupe(rd.facts)
with := *a
with.Facts = append(append([]Fact(nil), a.Facts...), added...)
with.Gender = [2]int{a.Gender[0] + rd.gender[0], a.Gender[1] + rd.gender[1]}
after := count(&with)
var lines []string
for _, s := range after.Steps {
if !mentions(s.Facts, index) {
continue
}
label := s.Masked
if reveal {
label = s.Plain
}
lines = append(lines, columns(label, "divides by "+divisor(1/math.Max(s.Factor, 1e-12)))...)
if reveal {
for _, f := range s.Facts {
if f.Post == index {
lines = append(lines, " "+quote(f.Said))
break
}
}
}
}
title := fmt.Sprintf("Posting this takes the lineup from about %s to about %s",
people(before.People), people(after.People))
advice := ""
switch {
case len(lines) == 0:
title = "Posting this does not narrow the lineup"
advice = "Nothing in it is a fact about the writer that this can count. That covers " +
"what it says outright; the README lists what it cannot see."
case after.People >= before.People*0.9:
advice = "It says something about you, and nothing the account had not said already."
default:
var alts []alternative
for _, f := range added {
alts = append(alts, loosen(&with, f, index)...)
}
best := map[string]alternative{}
var order []string
for _, alt := range alts {
if alt.people < after.People*1.5 {
continue
}
key := alt.masked
if old, ok := best[key]; !ok || alt.people > old.people {
if !ok {
order = append(order, key)
}
best[key] = alt
}
}
if len(order) > 0 {
lines = append(lines, "", "said less exactly:")
for _, key := range order {
alt := best[key]
label := alt.masked
if reveal {
label = alt.plain
}
lines = append(lines, columns(" "+label, "about "+people(alt.people))...)
}
}
advice = "The count only knows what the words say. Saying the same thing less exactly " +
"costs less, and leaving it out costs nothing. Saying something false instead " +
"moves you out of the lineup altogether, as long as it is false the same way " +
"every time and points at a crowd rather than at one real person."
}
r.add(Finding{Tag: "DRAFT ", Weight: Headline + 1, Title: title, Lines: lines, Advice: advice})
return after
}
func mentions(fs []Fact, post int) bool {
for _, f := range fs {
if f.Post == post {
return true
}
}
return false
}
// loosen tries each fact in the draft said less exactly, or not at all,
// and counts again.
func loosen(a *Account, f Fact, post int) []alternative {
var alts []alternative
try := func(masked, plain string, replace *Fact) {
b := *a
b.Facts = nil
for _, g := range a.Facts {
if g.Post == post && sameFact(g, f) {
if replace != nil {
b.Facts = append(b.Facts, *replace)
}
continue
}
b.Facts = append(b.Facts, g)
}
alts = append(alts, alternative{masked, plain, count(&b).People})
}
switch f.Kind {
case Lives, From:
if regions := regionsOf(f.Places); !samePlaces(regions, f.Places) {
g := f
g.Places, g.Near = regions, false
_, plain := placeLabels(regions)
try("the region instead of the town", "say "+plain+" instead", &g)
}
var countries []Place
seen := map[string]bool{}
for _, p := range f.Places {
if k := census().Countries[p.Country]; k != nil && !seen[p.Country] && p.Kind != CountryPlace {
seen[p.Country] = true
countries = append(countries, Place{Kind: CountryPlace, Name: k.Names[0], Country: k.Code, Population: k.Population})
}
}
if len(countries) > 0 {
g := f
g.Places, g.Near = countries, false
_, plain := placeLabels(countries)
try("the country instead", "say "+plain+" instead", &g)
}
case Age:
if f.Hi-f.Lo <= 2 {
age := float64(a.Now.Year()) - f.Hi
decade := math.Floor(age/10) * 10
g := f
g.Lo, g.Hi = float64(a.Now.Year())-decade-10, float64(a.Now.Year())-decade
try("the decade instead of the age", fmt.Sprintf("say \"in my %.0fs\" instead", decade), &g)
}
case Job:
if group := looserJob(f.Value); group != "" {
g := f
g.Value = group
try("the field instead of the job", "say one of the "+group+" instead", &g)
}
case Sex, Partner, Parent:
return nil
}
try("leaving the "+kindName[f.Kind]+" out", "leave out the "+kindName[f.Kind], nil)
return alts
}
func sameFact(a, b Fact) bool {
return a.Kind == b.Kind && a.Value == b.Value && a.Lo == b.Lo && a.Hi == b.Hi && a.Said == b.Said
}
// looserJob is the heading a job sits under in the BLS table, two levels
// up from the bottom, which is where the table stops being a job and starts
// being a field: "Registered nurses" becomes "Healthcare practitioners and
// technical occupations".
func looserJob(title string) string {
if s, ok := statuses[title]; ok {
return s.group
}
j, ok := census().Jobs[title]
if !ok || j.Group == nil {
return ""
}
g := j.Group
for g.Group != nil && g.Level > 2 {
g = g.Group
}
return g.Title
}
var kindName = map[Kind]string{
Lives: "place", From: "place", Age: "age", Job: "job", ChildBorn: "child's age",
Children: "family size", Twins: "twins", Grandparent: "family", Pet: "pet",
Height: "height", Hand: "hand", Degree: "degree", Condition: "health",
Sexuality: "sexuality", Gender: "gender", Speaks: "language", Unpriced: "detail",
}