Repository: elliotchance/pie
Pull request #103 (2019-05-05 15:40:29)

Issues/98 SortWith and SortStableWith
See #98

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/elliotchance/pie/103)
<!-- Reviewable:end -->


codecov-io: # [Codecov](https://codecov.io/gh/elliotchance/pie/pull/103?src=pr&el=h1) Report
> :exclamation: No coverage uploaded for pull request base (`master@57366d4`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference#section-missing-base-commit).
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/elliotchance/pie/pull/103/graphs/tree.svg?width=650&token=OvugwUQGM3&height=150&src=pr)](https://codecov.io/gh/elliotchance/pie/pull/103?src=pr&el=tree)

```diff
@@           Coverage Diff           @@
##             master   #103   +/-   ##
=======================================
  Coverage          ?   100%           
=======================================
  Files             ?      7           
  Lines             ?    837           
  Branches          ?      0           
=======================================
  Hits              ?    837           
  Misses            ?      0           
  Partials          ?      0
```


| [Impacted Files](https://codecov.io/gh/elliotchance/pie/pull/103?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pie/float64s\_pie.go](https://codecov.io/gh/elliotchance/pie/pull/103/diff?src=pr&el=tree#diff-cGllL2Zsb2F0NjRzX3BpZS5nbw==) | `100% <100%> (ø)` | |
| [pie/carpointers\_pie.go](https://codecov.io/gh/elliotchance/pie/pull/103/diff?src=pr&el=tree#diff-cGllL2NhcnBvaW50ZXJzX3BpZS5nbw==) | `100% <100%> (ø)` | |
| [pie/cars\_pie.go](https://codecov.io/gh/elliotchance/pie/pull/103/diff?src=pr&el=tree#diff-cGllL2NhcnNfcGllLmdv) | `100% <100%> (ø)` | |
| [pie/ints\_pie.go](https://codecov.io/gh/elliotchance/pie/pull/103/diff?src=pr&el=tree#diff-cGllL2ludHNfcGllLmdv) | `100% <100%> (ø)` | |
| [pie/strings\_pie.go](https://codecov.io/gh/elliotchance/pie/pull/103/diff?src=pr&el=tree#diff-cGllL3N0cmluZ3NfcGllLmdv) | `100% <100%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/elliotchance/pie/pull/103?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/elliotchance/pie/pull/103?src=pr&el=footer). Last update [57366d4...dcd5867](https://codecov.io/gh/elliotchance/pie/pull/103?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

Deleplace: I think the code re-gen produces a difference somewhere. I'll investigate and fix.

Patch:
diff --git a/README.md b/README.md
index 354dfb8..c4d4b5c 100644
--- a/README.md
+++ b/README.md
@@ -152,14 +152,17 @@ This will only generate `myInts.Average`, `myInts.Sum` and `myStrings.Filter`.
 | `Min`        | ✓      | ✓      |       |      | n        | The minimum value, or a zeroed value. |
 | `Product`    |        | ✓      |       |      | n        | Product of all elements. |
 | `Random`     | ✓      | ✓      | ✓     |      | 1        | Select a random element, or a zeroed value if empty. |
+| `Reduce`     | ✓      | ✓      |       |      | n        | Continously apply a function to the slice (left to right), reducing it to a single value. |
 | `Reverse`    | ✓      | ✓      | ✓     |      | n        | Reverse elements. |
-| `Send`     | ✓      | ✓      | ✓     |      | n        | Send all element to channel. |
+| `Send`       | ✓      | ✓      | ✓     |      | n        | Send all element to channel. |
 | `Sort`       | ✓      | ✓      |       |      | n⋅log(n) | Return a new sorted slice. |
+| `SortUsing`  | ✓      |        | ✓     |      | n⋅log(n) | Return a new sorted slice, using custom comparator. |
+| `SortStableUsing`  | ✓ |       | ✓     |      | n⋅log(n) | Return a new sorted slice, using custom comparator, keeping the original order of equal elements. |
 | `Sum`        |        | ✓      |       |      | n        | Sum (total) of all elements. |
 | `Shuffle`    | ✓      | ✓      | ✓     |      | n        | Returns a new shuffled slice. |
 | `Top`        | ✓      | ✓      | ✓     |      | n        | Gets several elements from top(head of slice).|
 | `ToStrings`  | ✓      | ✓      | ✓     |      | n        | Transforms each element to a string. |
-| `Unique`     | ✓      | ✓      |       |      | n⋅log(n) | Return a new slice with only unique elements. |
+| `Unique`     | ✓      | ✓      |       |      | n        | Return a new slice with only unique elements. |
 | `Values`     |        |        |       | ✓    | n        | Returns all values in the map (in random order). |
 
 # FAQ
diff --git a/functions/main.go b/functions/main.go
index 1099436..c0aeb32 100644
--- a/functions/main.go
+++ b/functions/main.go
@@ -46,9 +46,12 @@ var Functions = []struct {
 	{"Min", "min.go", ForNumbersAndStrings},
 	{"Product", "product.go", ForNumbers},
 	{"Random", "random.go", ForAll},
+	{"Reduce", "reduce.go", ForNumbersAndStrings},
 	{"Reverse", "reverse.go", ForAll},
 	{"Send", "send.go", ForAll},
 	{"Sort", "sort.go", ForNumbersAndStrings},
+	{"SortUsing", "sort_using.go", ForStrings | ForStructs},
+	{"SortStableUsing", "sort_stable_using.go", ForStrings | ForStructs},
 	{"Sum", "sum.go", ForNumbers},
 	{"Shuffle", "shuffle.go", ForAll},
 	{"Top", "top.go", ForAll},
diff --git a/functions/reduce.go b/functions/reduce.go
new file mode 100644
index 0000000..7fa3a8e
--- /dev/null
+++ b/functions/reduce.go
@@ -0,0 +1,17 @@
+package functions
+
+// Reduce continually applies the provided function
+// over the slice. Reducing the elements to a single value.
+//
+// Returns a zero value of ElementType if there are no elements in the slice. It will panic if the reducer is nil and the slice has more than one element (required to invoke reduce).
+// Otherwise returns result of applying reducer from left to right.
+func (ss SliceType) Reduce(reducer func(ElementType, ElementType) ElementType) (el ElementType) {
+	if len(ss) == 0 {
+		return
+	}
+	el = ss[0]
+	for _, s := range ss[1:] {
+		el = reducer(el, s)
+	}
+	return
+}
diff --git a/functions/sort.go b/functions/sort.go
index 9423236..4bc1d9b 100644
--- a/functions/sort.go
+++ b/functions/sort.go
@@ -15,7 +15,7 @@ func (ss SliceType) Sort() SliceType {
 		return ss
 	}
 
-	sorted := make([]ElementType, len(ss))
+	sorted := make(SliceType, len(ss))
 	copy(sorted, ss)
 	sort.Slice(sorted, func(i, j int) bool {
 		return sorted[i] < sorted[j]
diff --git a/functions/sort_stable_using.go b/functions/sort_stable_using.go
new file mode 100644
index 0000000..81a26b2
--- /dev/null
+++ b/functions/sort_stable_using.go
@@ -0,0 +1,23 @@
+package functions
+
+import (
+	"sort"
+)
+
+// SortStableUsing works similar to sort.SliceStable. However, unlike sort.SliceStable the
+// slice returned will be reallocated as to not modify the input slice.
+func (ss SliceType) SortStableUsing(less func(a, b ElementType) bool) SliceType {
+	// Avoid the allocation. If there is one element or less it is already
+	// sorted.
+	if len(ss) < 2 {
+		return ss
+	}
+
+	sorted := make(SliceType, len(ss))
+	copy(sorted, ss)
+	sort.SliceStable(sorted, func(i, j int) bool {
+		return less(sorted[i], sorted[j])
+	})
+
+	return sorted
+}
diff --git a/functions/sort_using.go b/functions/sort_using.go
new file mode 100644
index 0000000..a8ed393
--- /dev/null
+++ b/functions/sort_using.go
@@ -0,0 +1,23 @@
+package functions
+
+import (
+	"sort"
+)
+
+// SortUsing works similar to sort.Slice. However, unlike sort.Slice the
+// slice returned will be reallocated as to not modify the input slice.
+func (ss SliceType) SortUsing(less func(a, b ElementType) bool) SliceType {
+	// Avoid the allocation. If there is one element or less it is already
+	// sorted.
+	if len(ss) < 2 {
+		return ss
+	}
+
+	sorted := make(SliceType, len(ss))
+	copy(sorted, ss)
+	sort.Slice(sorted, func(i, j int) bool {
+		return less(sorted[i], sorted[j])
+	})
+
+	return sorted
+}
diff --git a/pie/carpointers_pie.go b/pie/carpointers_pie.go
index 617163d..c43b3af 100755
--- a/pie/carpointers_pie.go
+++ b/pie/carpointers_pie.go
@@ -5,6 +5,7 @@ import (
 	"encoding/json"
 	"github.com/elliotchance/pie/pie/util"
 	"math/rand"
+	"sort"
 )
 
 // All will return true if all callbacks return true. It follows the same logic
@@ -259,6 +260,42 @@ func (ss carPointers) Send(ctx context.Context, ch chan<- *car) carPointers {
 	return ss
 }
 
+// SortUsing works similar to sort.Slice. However, unlike sort.Slice the
+// slice returned will be reallocated as to not modify the input slice.
+func (ss carPointers) SortUsing(less func(a, b *car) bool) carPointers {
+	// Avoid the allocation. If there is one element or less it is already
+	// sorted.
+	if len(ss) < 2 {
+		return ss
+	}
+
+	sorted := make(carPointers, len(ss))
+	copy(sorted, ss)
+	sort.Slice(sorted, func(i, j int) bool {
+		return less(sorted[i], sorted[j])
+	})
+
+	return sorted
+}
+
+// SortStableUsing works similar to sort.SliceStable. However, unlike sort.SliceStable the
+// slice returned will be reallocated as to not modify the input slice.
+func (ss carPointers) SortStableUsing(less func(a, b *car) bool) carPointers {
+	// Avoid the allocation. If there is one element or less it is already
+	// sorted.
+	if len(ss) < 2 {
+		return ss
+	}
+
+	sorted := make(carPointers, len(ss))
+	copy(sorted, ss)
+	sort.SliceStable(sorted, func(i, j int) bool {
+		return less(sorted[i], sorted[j])
+	})
+
+	return sorted
+}
+
 // Shuffle returns shuffled slice by your rand.Source
 func (ss carPointers) Shuffle(source rand.Source) carPointers {
 	n := len(ss)
diff --git a/pie/cars_pie.go b/pie/cars_pie.go
index e783b1a..c4c8ff8 100755
--- a/pie/cars_pie.go
+++ b/pie/cars_pie.go
@@ -5,6 +5,7 @@ import (
 	"encoding/json"
 	"github.com/elliotchance/pie/pie/util"
 	"math/rand"
+	"sort"
 )
 
 // All will return true if all callbacks return true. It follows the same logic
@@ -259,6 +260,42 @@ func (ss cars) Send(ctx context.Context, ch chan<- car) cars {
 	return ss
 }
 
+// SortUsing works similar to sort.Slice. However, unlike sort.Slice the
+// slice returned will be reallocated as to not modify the input slice.
+func (ss cars) SortUsing(less func(a, b car) bool) cars {
+	// Avoid the allocation. If there is one element or less it is already
+	// sorted.
+	if len(ss) < 2 {
+		return ss
+	}
+
+	sorted := make(cars, len(ss))
+	copy(sorted, ss)
+	sort.Slice(sorted, func(i, j int) bool {
+		return less(sorted[i], sorted[j])
+	})
+
+	return sorted
+}
+
+// SortStableUsing works similar to sort.SliceStable. However, unlike sort.SliceStable the
+// slice returned will be reallocated as to not modify the input slice.
+func (ss cars) SortStableUsing(less func(a, b car) bool) cars {
+	// Avoid the allocation. If there is one element or less it is already
+	// sorted.
+	if len(ss) < 2 {
+		return ss
+	}
+
+	sorted := make(cars, len(ss))
+	copy(sorted, ss)
+	sort.SliceStable(sorted, func(i, j int) bool {
+		return less(sorted[i], sorted[j])
+	})
+
+	return sorted
+}
+
 // Shuffle returns shuffled slice by your rand.Source
 func (ss cars) Shuffle(source rand.Source) cars {
 	n := len(ss)
diff --git a/pie/float64s_pie.go b/pie/float64s_pie.go
index cb286e9..8bba2c2 100755
--- a/pie/float64s_pie.go
+++ b/pie/float64s_pie.go
@@ -359,6 +359,22 @@ func (ss Float64s) Random(source rand.Source) float64 {
 	return ss[i]
 }
 
+// Reduce continually applies the provided function
+// over the slice. Reducing the elements to a single value.
+//
+// Returns a zero value of float64 if there are no elements in the slice. It will panic if the reducer is nil and the slice has more than one element (required to invoke reduce).
+// Otherwise returns result of applying reducer from left to right.
+func (ss Float64s) Reduce(reducer func(float64, float64) float64) (el float64) {
+	if len(ss) == 0 {
+		return
+	}
+	el = ss[0]
+	for _, s := range ss[1:] {
+		el = reducer(el, s)
+	}
+	return
+}
+
 // Reverse returns a new copy of the slice with the elements ordered in reverse.
 // This is useful when combined with Sort to get a descending sort order:
 //
@@ -409,7 +425,7 @@ func (ss Float64s) Sort() Float64s {
 		return ss
 	}
 
-	sorted := make([]float64, len(ss))
+	sorted := make(Float64s, len(ss))
 	copy(sorted, ss)
 	sort.Slice(sorted, func(i, j int) bool {
 		return sorted[i] < sorted[j]
diff --git a/pie/ints_pie.go b/pie/ints_pie.go
index e8fa45d..9fba99d 100755
--- a/pie/ints_pie.go
+++ b/pie/ints_pie.go
@@ -359,6 +359,22 @@ func (ss Ints) Random(source rand.Source) int {
 	return ss[i]
 }
 
+// Reduce continually applies the provided function
+// over the slice. Reducing the elements to a single value.
+//
+// Returns a zero value of int if there are no elements in the slice. It will panic if the reducer is nil and the slice has more than one element (required to invoke reduce).
+// Otherwise returns result of applying reducer from left to right.
+func (ss Ints) Reduce(reducer func(int, int) int) (el int) {
+	if len(ss) == 0 {
+		return
+	}
+	el = ss[0]
+	for _, s := range ss[1:] {
+		el = reducer(el, s)
+	}
+	return
+}
+
 // Reverse returns a new copy of the slice with the elements ordered in reverse.
 // This is useful when combined with Sort to get a descending sort order:
 //
@@ -409,7 +425,7 @@ func (ss Ints) Sort() Ints {
 		return ss
 	}
 
-	sorted := make([]int, len(ss))
+	sorted := make(Ints, len(ss))
 	copy(sorted, ss)
 	sort.Slice(sorted, func(i, j int) bool {
 		return sorted[i] < sorted[j]
diff --git a/pie/strings_pie.go b/pie/strings_pie.go
index 0dd7452..36fe179 100755
--- a/pie/strings_pie.go
+++ b/pie/strings_pie.go
@@ -315,6 +315,22 @@ func (ss Strings) Random(source rand.Source) string {
 	return ss[i]
 }
 
+// Reduce continually applies the provided function
+// over the slice. Reducing the elements to a single value.
+//
+// Returns a zero value of string if there are no elements in the slice. It will panic if the reducer is nil and the slice has more than one element (required to invoke reduce).
+// Otherwise returns result of applying reducer from left to right.
+func (ss Strings) Reduce(reducer func(string, string) string) (el string) {
+	if len(ss) == 0 {
+		return
+	}
+	el = ss[0]
+	for _, s := range ss[1:] {
+		el = reducer(el, s)
+	}
+	return
+}
+
 // Reverse returns a new copy of the slice with the elements ordered in reverse.
 // This is useful when combined with Sort to get a descending sort order:
 //
@@ -365,7 +381,7 @@ func (ss Strings) Sort() Strings {
 		return ss
 	}
 
-	sorted := make([]string, len(ss))
+	sorted := make(Strings, len(ss))
 	copy(sorted, ss)
 	sort.Slice(sorted, func(i, j int) bool {
 		return sorted[i] < sorted[j]
@@ -374,6 +390,42 @@ func (ss Strings) Sort() Strings {
 	return sorted
 }
 
+// SortUsing works similar to sort.Slice. However, unlike sort.Slice the
+// slice returned will be reallocated as to not modify the input slice.
+func (ss Strings) SortUsing(less func(a, b string) bool) Strings {
+	// Avoid the allocation. If there is one element or less it is already
+	// sorted.
+	if len(ss) < 2 {
+		return ss
+	}
+
+	sorted := make(Strings, len(ss))
+	copy(sorted, ss)
+	sort.Slice(sorted, func(i, j int) bool {
+		return less(sorted[i], sorted[j])
+	})
+
+	return sorted
+}
+
+// SortStableUsing works similar to sort.SliceStable. However, unlike sort.SliceStable the
+// slice returned will be reallocated as to not modify the input slice.
+func (ss Strings) SortStableUsing(less func(a, b string) bool) Strings {
+	// Avoid the allocation. If there is one element or less it is already
+	// sorted.
+	if len(ss) < 2 {
+		return ss
+	}
+
+	sorted := make(Strings, len(ss))
+	copy(sorted, ss)
+	sort.SliceStable(sorted, func(i, j int) bool {
+		return less(sorted[i], sorted[j])
+	})
+
+	return sorted
+}
+
 // Shuffle returns shuffled slice by your rand.Source
 func (ss Strings) Shuffle(source rand.Source) Strings {
 	n := len(ss)
diff --git a/template.go b/template.go
index b282e93..e2d69d1 100644
--- a/template.go
+++ b/template.go
@@ -456,6 +456,24 @@ func (ss SliceType) Random(source rand.Source) ElementType {
 	i := rnd.Intn(n)
 	return ss[i]
 }
+`,
+	"Reduce": `package functions
+
+// Reduce continually applies the provided function
+// over the slice. Reducing the elements to a single value.
+//
+// Returns a zero value of ElementType if there are no elements in the slice. It will panic if the reducer is nil and the slice has more than one element (required to invoke reduce).
+// Otherwise returns result of applying reducer from left to right.
+func (ss SliceType) Reduce(reducer func(ElementType, ElementType) ElementType) (el ElementType) {
+	if len(ss) == 0 {
+		return
+	}
+	el = ss[0]
+	for _, s := range ss[1:] {
+		el = reducer(el, s)
+	}
+	return
+}
 `,
 	"Reverse": `package functions
 
@@ -552,7 +570,7 @@ func (ss SliceType) Sort() SliceType {
 		return ss
 	}
 
-	sorted := make([]ElementType, len(ss))
+	sorted := make(SliceType, len(ss))
 	copy(sorted, ss)
 	sort.Slice(sorted, func(i, j int) bool {
 		return sorted[i] < sorted[j]
@@ -560,6 +578,54 @@ func (ss SliceType) Sort() SliceType {
 
 	return sorted
 }
+`,
+	"SortStableUsing": `package functions
+
+import (
+	"sort"
+)
+
+// SortStableUsing works similar to sort.SliceStable. However, unlike sort.SliceStable the
+// slice returned will be reallocated as to not modify the input slice.
+func (ss SliceType) SortStableUsing(less func(a, b ElementType) bool) SliceType {
+	// Avoid the allocation. If there is one element or less it is already
+	// sorted.
+	if len(ss) < 2 {
+		return ss
+	}
+
+	sorted := make(SliceType, len(ss))
+	copy(sorted, ss)
+	sort.SliceStable(sorted, func(i, j int) bool {
+		return less(sorted[i], sorted[j])
+	})
+
+	return sorted
+}
+`,
+	"SortUsing": `package functions
+
+import (
+	"sort"
+)
+
+// SortUsing works similar to sort.Slice. However, unlike sort.Slice the
+// slice returned will be reallocated as to not modify the input slice.
+func (ss SliceType) SortUsing(less func(a, b ElementType) bool) SliceType {
+	// Avoid the allocation. If there is one element or less it is already
+	// sorted.
+	if len(ss) < 2 {
+		return ss
+	}
+
+	sorted := make(SliceType, len(ss))
+	copy(sorted, ss)
+	sort.Slice(sorted, func(i, j int) bool {
+		return less(sorted[i], sorted[j])
+	})
+
+	return sorted
+}
 `,
 	"Sum": `package functions
 


Test patch:
diff --git a/pie/carpointers_test.go b/pie/carpointers_test.go
index c5ce348..7adbf17 100644
--- a/pie/carpointers_test.go
+++ b/pie/carpointers_test.go
@@ -326,6 +326,78 @@ var stringsToStringsTests = []struct {
 	},
 }
 
+var carPointersSortCustomTests = []struct {
+	ss                  carPointers
+	sortedStableByName  carPointers
+	sortedStableByColor carPointers
+}{
+	{
+		nil,
+		nil,
+		nil,
+	},
+	{
+		carPointers{},
+		carPointers{},
+		carPointers{},
+	},
+	{
+		carPointers{&car{"foo", "red"}},
+		carPointers{&car{"foo", "red"}},
+		carPointers{&car{"foo", "red"}},
+	},
+	{
+		carPointers{&car{"bar", "yellow"}, &car{"Baz", "black"}, &car{"foo", "red"}},
+		carPointers{&car{"Baz", "black"}, &car{"bar", "yellow"}, &car{"foo", "red"}},
+		carPointers{&car{"Baz", "black"}, &car{"foo", "red"}, &car{"bar", "yellow"}},
+	},
+	{
+		carPointers{&car{"bar", "yellow"}, &car{"Baz", "black"}, &car{"qux", "cyan"}, &car{"foo", "red"}},
+		carPointers{&car{"Baz", "black"}, &car{"bar", "yellow"}, &car{"foo", "red"}, &car{"qux", "cyan"}},
+		carPointers{&car{"Baz", "black"}, &car{"qux", "cyan"}, &car{"foo", "red"}, &car{"bar", "yellow"}},
+	},
+	{
+		carPointers{&car{"aaa", "yellow"}, &car{"aaa", "black"}, &car{"bbb", "yellow"}, &car{"bbb", "black"}},
+		carPointers{&car{"aaa", "yellow"}, &car{"aaa", "black"}, &car{"bbb", "yellow"}, &car{"bbb", "black"}},
+		carPointers{&car{"aaa", "black"}, &car{"bbb", "black"}, &car{"aaa", "yellow"}, &car{"bbb", "yellow"}},
+	},
+}
+
+func carPointerNameLess(a, b *car) bool {
+	return a.Name < b.Name
+}
+
+func carPointerColorLess(a, b *car) bool {
+	return a.Color < b.Color
+}
+
+func TestCarPointers_SortUsing(t *testing.T) {
+	isSortedUsing := func(ss carPointers, less func(a, b *car) bool) bool {
+		for i := 1; i < len(ss); i++ {
+			if less(ss[i], ss[i-1]) {
+				return false
+			}
+		}
+		return true
+	}
+
+	for _, test := range carPointersSortCustomTests {
+		t.Run("", func(t *testing.T) {
+			defer assertImmutableCarPointers(t, &test.ss)()
+
+			sortedByName := test.ss.SortUsing(carPointerNameLess)
+			assert.True(t, isSortedUsing(sortedByName, carPointerNameLess))
+			sortedStableByName := test.ss.SortStableUsing(carPointerNameLess)
+			assert.Equal(t, test.sortedStableByName, sortedStableByName)
+
+			sortedByColor := test.ss.SortUsing(carPointerColorLess)
+			assert.True(t, isSortedUsing(sortedByColor, carPointerColorLess))
+			sortedStableByColor := test.ss.SortStableUsing(carPointerColorLess)
+			assert.Equal(t, test.sortedStableByColor, sortedStableByColor)
+		})
+	}
+}
+
 func TestCarPointers_ToStrings(t *testing.T) {
 	for _, test := range stringsToStringsTests {
 		t.Run("", func(t *testing.T) {
diff --git a/pie/cars_test.go b/pie/cars_test.go
index 7706f94..674d81e 100644
--- a/pie/cars_test.go
+++ b/pie/cars_test.go
@@ -288,6 +288,78 @@ func TestCars_Reverse(t *testing.T) {
 	}
 }
 
+var carsSortCustomTests = []struct {
+	ss                  cars
+	sortedStableByName  cars
+	sortedStableByColor cars
+}{
+	{
+		nil,
+		nil,
+		nil,
+	},
+	{
+		cars{},
+		cars{},
+		cars{},
+	},
+	{
+		cars{car{"foo", "red"}},
+		cars{car{"foo", "red"}},
+		cars{car{"foo", "red"}},
+	},
+	{
+		cars{car{"bar", "yellow"}, car{"Baz", "black"}, car{"foo", "red"}},
+		cars{car{"Baz", "black"}, car{"bar", "yellow"}, car{"foo", "red"}},
+		cars{car{"Baz", "black"}, car{"foo", "red"}, car{"bar", "yellow"}},
+	},
+	{
+		cars{car{"bar", "yellow"}, car{"Baz", "black"}, car{"qux", "cyan"}, car{"foo", "red"}},
+		cars{car{"Baz", "black"}, car{"bar", "yellow"}, car{"foo", "red"}, car{"qux", "cyan"}},
+		cars{car{"Baz", "black"}, car{"qux", "cyan"}, car{"foo", "red"}, car{"bar", "yellow"}},
+	},
+	{
+		cars{car{"aaa", "yellow"}, car{"aaa", "black"}, car{"bbb", "yellow"}, car{"bbb", "black"}},
+		cars{car{"aaa", "yellow"}, car{"aaa", "black"}, car{"bbb", "yellow"}, car{"bbb", "black"}},
+		cars{car{"aaa", "black"}, car{"bbb", "black"}, car{"aaa", "yellow"}, car{"bbb", "yellow"}},
+	},
+}
+
+func carNameLess(a, b car) bool {
+	return a.Name < b.Name
+}
+
+func carColorLess(a, b car) bool {
+	return a.Color < b.Color
+}
+
+func TestCars_SortUsing(t *testing.T) {
+	isSortedUsing := func(ss cars, less func(a, b car) bool) bool {
+		for i := 1; i < len(ss); i++ {
+			if less(ss[i], ss[i-1]) {
+				return false
+			}
+		}
+		return true
+	}
+
+	for _, test := range carsSortCustomTests {
+		t.Run("", func(t *testing.T) {
+			defer assertImmutableCars(t, &test.ss)()
+
+			sortedByName := test.ss.SortUsing(carNameLess)
+			assert.True(t, isSortedUsing(sortedByName, carNameLess))
+			sortedStableByName := test.ss.SortStableUsing(carNameLess)
+			assert.Equal(t, test.sortedStableByName, sortedStableByName)
+
+			sortedByColor := test.ss.SortUsing(carColorLess)
+			assert.True(t, isSortedUsing(sortedByColor, carColorLess))
+			sortedStableByColor := test.ss.SortStableUsing(carColorLess)
+			assert.Equal(t, test.sortedStableByColor, sortedStableByColor)
+		})
+	}
+}
+
 var carsToStringsTests = []struct {
 	ss        cars
 	transform func(car) string
diff --git a/pie/float64s_test.go b/pie/float64s_test.go
index bcb0902..08613a6 100644
--- a/pie/float64s_test.go
+++ b/pie/float64s_test.go
@@ -746,6 +746,41 @@ func TestFloat64s_Random(t *testing.T) {
 	}
 }
 
+var float64sReduceTests = []struct {
+	ss       Float64s
+	expected float64
+	reducer  func(a, b float64) float64
+}{
+	{
+		Float64s{1, 2, 3},
+		6,
+		func(a, b float64) float64 { return a + b },
+	},
+	{
+		Float64s{1, 2, 3},
+		-4,
+		func(a, b float64) float64 { return a - b },
+	},
+	{
+		Float64s{},
+		0,
+		func(a, b float64) float64 { return a - b },
+	},
+	{
+		Float64s{1},
+		1,
+		func(a, b float64) float64 { return a - b },
+	},
+}
+
+func TestFloat64s_Reduce(t *testing.T) {
+	for _, test := range float64sReduceTests {
+		t.Run("", func(t *testing.T) {
+			assert.Equal(t, test.expected, test.ss.Reduce(test.reducer))
+		})
+	}
+}
+
 var float64sAbsTests = []struct {
 	ss  Float64s
 	abs Float64s
diff --git a/pie/ints_test.go b/pie/ints_test.go
index 686913b..dcc1874 100644
--- a/pie/ints_test.go
+++ b/pie/ints_test.go
@@ -731,6 +731,41 @@ func TestInts_Random(t *testing.T) {
 	}
 }
 
+var intsReduceTests = []struct {
+	ss       Ints
+	expected int
+	reducer  func(a, b int) int
+}{
+	{
+		Ints{1, 2, 3},
+		6,
+		func(a, b int) int { return a + b },
+	},
+	{
+		Ints{1, 2, 3},
+		-4,
+		func(a, b int) int { return a - b },
+	},
+	{
+		Ints{},
+		0,
+		func(a, b int) int { return a - b },
+	},
+	{
+		Ints{1},
+		1,
+		func(a, b int) int { return a - b },
+	},
+}
+
+func TestInts_Reduce(t *testing.T) {
+	for _, test := range intsReduceTests {
+		t.Run("", func(t *testing.T) {
+			assert.Equal(t, test.expected, test.ss.Reduce(test.reducer))
+		})
+	}
+}
+
 func TestInts_Abs(t *testing.T) {
 	assert.Equal(t, Ints{1, 5, 7}, Ints{-1, 5, -7}.Abs())
 	assert.Equal(t, Ints{689845, 688969, 220373, 89437, 308836}, Ints{-689845, -688969, -220373, -89437, 308836}.Abs())
diff --git a/pie/strings_test.go b/pie/strings_test.go
index 5fef94a..375559e 100644
--- a/pie/strings_test.go
+++ b/pie/strings_test.go
@@ -320,6 +320,65 @@ func TestStrings_AreSorted(t *testing.T) {
 	}
 }
 
+func stringShorter(a, b string) bool {
+	return len(a) < len(b)
+}
+
+var stringsSortByLengthTests = []struct {
+	ss           Strings
+	sortedStable Strings
+}{
+	{
+		nil,
+		nil,
+	},
+	{
+		Strings{},
+		Strings{},
+	},
+	{
+		Strings{"foo"},
+		Strings{"foo"},
+	},
+	{
+		Strings{"aaa", "b", "cc"},
+		Strings{"b", "cc", "aaa"},
+	},
+	{
+		Strings{"zz", "aaa", "b", "cc"},
+		Strings{"b", "zz", "cc", "aaa"},
+	},
+}
+
+func TestStrings_SortUsing(t *testing.T) {
+	isSortedByLength := func(ss Strings) bool {
+		for i := 1; i < len(ss); i++ {
+			if stringShorter(ss[i], ss[i-1]) {
+				return false
+			}
+		}
+		return true
+	}
+	less := stringShorter
+	for _, test := range stringsSortByLengthTests {
+		t.Run("", func(t *testing.T) {
+			defer assertImmutableStrings(t, &test.ss)()
+			sortedCustom := test.ss.SortUsing(less)
+			assert.True(t, isSortedByLength(sortedCustom))
+		})
+	}
+}
+
+func TestStrings_SortStableUsing(t *testing.T) {
+	less := stringShorter
+	for _, test := range stringsSortByLengthTests {
+		t.Run("", func(t *testing.T) {
+			defer assertImmutableStrings(t, &test.ss)()
+			assert.Equal(t, test.sortedStable, test.ss.SortStableUsing(less))
+		})
+	}
+}
+
 var stringsUniqueTests = []struct {
 	ss        Strings
 	unique    Strings
@@ -705,6 +764,36 @@ func TestStrings_Random(t *testing.T) {
 	}
 }
 
+var stringsReduceTests = []struct {
+	ss       Strings
+	expected string
+	reducer  func(a, b string) string
+}{
+	{
+		Strings{"Hello", " ", "World"},
+		"Hello World",
+		func(a, b string) string { return a + b },
+	},
+	{
+		Strings{},
+		"",
+		func(a, b string) string { return a + b },
+	},
+	{
+		Strings{"Hello"},
+		"Hello",
+		func(a, b string) string { return a + b },
+	},
+}
+
+func TestStrings_Reduce(t *testing.T) {
+	for _, test := range stringsReduceTests {
+		t.Run("", func(t *testing.T) {
+			assert.Equal(t, test.expected, test.ss.Reduce(test.reducer))
+		})
+	}
+}
+
 var stringsSendTests = []struct {
 	ss            Strings
 	recieveDelay  time.Duration