Skip to content

Commit 996ed76

Browse files
committed
Add facetCount
1 parent 532bd73 commit 996ed76

7 files changed

Lines changed: 297 additions & 1 deletion

File tree

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,20 @@ counts, wasRead := x.ValueCounts("Country")
369369

370370
The original csv is not modified. `wasRead` is false if the csv is empty or the field name does not exist; in that case a copy of the csv is returned.
371371

372+
`FacetCount` returns a map that counts how many times each string appears in a column (data cells only, the header cell is never counted). Case does not matter by default: `Foo`, `foo` and `FOO` are counted together under the lower-case key `foo`:
373+
374+
```go
375+
counts, wasRead := x.FacetCount("Country")
376+
```
377+
378+
`wasRead` is false if the csv is empty, the field name is empty, or the field name does not exist; in that case an empty map is returned. A csv with only the header row returns an empty map and true.
379+
380+
`FacetCountCaseSensitive` is the same but case matters: `Foo`, `foo` and `FOO` are counted as separate entries:
381+
382+
```go
383+
counts, wasRead = x.FacetCountCaseSensitive("Country")
384+
```
385+
372386
### Group by
373387

374388
`GroupBy` collapses the data rows into one output row per group, where a group is all the data rows whose key fields have the same values. The output header row is the key field names followed by the `As` name of each aggregation; each output data row is the key values followed by one cell per aggregation. Groups appear in order of first occurrence:

distinct.go

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package simplecsv
22

3-
import "strconv"
3+
import (
4+
"strconv"
5+
"strings"
6+
)
47

58
// Distinct returns the distinct values of the data cells of the column
69
// with the header name field, in order of first appearance. The header
@@ -55,3 +58,47 @@ func (s SimpleCsv) ValueCounts(field string) (SimpleCsv, bool) {
5558
}
5659
return newCsv, true
5760
}
61+
62+
// facetCount returns a map that counts how many times each string appears in
63+
// the data cells of the column with the header name field. If caseSensitive
64+
// is false, case does not matter: values that only differ in case are
65+
// counted together under their lower-case key. If it is true, they are
66+
// separate entries. The header cell in row 0 is never counted. It returns
67+
// an empty map and false if the csv is empty, the field name is empty, or
68+
// the field name does not exist. A csv with only the header row returns an
69+
// empty map and true.
70+
func (s SimpleCsv) facetCount(field string, caseSensitive bool) (map[string]int, bool) {
71+
if field == "" {
72+
return map[string]int{}, false
73+
}
74+
columnPosition := s.GetHeaderPosition(field)
75+
if columnPosition == -1 {
76+
return map[string]int{}, false
77+
}
78+
counts := make(map[string]int)
79+
for i := 1; i < len(s); i++ {
80+
value := s[i][columnPosition]
81+
if !caseSensitive {
82+
value = strings.ToLower(value)
83+
}
84+
counts[value]++
85+
}
86+
return counts, true
87+
}
88+
89+
// FacetCount returns a map that counts how many times each string appears
90+
// in the data cells of the column with the header name field. The header
91+
// cell in row 0 is never counted. Case does not matter by default: Foo,
92+
// foo and FOO are counted together under the lower-case key "foo". If the
93+
// csv is empty, the field name is empty, or the field name does not exist,
94+
// it returns an empty map and false. A csv with only the header row
95+
// returns an empty map and true.
96+
func (s SimpleCsv) FacetCount(field string) (map[string]int, bool) {
97+
return s.facetCount(field, false)
98+
}
99+
100+
// FacetCountCaseSensitive is like FacetCount but case matters: Foo, foo
101+
// and FOO are counted as separate entries.
102+
func (s SimpleCsv) FacetCountCaseSensitive(field string) (map[string]int, bool) {
103+
return s.facetCount(field, true)
104+
}

