Skip to content

Commit 1677b97

Browse files
committed
Fix #215.
1 parent 407e13d commit 1677b97

File tree

3 files changed

+204
-4
lines changed

3 files changed

+204
-4
lines changed

ext/stats/mode.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package stats
2+
3+
import (
4+
"unsafe"
5+
6+
"github.com/ncruces/go-sqlite3"
7+
)
8+
9+
func newMode() sqlite3.AggregateFunction {
10+
return &mode{}
11+
}
12+
13+
type mode struct {
14+
ints counter[int64]
15+
reals counter[float64]
16+
texts counter[string]
17+
blobs counter[string]
18+
}
19+
20+
func (m mode) Value(ctx sqlite3.Context) {
21+
var (
22+
max = 0
23+
typ = sqlite3.NULL
24+
i64 int64
25+
f64 float64
26+
str string
27+
)
28+
for k, v := range m.ints {
29+
if v > max || v == max && k < i64 {
30+
typ = sqlite3.INTEGER
31+
max = v
32+
i64 = k
33+
}
34+
}
35+
f64 = float64(i64)
36+
for k, v := range m.reals {
37+
if v > max || v == max && k < f64 {
38+
typ = sqlite3.FLOAT
39+
max = v
40+
f64 = k
41+
}
42+
}
43+
for k, v := range m.texts {
44+
if v > max || v == max && typ == sqlite3.TEXT && k < str {
45+
typ = sqlite3.TEXT
46+
max = v
47+
str = k
48+
}
49+
}
50+
for k, v := range m.blobs {
51+
if v > max || v == max && typ == sqlite3.BLOB && k < str {
52+
typ = sqlite3.BLOB
53+
max = v
54+
str = k
55+
}
56+
}
57+
switch typ {
58+
case sqlite3.INTEGER:
59+
ctx.ResultInt64(i64)
60+
case sqlite3.FLOAT:
61+
ctx.ResultFloat(f64)
62+
case sqlite3.TEXT:
63+
ctx.ResultText(str)
64+
case sqlite3.BLOB:
65+
ctx.ResultBlob(unsafe.Slice(unsafe.StringData(str), len(str)))
66+
}
67+
}
68+
69+
func (b *mode) Step(ctx sqlite3.Context, arg ...sqlite3.Value) {
70+
switch arg[0].Type() {
71+
case sqlite3.INTEGER:
72+
b.ints.add(arg[0].Int64())
73+
case sqlite3.FLOAT:
74+
b.reals.add(arg[0].Float())
75+
case sqlite3.TEXT:
76+
b.texts.add(arg[0].Text())
77+
case sqlite3.BLOB:
78+
b.blobs.add(string(arg[0].RawBlob()))
79+
}
80+
}
81+
82+
func (b *mode) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
83+
switch arg[0].Type() {
84+
case sqlite3.INTEGER:
85+
b.ints.del(arg[0].Int64())
86+
case sqlite3.FLOAT:
87+
b.reals.del(arg[0].Float())
88+
case sqlite3.TEXT:
89+
b.texts.del(arg[0].Text())
90+
case sqlite3.BLOB:
91+
b.blobs.del(string(arg[0].RawBlob()))
92+
}
93+
}
94+
95+
type counter[T comparable] map[T]int
96+
97+
func (c *counter[T]) add(k T) {
98+
if (*c) == nil {
99+
(*c) = make(counter[T])
100+
}
101+
(*c)[k]++
102+
}
103+
104+
func (c counter[T]) del(k T) {
105+
switch n := c[k]; n {
106+
default:
107+
c[k] = n - 1
108+
case 1:
109+
delete(c, k)
110+
case 0:
111+
}
112+
}

