Skip to content

Commit 5fa7e9f

Browse files
authored
Merge pull request #12 from devalexandre/feat/group
# Release Notes ## Go 1.24 Functional Helpers Update This release updates `gofn` for Go 1.24 and expands the library with helpers focused on working with large slices of records in a cleaner, functional style. ### Highlights - Updated the module target to Go 1.24. - Modernized internal implementations with Go standard library helpers such as `slices`, `cmp`, and `math/rand/v2`. - Added row-oriented aggregation helpers to reduce repetitive `for` and `if` blocks. - Improved grouping behavior so `GroupBy` now stores the grouped rows instead of returning empty groups. - Added more tests around grouping, aggregation, sorting, partitioning, pagination, and batching. ### New Array Helpers The `array` package now includes: - `GroupSumBy`: group rows by a key and sum a numeric value. - `GroupSumByWhere`: filter, group, and sum in a single pass. - `GroupCountBy`: count rows by key. - `GroupReduceBy`: build custom aggregate summaries by key. - `GroupStatsBy`: calculate count, sum, min, max, and average by key. - `DistinctBy`: keep the first row for each key. - `IndexBy`: create a map by key, keeping the last row for duplicate keys. - `Partition`: split a slice into matching and non-matching rows. - `SortBy`: sort by a selected ordered field without mutating the original slice. - `Take`: return the first `n` rows. - `Skip`: skip the first `n` rows. - `Chunk`: split rows into batches. ### Chaining Support The `array.Array[T]` chain API now includes practical methods for common record workflows: - `GroupSumBy` - `GroupSumByWhere` - `GroupCountBy` - `GroupStatsBy` - `DistinctBy` - `IndexBy` - `Partition` - `SortByString` - `SortByFloat64` - `Take` - `Skip` - `Chunk` Because Go does not currently support method-specific type parameters, some chained helpers use practical fixed key/value types. For fully generic keys and numeric value types, use the package-level functions. ### Pipe Support The `pipe` package now includes adapters for the new helpers: - `GroupSumBy` - `GroupSumByWhere` - `GroupCountBy` - `GroupReduceBy` - `GroupStatsBy` - `DistinctBy` - `IndexBy` - `Partition` - `SortBy` - `Take` - `Skip` - `Chunk` ### Example ```go type Item struct { Name string Price float64 Qty int } items := []Item{ {"Item 1", 10.0, 1}, {"Item 4", 40.0, 10}, {"Item 4", 40.0, 15}, {"Item 4", 40.0, 25}, } totalByName := array.GroupSumBy(items, func(item Item) string { return item.Name }, func(item Item) float64 { return item.Price }, ) fmt.Println(totalByName["Item 4"]) // 120 ``` For custom summaries: ```go type Summary struct { TotalPrice float64 TotalQty int } summaryByName := array.GroupReduceBy(items, func(item Item) string { return item.Name }, func(acc Summary, item Item) Summary { acc.TotalPrice += item.Price acc.TotalQty += item.Qty return acc }, ) fmt.Println(summaryByName["Item 4"]) // {120 50} ``` ### Compatibility The existing public APIs remain available. The changes are intended to preserve the current usage style while adding new helpers and improving correctness. One behavioral fix to note: `GroupBy` now returns populated groups. Previous behavior created the group keys but did not append the matching rows. ### Verification The release was validated with: ```sh go test -count=1 ./... go vet ./... ``` Local coverage after these changes is `88.7%`.
2 parents 32c7fe9 + 6fe9294 commit 5fa7e9f

9 files changed

Lines changed: 964 additions & 12 deletions

File tree

.github/workflows/coverage.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,17 @@ jobs:
1212
name: Update coverage badge
1313
steps:
1414
- name: Checkout
15-
uses: actions/checkout@v3
15+
uses: actions/checkout@v4
1616
with:
1717
persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of your personal access token.
1818
fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository.
1919