distinct_test.go

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,3 +261,205 @@ func TestDistinctValueCountsDoNotMutateSource(t *testing.T) {
261261
t.Errorf("mutating failed ValueCounts result changed source: %v", s)
262262
}
263263
}
264+
265+
func TestSimpleCsv_FacetCount(t *testing.T) {
266+
tests := []struct {
267+
name string
268+
s SimpleCsv
269+
field string
270+
want map[string]int
271+
want1 bool
272+
}{
273+
{
274+
name: "FacetCountIgnoresCaseByDefault",
275+
s: SimpleCsv{
276+
{"Country", "City"},
277+
{"PT", "Lisbon"},
278+
{"pt", "Porto"},
279+
{"PT", "Braga"},
280+
{"ES", "Madrid"},
281+
},
282+
field: "Country",
283+
want: map[string]int{"pt": 3, "es": 1},
284+
want1: true,
285+
},
286+
{
287+
name: "FacetCountGroupsOnlyCaseVariants",
288+
s: SimpleCsv{
289+
{"Code"},
290+
{"Foo"},
291+
{"foo"},
292+
{"FOO"},
293+
},
294+
field: "Code",
295+
want: map[string]int{"foo": 3},
296+
want1: true,
297+
},
298+
{
299+
name: "FacetCountKeepsEmptyValues",
300+
s: SimpleCsv{
301+
{"Code"},
302+
{""},
303+
{"x"},
304+
{""},
305+
},
306+
field: "Code",
307+
want: map[string]int{"": 2, "x": 1},
308+
want1: true,
309+
},
310+
{
311+
name: "FacetCountHeaderCellNotIncluded",
312+
s: SimpleCsv{
313+
{"Country"},
314+
{"Country"},
315+
{"PT"},
316+
{"country"},
317+
},
318+
field: "Country",
319+
want: map[string]int{"country": 2, "pt": 1},
320+
want1: true,
321+
},
322+
{
323+
name: "FacetCountHeaderOnly",
324+
want: map[string]int{},
325+
s: SimpleCsv{
326+
{"Country", "City"},
327+
},
328+
field: "Country",
329+
want1: true,
330+
},
331+
{
332+
name: "FacetCountEmpty",
333+
want: map[string]int{},
334+
s: SimpleCsv{},
335+
field: "Country",
336+
want1: false,
337+
},
338+
{
339+
name: "FacetCountMissingField",
340+
want: map[string]int{},
341+
s: SimpleCsv{
342+
{"Country", "City"},
343+
{"PT", "Lisbon"},
344+
},
345+
field: "Book",
346+
want1: false,
347+
},
348+
{
349+
name: "FacetCountEmptyFieldName",
350+
want: map[string]int{},
351+
s: SimpleCsv{
352+
{"Country", "City"},
353+
{"PT", "Lisbon"},
354+
},
355+
field: "",
356+
want1: false,
357+
},
358+
}
359+
for _, tt := range tests {
360+
t.Run(tt.name, func(t *testing.T) {
361+
got, got1 := tt.s.FacetCount(tt.field)
362+
if !reflect.DeepEqual(got, tt.want) {
363+
t.Errorf("SimpleCsv.FacetCount() got = %v, want %v", got, tt.want)
364+
}
365+
if got1 != tt.want1 {
366+
t.Errorf("SimpleCsv.FacetCount() got1 = %v, want %v", got1, tt.want1)
367+
}
368+
})
369+
}
370+
}
371+
372+
func TestSimpleCsv_FacetCountCaseSensitive(t *testing.T) {
373+
tests := []struct {
374+
name string
375+
s SimpleCsv
376+
field string
377+
want map[string]int
378+
want1 bool
379+
}{
380+
{
381+
name: "FacetCountCaseSensitiveSeparatesCase",
382+
s: SimpleCsv{
383+
{"Country", "City"},
384+
{"PT", "Lisbon"},
385+
{"pt", "Porto"},
386+
{"PT", "Braga"},
387+
{"ES", "Madrid"},
388+
},
389+
field: "Country",
390+
want: map[string]int{"PT": 2, "pt": 1, "ES": 1},
391+
want1: true,
392+
},
393+
{
394+
name: "FacetCountCaseSensitiveKeepsEmptyValues",
395+
s: SimpleCsv{
396+
{"Code"},
397+
{""},
398+
{"x"},
399+
{""},
400+
},
401+
field: "Code",
402+
want: map[string]int{"": 2, "x": 1},
403+
want1: true,
404+
},
405+
{
406+
name: "FacetCountCaseSensitiveHeaderCellNotIncluded",
407+
s: SimpleCsv{
408+
{"Country"},
409+
{"Country"},
410+
{"PT"},
411+
{"country"},
412+
},
413+
field: "Country",
414+
want: map[string]int{"Country": 1, "PT": 1, "country": 1},
415+
want1: true,
416+
},
417+
{
418+
name: "FacetCountCaseSensitiveHeaderOnly",
419+
want: map[string]int{},
420+
s: SimpleCsv{
421+
{"Country", "City"},
422+
},
423+
field: "Country",
424+
want1: true,
425+
},
426+
{
427+
name: "FacetCountCaseSensitiveEmpty",
428+
want: map[string]int{},
429+
s: SimpleCsv{},
430+
field: "Country",
431+
want1: false,
432+
},
433+
{
434+
name: "FacetCountCaseSensitiveMissingField",
435+
want: map[string]int{},
436+
s: SimpleCsv{
437+
{"Country", "City"},
438+
{"PT", "Lisbon"},
439+
},
440+
field: "Book",
441+
want1: false,
442+
},
443+
{
444+
name: "FacetCountCaseSensitiveEmptyFieldName",
445+
want: map[string]int{},
446+
s: SimpleCsv{
447+
{"Country", "City"},
448+
{"PT", "Lisbon"},
449+
},
450+
field: "",
451+
want1: false,
452+
},
453+
}
454+
for _, tt := range tests {
455+
t.Run(tt.name, func(t *testing.T) {
456+
got, got1 := tt.s.FacetCountCaseSensitive(tt.field)
457+
if !reflect.DeepEqual(got, tt.want) {
458+
t.Errorf("SimpleCsv.FacetCountCaseSensitive() got = %v, want %v", got, tt.want)
459+
}
460+
if got1 != tt.want1 {
461+
t.Errorf("SimpleCsv.FacetCountCaseSensitive() got1 = %v, want %v", got1, tt.want1)
462+
}
463+
})
464+
}
465+
}