ext/stats/mode_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package stats_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/ncruces/go-sqlite3"
7+
_ "github.com/ncruces/go-sqlite3/embed"
8+
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
9+
)
10+
11+
func TestRegister_mode(t *testing.T) {
12+
t.Parallel()
13+
14+
db, err := sqlite3.Open(":memory:")
15+
if err != nil {
16+
t.Fatal(err)
17+
}
18+
defer db.Close()
19+
20+
stmt, _, err := db.Prepare(`SELECT mode(column1) FROM (VALUES (NULL), (1), (NULL), (2), (NULL), (3), (3))`)
21+
if err != nil {
22+
t.Fatal(err)
23+
}
24+
if stmt.Step() {
25+
if got := stmt.ColumnInt(0); got != 3 {
26+
t.Errorf("got %v, want 3", got)
27+
}
28+
}
29+
stmt.Close()
30+
31+
stmt, _, err = db.Prepare(`SELECT mode(column1) FROM (VALUES (1), (1), (2), (2), (3))`)
32+
if err != nil {
33+
t.Fatal(err)
34+
}
35+
if stmt.Step() {
36+
if got := stmt.ColumnInt(0); got != 1 {
37+
t.Errorf("got %v, want 1", got)
38+
}
39+
}
40+
stmt.Close()
41+
42+
stmt, _, err = db.Prepare(`SELECT mode(column1) FROM (VALUES (0.5), (1), (2.5), (2), (2.5))`)
43+
if err != nil {
44+
t.Fatal(err)
45+
}
46+
if stmt.Step() {
47+
if got := stmt.ColumnFloat(0); got != 2.5 {
48+
t.Errorf("got %v, want 2.5", got)
49+
}
50+
}
51+
stmt.Close()
52+
53+
stmt, _, err = db.Prepare(`SELECT mode(column1) FROM (VALUES ('red'), ('green'), ('blue'), ('red'))`)
54+
if err != nil {
55+
t.Fatal(err)
56+
}
57+
if stmt.Step() {
58+
if got := stmt.ColumnText(0); got != "red" {
59+
t.Errorf("got %q, want red", got)
60+
}
61+
}
62+
stmt.Close()
63+
64+
stmt, _, err = db.Prepare(`SELECT mode(column1) FROM (VALUES (X'cafebabe'), ('green'), ('blue'), (X'cafebabe'))`)
65+
if err != nil {
66+
t.Fatal(err)
67+
}
68+
if stmt.Step() {
69+
if got := stmt.ColumnText(0); got != "\xca\xfe\xba\xbe" {
70+
t.Errorf("got %q, want cafebabe", got)
71+
}
72+
}
73+
stmt.Close()
74+
75+
stmt, _, err = db.Prepare(`
76+
SELECT mode(column1) OVER (ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING)
77+
FROM (VALUES (1), (1), (2.5), ('blue'), (X'cafebabe'), (1), (1))
78+
`)
79+
if err != nil {
80+
t.Fatal(err)
81+
}
82+
for stmt.Step() {
83+
}
84+
stmt.Close()
85+
}

ext/stats/stats.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@
1818
// - regr_slope: slope of the least-squares-fit linear equation
1919
// - regr_intercept: y-intercept of the least-squares-fit linear equation
2020
// - regr_json: all regr stats in a JSON object
21-
// - percentile_disc: discrete percentile
22-
// - percentile_cont: continuous percentile
23-
// - median: median value
21+
// - percentile_disc: discrete quantile
22+
// - percentile_cont: continuous quantile
23+
// - percentile: continuous percentile
24+
// - median: middle value
25+
// - mode: most frequent value
2426
// - every: boolean and
2527
// - some: boolean or
2628
//
@@ -77,7 +79,8 @@ func Register(db *sqlite3.Conn) error {
7779
db.CreateWindowFunction("percentile_cont", 2, order, newPercentile(percentile_cont)),
7880
db.CreateWindowFunction("percentile_disc", 2, order, newPercentile(percentile_disc)),
7981
db.CreateWindowFunction("every", 1, flags, newBoolean(every)),
80-
db.CreateWindowFunction("some", 1, flags, newBoolean(some)))
82+
db.CreateWindowFunction("some", 1, flags, newBoolean(some)),
83+
db.CreateWindowFunction("mode", 1, order, newMode))
8184
}
8285

8386
const (

0 commit comments

Comments
 (0)