2020
- name: Setup go
21-
uses: actions/setup-go@v3
21+
uses: actions/setup-go@v5
2222
with:
23-
go-version: '1.18'
23+
go-version: '1.24.x'
2424

25-
- uses: actions/cache@v3
25+
- uses: actions/cache@v4
2626
with:
2727
path: ~/go/pkg/mod
2828
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
@@ -58,4 +58,4 @@ jobs:
5858
uses: ad-m/github-push-action@master
5959
with:
6060
github_token: ${{ github.token }}
61-
branch: ${{ github.head_ref }}
61+
branch: ${{ github.head_ref }}

.github/workflows/go.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ jobs:
1414
build:
1515
runs-on: ubuntu-latest
1616
steps:
17-
- uses: actions/checkout@v3
17+
- uses: actions/checkout@v4
1818

1919
- name: Set up Go
20-
uses: actions/setup-go@v3
20+
uses: actions/setup-go@v5
2121
with:
22-
go-version: 1.18
22+
go-version: '1.24.x'
2323

2424
- name: Test
2525
run: go test -v ./...

README.md

Lines changed: 222 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# gofn
2-
![Coverage](https://img.shields.io/badge/Coverage-87.7%25-brightgreen)
2+
![Coverage](https://img.shields.io/badge/Coverage-88.7%25-brightgreen)
33
[![Go](https://github.com/devalexandre/gofn/actions/workflows/go.yml/badge.svg)](https://github.com/devalexandre/gofn/actions/workflows/go.yml)
44

55
# library to use golang functional
@@ -31,6 +31,16 @@
3131
- [array.Shift](#arrayshift)
3232
- [array.Sort](#arraysort)
3333
- [array.GroupBy](#arraygroupby)
34+
- [array.GroupSumBy](#arraygroupsumby)
35+
- [array.GroupSumByWhere](#arraygroupsumbywhere)
36+
- [array.GroupCountBy](#arraygroupcountby)
37+
- [array.GroupReduceBy](#arraygroupreduceby)
38+
- [array.GroupStatsBy](#arraygroupstatsby)
39+
- [array.DistinctBy](#arraydistinctby)
40+
- [array.IndexBy](#arrayindexby)
41+
- [array.Partition](#arraypartition)
42+
- [array.SortBy](#arraysortby)
43+
- [array.Take, array.Skip, array.Chunk](#arraytake-arrayskip-arraychunk)
3444
- [chaining functions](#chaining-functions)
3545

3646

@@ -379,6 +389,160 @@ type Itens struct {
379389
fmt.Println(len(grouped)) // 4
380390

381391
```
392+
393+
### array.GroupSumBy
394+
Group items by a key and sum a numeric value for each group.
395+
396+
```go
397+
type Itens struct {
398+
Name string
399+
Price float64
400+
Description string
401+
Qty int
402+
}
403+
404+
itens := []Itens{
405+
{"Item 1", 10.0, "Description 1", 1},
406+
{"Item 2", 20.0, "Description 2", 2},
407+
{"Item 3", 30.0, "Description 3", 3},
408+
{"Item 4", 40.0, "Description 4", 10},
409+
{"Item 4", 40.0, "Description 4", 15},
410+
{"Item 4", 40.0, "Description 4", 25},
411+
}
412+
413+
priceByName := array.GroupSumBy(itens,
414+
func(item Itens) string { return item.Name },
415+
func(item Itens) float64 { return item.Price },
416+
)
417+
418+
qtyByName := array.GroupSumBy(itens,
419+
func(item Itens) string { return item.Name },
420+
func(item Itens) int { return item.Qty },
421+
)
422+
423+
fmt.Println(priceByName["Item 4"]) // 120
424+
fmt.Println(qtyByName["Item 4"]) // 50
425+
```
426+
427+
### array.GroupSumByWhere
428+
Filter, group and sum in one pass.
429+
430+
```go
431+
priceByName := array.GroupSumByWhere(itens,
432+
func(item Itens) bool { return item.Qty > 3 },
433+
func(item Itens) string { return item.Name },
434+
func(item Itens) float64 { return item.Price },
435+
)
436+
437+
fmt.Println(priceByName["Item 4"]) // 120
438+
```
439+
440+
### array.GroupCountBy
441+
Count rows by a key.
442+
443+
```go
444+
countByName := array.GroupCountBy(itens, func(item Itens) string {
445+
return item.Name
446+
})
447+
448+
fmt.Println(countByName["Item 4"]) // 3
449+
```
450+
451+
### array.GroupReduceBy
452+
Build custom summaries by group.
453+
454+
```go
455+
type Summary struct {
456+
TotalPrice float64
457+
TotalQty int
458+
}
459+
460+
summaryByName := array.GroupReduceBy(itens,
461+
func(item Itens) string {
462+
return item.Name
463+
},
464+
func(acc Summary, item Itens) Summary {
465+
acc.TotalPrice += item.Price
466+
acc.TotalQty += item.Qty
467+
return acc
468+
},
469+
)
470+
471+
fmt.Println(summaryByName["Item 4"]) // {120 50}
472+
```
473+
474+
### array.GroupStatsBy
475+
Calculate count, sum, min, max and average by group.
476+
477+
```go
478+
statsByName := array.GroupStatsBy(itens,
479+
func(item Itens) string { return item.Name },
480+
func(item Itens) float64 { return item.Price },
481+
)
482+
483+
fmt.Println(statsByName["Item 4"].Count) // 3
484+
fmt.Println(statsByName["Item 4"].Sum) // 120
485+
fmt.Println(statsByName["Item 4"].Avg) // 40
486+
```
487+
488+
### array.DistinctBy
489+
Keep the first row for each key.
490+
491+
```go
492+
uniqueByName := array.DistinctBy(itens, func(item Itens) string {
493+
return item.Name
494+
})
495+
496+
fmt.Println(len(uniqueByName)) // 4
497+
```
498+
499+
### array.IndexBy
500+
Create a map by key. If a key repeats, the last row wins.
501+
502+
```go
503+
itemByName := array.IndexBy(itens, func(item Itens) string {
504+
return item.Name
505+
})
506+
507+
fmt.Println(itemByName["Item 4"].Qty) // 25
508+
```
509+
510+
### array.Partition
511+
Split rows into matched and unmatched slices.
512+
513+
```go
514+
highQty, lowQty := array.Partition(itens, func(item Itens) bool {
515+
return item.Qty > 3
516+
})
517+
518+
fmt.Println(len(highQty)) // 3
519+
fmt.Println(len(lowQty)) // 3
520+
```
521+
522+
### array.SortBy
523+
Sort rows by a selected field without mutating the original slice.
524+
525+
```go
526+
byQty := array.SortBy(itens, func(item Itens) int {
527+
return item.Qty
528+
})
529+
530+
fmt.Println(byQty[0].Qty) // 1
531+
```
532+
533+
### array.Take, array.Skip, array.Chunk
534+
Use these helpers for pagination and batch processing.
535+
536+
```go
537+
firstPage := array.Take(itens, 2)
538+
nextRows := array.Skip(itens, 2)
539+
batches := array.Chunk(itens, 2)
540+
541+
fmt.Println(len(firstPage)) // 2
542+
fmt.Println(len(nextRows)) // 4
543+
fmt.Println(len(batches)) // 3
544+
```
545+
382546
## chaining functions
383547

384548
You can chain the functions together.
@@ -398,3 +562,60 @@ data := array.Array[int]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
398562
fmt.Println(a) // [4 8 12 16 20]
399563

400564
```
565+
566+
You can also group and sum values after filtering.
567+
568+
```go
569+
type Itens struct {
570+
Name string
571+
Price float64
572+
Description string
573+
Qty int
574+
}
575+
576+
data := array.Array[Itens]{
577+
{"Item 1", 10.0, "Description 1", 1},
578+
{"Item 2", 20.0, "Description 2", 2},
579+
{"Item 3", 30.0, "Description 3", 3},
580+
{"Item 4", 40.0, "Description 4", 10},
581+
{"Item 4", 40.0, "Description 4", 15},
582+
{"Item 4", 40.0, "Description 4", 25},
583+
}
584+
585+
priceByName := data.
586+
Filter(func(item Itens) bool {
587+
return item.Qty > 3
588+
}).
589+
GroupSumBy(
590+
func(item Itens) string { return item.Name },
591+
func(item Itens) float64 { return item.Price },
592+
)
593+
594+
fmt.Println(priceByName["Item 4"]) // 120
595+
```
596+
597+
Some helpers are also available in chain form.
598+
599+
```go
600+
countByName := data.GroupCountBy(func(item Itens) string {
601+
return item.Name
602+
})
603+
604+
statsByName := data.GroupStatsBy(
605+
func(item Itens) string { return item.Name },
606+
func(item Itens) float64 { return item.Price },
607+
)
608+
609+
uniqueByName := data.DistinctBy(func(item Itens) string {
610+
return item.Name
611+
})
612+
613+
highQty, lowQty := data.Partition(func(item Itens) bool {
614+
return item.Qty > 3
615+
})
616+
617+
page := data.
618+
SortByFloat64(func(item Itens) float64 { return item.Price }).
619+
Skip(10).
620+
Take(10)
621+
```

array/array.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,58 @@ func (a Array[T]) Sort(f func(i, j int) bool) Array[T] {
6161
func (a Array[T]) GroupBy(f func(T) string) map[string][]T {
6262
return GroupBy(a, f)
6363
}
64+
65+
func (a Array[T]) GroupSumBy(key func(T) string, value func(T) float64) map[string]float64 {
66+
return GroupSumBy(a, key, value)
67+
}
68+
69+
func (a Array[T]) GroupSumByWhere(where func(T) bool, key func(T) string, value func(T) float64) map[string]float64 {
70+
return GroupSumByWhere(a, where, key, value)
71+
}
72+
73+
func (a Array[T]) GroupCountBy(key func(T) string) map[string]int {
74+
return GroupCountBy(a, key)
75+
}
76+
77+
func (a Array[T]) GroupStatsBy(key func(T) string, value func(T) float64) map[string]GroupStats[float64] {
78+
return GroupStatsBy(a, key, value)
79+
}
80+
81+
func (a Array[T]) DistinctBy(key func(T) string) Array[T] {
82+
return DistinctBy(a, key)
83+
}
84+
85+
func (a Array[T]) IndexBy(key func(T) string) map[string]T {
86+
return IndexBy(a, key)
87+
}
88+
89+
func (a Array[T]) Partition(f func(T) bool) (Array[T], Array[T]) {
90+
matched, unmatched := Partition(a, f)
91+
return matched, unmatched
92+
}
93+
94+
func (a Array[T]) SortByString(key func(T) string) Array[T] {
95+
return SortBy(a, key)
96+
}
97+
98+
func (a Array[T]) SortByFloat64(key func(T) float64) Array[T] {
99+
return SortBy(a, key)
100+
}
101+
102+
func (a Array[T]) Take(n int) Array[T] {
103+
return Take(a, n)
104+
}
105+
106+
func (a Array[T]) Skip(n int) Array[T] {
107+
return Skip(a, n)
108+
}
109+
110+
func (a Array[T]) Chunk(size int) []Array[T] {
111+
chunks := Chunk(a, size)
112+
result := make([]Array[T], len(chunks))
113+
for i, chunk := range chunks {
114+
result[i] = chunk
115+
}
116+
117+
return result
118+
}

0 commit comments

Comments
 (0)