doc.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,22 @@ The header cell is never counted and the original csv is not modified:
331331
`wasRead` is false if the csv is empty or the field name does not exist; in
332332
that case a copy of the csv is returned.
333333
334+
`FacetCount` returns a map that counts how many times each string appears in
335+
the data cells of a column. The header cell is never counted. Case does not
336+
matter by default: `Foo`, `foo` and `FOO` are counted together under the
337+
lower-case key `foo`:
338+
339+
counts, wasRead := x.FacetCount("Country")
340+
341+
`wasRead` is false if the csv is empty, the field name is empty, or the
342+
field name does not exist; in that case an empty map is returned. A csv
343+
with only the header row returns an empty map and true.
344+
345+
`FacetCountCaseSensitive` is the same but case matters: `Foo`, `foo` and
346+
`FOO` are counted as separate entries:
347+
348+
counts, wasRead = x.FacetCountCaseSensitive("Country")
349+
334350
* GROUP BY *
335351
336352
`GroupBy` collapses the data rows into one output row per group, where a

docs/api.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ description: A practical map of simplecsv APIs grouped by the job they solve.
114114
<li><code>UniqueByFields</code></li>
115115
<li><code>Distinct</code></li>
116116
<li><code>ValueCounts</code></li>
117+
<li><code>FacetCount</code></li>
118+
<li><code>FacetCountCaseSensitive</code></li>
117119
<li><code>SortByField</code></li>
118120
<li><code>SortByFields</code></li>
119121
</ul>

docs/concepts.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ row 2: 102, Ben, paused
9595
<ul>
9696
<li><code>Distinct</code></li>
9797
<li><code>ValueCounts</code></li>
98+
<li><code>FacetCount</code></li>
9899
<li><code>GetColumnByField</code></li>
99100
<li><code>EachDataRow</code></li>
100101
</ul>

docs/recipes/summarize.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,20 @@ counts, ok := orders.ValueCounts("status")
118118
if !ok {
119119
log.Fatal("status field not found")
120120
}
121+
{% endhighlight %}
122+
123+
<p>Use <code>FacetCount</code> when you need the counts as a map, e.g. for
124+
facet navigation. It is case-insensitive by default; use
125+
<code>FacetCountCaseSensitive</code> when case matters:</p>
126+
127+
{% highlight go %}
128+
facet, ok := orders.FacetCount("status")
129+
if !ok {
130+
log.Fatal("status field not found")
131+
}
132+
for status, n := range facet {
133+
fmt.Println(status, n)
134+
}
121135
{% endhighlight %}
122136

123137
<h2 id="stable-report-order">Control report order</h2>

0 commit comments

Comments
 (0)