Repository: elliotchance/pie
Pull request #101 (2019-05-05 12:29:11)

Issues/97 append non destructive
See #97

<!-- 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/101)
<!-- Reviewable:end -->


Deleplace: Oops, I must have a discrepancy somewhere, sorry about that. I'll make the necessary changes.
Deleplace: The failing tests say that empty slice and nil are "not equal". This is a great philosophical debate, I'll check if the distinction is meaningful here.
codecov-io: # [Codecov](https://codecov.io/gh/elliotchance/pie/pull/101?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/101/graphs/tree.svg?width=650&token=OvugwUQGM3&height=150&src=pr)](https://codecov.io/gh/elliotchance/pie/pull/101?src=pr&el=tree)

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


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

------

[Continue to review full report at Codecov](https://codecov.io/gh/elliotchance/pie/pull/101?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/101?src=pr&el=footer). Last update [57366d4...d93aefa](https://codecov.io/gh/elliotchance/pie/pull/101?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

Deleplace: > Is this trying to get around the result now being non-nil? It's better to put a check in Append() and avoid the allocation if it's​ not needed.

No, this is intentional. In go there is no meaningful difference between nil slice and empty slice, so it would be a mistake to have rigid tests on a too strict equality check. This is part of why slices are not comparable in the language, even if we desire to compare for testing purpose.

Allocating a slice of size zero doesn't take any memory and afaik is free (no allocation).
Deleplace: > You can move this to `strings_test.go`.

Yes
Deleplace: > Please put a comment that links the issue.

Yes

Patch:
diff --git a/README.md b/README.md
index 3c23e11..025edb6 100644
--- a/README.md
+++ b/README.md
@@ -156,6 +156,7 @@ This will only generate `myInts.Average`, `myInts.Sum` and `myStrings.Filter`.
 | `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. |
+| `Sequence`     | ✓      | ✓      |    |      | n        | Generates sequence of numbers. |
 | `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. |
diff --git a/functions/append.go b/functions/append.go
index 15087dc..642d5bf 100644
--- a/functions/append.go
+++ b/functions/append.go
@@ -1,10 +1,13 @@
 package functions
 
-// Append will return a new slice with the elements appended to the end. It is a
-// wrapper for the internal append(). It is offered as a function so that it can
-// more easily chained.
+// Append will return a new slice with the elements appended to the end.
 //
 // It is acceptable to provide zero arguments.
 func (ss SliceType) Append(elements ...ElementType) SliceType {
-	return append(ss, elements...)
+	// Copy ss, to make sure no memory is overlapping between input and
+	// output. See issue #97.
+	result := append(SliceType{}, ss...)
+
+	result = append(result, elements...)
+	return result
 }
diff --git a/functions/main.go b/functions/main.go
index e4232b3..913fdc1 100644
--- a/functions/main.go
+++ b/functions/main.go
@@ -50,6 +50,7 @@ var Functions = []struct {
 	{"Reduce", "reduce.go", ForNumbersAndStrings},
 	{"Reverse", "reverse.go", ForAll},
 	{"Send", "send.go", ForAll},
+	{"Sequence", "sequence.go", ForNumbers},
 	{"Sort", "sort.go", ForNumbersAndStrings},
 	{"SortUsing", "sort_using.go", ForStrings | ForStructs},
 	{"SortStableUsing", "sort_stable_using.go", ForStrings | ForStructs},
diff --git a/functions/sequence.go b/functions/sequence.go
new file mode 100644
index 0000000..447db12
--- /dev/null
+++ b/functions/sequence.go
@@ -0,0 +1,42 @@
+package functions
+
+import "github.com/elliotchance/pie/pie/util"
+
+// Sequence generates all numbers in range or returns nil if params invalid
+//
+// There are 3 variations to generate:
+// 		1. [0, n).
+//		2. [min, max).
+//		3. [min, max) with step.
+//
+// if len(params) == 1 considered that will be returned slice between 0 and n,
+// where n is the first param, [0, n).
+// if len(params) == 2 considered that will be returned slice between min and max,
+// where min is the first param, max is the second, [min, max).
+// if len(params) > 2 considered that will be returned slice between min and max with step,
+// where min is the first param, max is the second, step is the third one, [min, max) with step,
+// others params will be ignored
+func (ss SliceType) Sequence(params ...int) SliceType {
+	var seq = func(min, max, step int) (seq SliceType) {
+		lenght := int(util.Round(float64(max-min) / float64(step)))
+		if lenght < 1 {
+			return
+		}
+
+		seq = make(SliceType, lenght)
+		for i := 0; i < lenght; min += step {
+			seq[i] = ElementType(min)
+			i++
+		}
+
+		return seq
+	}
+
+	if len(params) > 2 {
+		return seq(params[0], params[1], params[2])
+	} else if len(params) > 1 {
+		return seq(params[0], params[1], 1)
+	} else {
+		return seq(0, params[0], 1)
+	}
+}
diff --git a/pie/carpointers_pie.go b/pie/carpointers_pie.go
index a4ca8bd..d9fbee6 100755
--- a/pie/carpointers_pie.go
+++ b/pie/carpointers_pie.go
@@ -36,13 +36,16 @@ func (ss carPointers) Any(fn func(value *car) bool) bool {
 	return false
 }
 
-// Append will return a new slice with the elements appended to the end. It is a
-// wrapper for the internal append(). It is offered as a function so that it can
-// more easily chained.
+// Append will return a new slice with the elements appended to the end.
 //
 // It is acceptable to provide zero arguments.
 func (ss carPointers) Append(elements ...*car) carPointers {
-	return append(ss, elements...)
+	// Copy ss, to make sure no memory is overlapping between input and
+	// output. See issue #97.
+	result := append(carPointers{}, ss...)
+
+	result = append(result, elements...)
+	return result
 }
 
 // Bottom will return n elements from bottom
diff --git a/pie/cars_pie.go b/pie/cars_pie.go
index 345b6dc..87daa87 100755
--- a/pie/cars_pie.go
+++ b/pie/cars_pie.go
@@ -36,13 +36,16 @@ func (ss cars) Any(fn func(value car) bool) bool {
 	return false
 }
 
-// Append will return a new slice with the elements appended to the end. It is a
-// wrapper for the internal append(). It is offered as a function so that it can
-// more easily chained.
+// Append will return a new slice with the elements appended to the end.
 //
 // It is acceptable to provide zero arguments.
 func (ss cars) Append(elements ...car) cars {
-	return append(ss, elements...)
+	// Copy ss, to make sure no memory is overlapping between input and
+	// output. See issue #97.
+	result := append(cars{}, ss...)
+
+	result = append(result, elements...)
+	return result
 }
 
 // Bottom will return n elements from bottom
diff --git a/pie/float64s_pie.go b/pie/float64s_pie.go
index cf52982..65c70d6 100755
--- a/pie/float64s_pie.go
+++ b/pie/float64s_pie.go
@@ -46,13 +46,16 @@ func (ss Float64s) Any(fn func(value float64) bool) bool {
 	return false
 }
 
-// Append will return a new slice with the elements appended to the end. It is a
-// wrapper for the internal append(). It is offered as a function so that it can
-// more easily chained.
+// Append will return a new slice with the elements appended to the end.
 //
 // It is acceptable to provide zero arguments.
 func (ss Float64s) Append(elements ...float64) Float64s {
-	return append(ss, elements...)
+	// Copy ss, to make sure no memory is overlapping between input and
+	// output. See issue #97.
+	result := append(Float64s{}, ss...)
+
+	result = append(result, elements...)
+	return result
 }
 
 // AreSorted will return true if the slice is already sorted. It is a wrapper
@@ -454,6 +457,45 @@ func (ss Float64s) Send(ctx context.Context, ch chan<- float64) Float64s {
 	return ss
 }
 
+// Sequence generates all numbers in range or returns nil if params invalid
+//
+// There are 3 variations to generate:
+// 		1. [0, n).
+//		2. [min, max).
+//		3. [min, max) with step.
+//
+// if len(params) == 1 considered that will be returned slice between 0 and n,
+// where n is the first param, [0, n).
+// if len(params) == 2 considered that will be returned slice between min and max,
+// where min is the first param, max is the second, [min, max).
+// if len(params) > 2 considered that will be returned slice between min and max with step,
+// where min is the first param, max is the second, step is the third one, [min, max) with step,
+// others params will be ignored
+func (ss Float64s) Sequence(params ...int) Float64s {
+	var seq = func(min, max, step int) (seq Float64s) {
+		lenght := int(util.Round(float64(max-min) / float64(step)))
+		if lenght < 1 {
+			return
+		}
+
+		seq = make(Float64s, lenght)
+		for i := 0; i < lenght; min += step {
+			seq[i] = float64(min)
+			i++
+		}
+
+		return seq
+	}
+
+	if len(params) > 2 {
+		return seq(params[0], params[1], params[2])
+	} else if len(params) > 1 {
+		return seq(params[0], params[1], 1)
+	} else {
+		return seq(0, params[0], 1)
+	}
+}
+
 // Sort works similar to sort.Float64s(). However, unlike sort.Float64s the
 // slice returned will be reallocated as to not modify the input slice.
 //
diff --git a/pie/ints_pie.go b/pie/ints_pie.go
index c3950dc..810387f 100755
--- a/pie/ints_pie.go
+++ b/pie/ints_pie.go
@@ -46,13 +46,16 @@ func (ss Ints) Any(fn func(value int) bool) bool {
 	return false
 }
 
-// Append will return a new slice with the elements appended to the end. It is a
-// wrapper for the internal append(). It is offered as a function so that it can
-// more easily chained.
+// Append will return a new slice with the elements appended to the end.
 //
 // It is acceptable to provide zero arguments.
 func (ss Ints) Append(elements ...int) Ints {
-	return append(ss, elements...)
+	// Copy ss, to make sure no memory is overlapping between input and
+	// output. See issue #97.
+	result := append(Ints{}, ss...)
+
+	result = append(result, elements...)
+	return result
 }
 
 // AreSorted will return true if the slice is already sorted. It is a wrapper
@@ -454,6 +457,45 @@ func (ss Ints) Send(ctx context.Context, ch chan<- int) Ints {
 	return ss
 }
 
+// Sequence generates all numbers in range or returns nil if params invalid
+//
+// There are 3 variations to generate:
+// 		1. [0, n).
+//		2. [min, max).
+//		3. [min, max) with step.
+//
+// if len(params) == 1 considered that will be returned slice between 0 and n,
+// where n is the first param, [0, n).
+// if len(params) == 2 considered that will be returned slice between min and max,
+// where min is the first param, max is the second, [min, max).
+// if len(params) > 2 considered that will be returned slice between min and max with step,
+// where min is the first param, max is the second, step is the third one, [min, max) with step,
+// others params will be ignored
+func (ss Ints) Sequence(params ...int) Ints {
+	var seq = func(min, max, step int) (seq Ints) {
+		lenght := int(util.Round(float64(max-min) / float64(step)))
+		if lenght < 1 {
+			return
+		}
+
+		seq = make(Ints, lenght)
+		for i := 0; i < lenght; min += step {
+			seq[i] = int(min)
+			i++
+		}
+
+		return seq
+	}
+
+	if len(params) > 2 {
+		return seq(params[0], params[1], params[2])
+	} else if len(params) > 1 {
+		return seq(params[0], params[1], 1)
+	} else {
+		return seq(0, params[0], 1)
+	}
+}
+
 // Sort works similar to sort.Ints(). However, unlike sort.Ints the
 // slice returned will be reallocated as to not modify the input slice.
 //
diff --git a/pie/strings_pie.go b/pie/strings_pie.go
index cd32572..eb18d9a 100755
--- a/pie/strings_pie.go
+++ b/pie/strings_pie.go
@@ -36,13 +36,16 @@ func (ss Strings) Any(fn func(value string) bool) bool {
 	return false
 }
 
-// Append will return a new slice with the elements appended to the end. It is a
-// wrapper for the internal append(). It is offered as a function so that it can
-// more easily chained.
+// Append will return a new slice with the elements appended to the end.
 //
 // It is acceptable to provide zero arguments.
 func (ss Strings) Append(elements ...string) Strings {
-	return append(ss, elements...)
+	// Copy ss, to make sure no memory is overlapping between input and
+	// output. See issue #97.
+	result := append(Strings{}, ss...)
+
+	result = append(result, elements...)
+	return result
 }
 
 // AreSorted will return true if the slice is already sorted. It is a wrapper
diff --git a/pie/util/floor.go b/pie/util/floor.go
new file mode 100644
index 0000000..cbffe25
--- /dev/null
+++ b/pie/util/floor.go
@@ -0,0 +1,47 @@
+package util
+
+import "math"
+
+// These constants was copied from src/math/bits.go to support Round in go
+// versions before 1.10.
+const (
+	shift    = 64 - 11 - 1
+	mask     = 0x7FF
+	bias     = 1023
+	signMask = 1 << 63
+	uvone    = 0x3FF0000000000000
+	fracMask = 1<<shift - 1
+)
+
+// Round was copied from src/math/floor.go to support Round in go
+// versions before 1.10.
+func Round(x float64) float64 {
+	// Round is a faster implementation of:
+	//
+	// func Round(x float64) float64 {
+	//   t := Trunc(x)
+	//   if Abs(x-t) >= 0.5 {
+	//     return t + Copysign(1, x)
+	//   }
+	//   return t
+	// }
+	bits := math.Float64bits(x)
+	e := uint(bits>>shift) & mask
+	if e < bias {
+		// Round abs(x) < 1 including denormals.
+		bits &= signMask // +-0
+		if e == bias-1 {
+			bits |= uvone // +-1
+		}
+	} else if e < bias+shift {
+		// Round any abs(x) >= 1 containing a fractional component [0,1).
+		//
+		// Numbers with larger exponents are returned unchanged since they
+		// must be either an integer, infinity, or NaN.
+		const half = 1 << (shift - 1)
+		e -= bias
+		bits += half >> e
+		bits &^= fracMask >> e
+	}
+	return math.Float64frombits(bits)
+}
diff --git a/template.go b/template.go
index c57f2a3..e9d6008 100644
--- a/template.go
+++ b/template.go
@@ -51,13 +51,16 @@ func (ss SliceType) Any(fn func(value ElementType) bool) bool {
 `,
 	"Append": `package functions
 
-// Append will return a new slice with the elements appended to the end. It is a
-// wrapper for the internal append(). It is offered as a function so that it can
-// more easily chained.
+// Append will return a new slice with the elements appended to the end.
 //
 // It is acceptable to provide zero arguments.
 func (ss SliceType) Append(elements ...ElementType) SliceType {
-	return append(ss, elements...)
+	// Copy ss, to make sure no memory is overlapping between input and
+	// output. See issue #97.
+	result := append(SliceType{}, ss...)
+
+	result = append(result, elements...)
+	return result
 }
 `,
 	"AreSorted": `package functions
@@ -563,6 +566,49 @@ func (ss SliceType) Send(ctx context.Context, ch chan<- ElementType) SliceType {
 
 	return ss
 }
+`,
+	"Sequence": `package functions
+
+import "github.com/elliotchance/pie/pie/util"
+
+// Sequence generates all numbers in range or returns nil if params invalid
+//
+// There are 3 variations to generate:
+// 		1. [0, n).
+//		2. [min, max).
+//		3. [min, max) with step.
+//
+// if len(params) == 1 considered that will be returned slice between 0 and n,
+// where n is the first param, [0, n).
+// if len(params) == 2 considered that will be returned slice between min and max,
+// where min is the first param, max is the second, [min, max).
+// if len(params) > 2 considered that will be returned slice between min and max with step,
+// where min is the first param, max is the second, step is the third one, [min, max) with step,
+// others params will be ignored
+func (ss SliceType) Sequence(params ...int) SliceType {
+	var seq = func(min, max, step int) (seq SliceType) {
+		lenght := int(util.Round(float64(max-min) / float64(step)))
+		if lenght < 1 {
+			return
+		}
+
+		seq = make(SliceType, lenght)
+		for i := 0; i < lenght; min += step {
+			seq[i] = ElementType(min)
+			i++
+		}
+
+		return seq
+	}
+
+	if len(params) > 2 {
+		return seq(params[0], params[1], params[2])
+	} else if len(params) > 1 {
+		return seq(params[0], params[1], 1)
+	} else {
+		return seq(0, params[0], 1)
+	}
+}
 `,
 	"Shuffle": `package functions
 


Test patch:
diff --git a/pie/carpointers_test.go b/pie/carpointers_test.go
index 0176cea..577c37c 100644
--- a/pie/carpointers_test.go
+++ b/pie/carpointers_test.go
@@ -412,8 +412,8 @@ func TestCarPointers_ToStrings(t *testing.T) {
 
 func TestCarPointers_Append(t *testing.T) {
 	assert.Equal(t,
-		(carPointers)(nil).Append(),
-		(carPointers)(nil),
+		len((carPointers)(nil).Append()),
+		0,
 	)
 
 	assert.Equal(t,
diff --git a/pie/cars_test.go b/pie/cars_test.go
index 719622e..3c190d5 100644
--- a/pie/cars_test.go
+++ b/pie/cars_test.go
@@ -399,8 +399,8 @@ func TestCars_ToStrings(t *testing.T) {
 
 func TestCars_Append(t *testing.T) {
 	assert.Equal(t,
-		cars{}.Append(),
-		cars{},
+		len(cars{}.Append()),
+		0,
 	)
 
 	assert.Equal(t,
diff --git a/pie/float64s_test.go b/pie/float64s_test.go
index cc727fa..941ba37 100644
--- a/pie/float64s_test.go
+++ b/pie/float64s_test.go
@@ -452,8 +452,8 @@ func TestFloat64s_ToStrings(t *testing.T) {
 
 func TestFloat64s_Append(t *testing.T) {
 	assert.Equal(t,
-		Float64s{}.Append(),
-		Float64s{},
+		len(Float64s{}.Append()),
+		0,
 	)
 
 	assert.Equal(t,
@@ -964,3 +964,102 @@ func TestFloat64s_Diff(t *testing.T) {
 		})
 	}
 }
+
+var float64sSequenceTests = []struct {
+	ss       Float64s
+	params   []int
+	expected Float64s
+}{
+	// n
+	{
+		nil,
+		[]int{-1},
+		nil,
+	},
+	{
+		nil,
+		[]int{0},
+		nil,
+	},
+	{
+		nil,
+		[]int{3},
+		Float64s{0, 1, 2},
+	},
+	{
+		Float64s{},
+		[]int{3},
+		Float64s{0, 1, 2},
+	},
+	// range
+	{
+		nil,
+		[]int{2, 2},
+		nil,
+	},
+	{
+		Float64s{},
+		[]int{3, 2},
+		nil,
+	},
+	{
+		nil,
+		[]int{0, 3},
+		Float64s{0, 1, 2},
+	},
+	{
+		Float64s{},
+		[]int{3, 6},
+		Float64s{3, 4, 5},
+	},
+	{
+		Float64s{},
+		[]int{-5, 0},
+		Float64s{-5, -4, -3, -2, -1},
+	},
+	{
+		Float64s{},
+		[]int{-5, -10},
+		nil,
+	},
+	// range with step
+	{
+		nil,
+		[]int{3, 3, 1},
+		nil,
+	},
+	{
+		Float64s{},
+		[]int{3, 6, 2},
+		Float64s{3, 5},
+	},
+	{
+		Float64s{},
+		[]int{3, 7, 2},
+		Float64s{3, 5},
+	},
+	{
+		Float64s{},
+		[]int{-10, -6, 1},
+		Float64s{-10, -9, -8, -7},
+	},
+	{
+		Float64s{},
+		[]int{-6, -10, -1},
+		Float64s{-6, -7, -8, -9},
+	},
+	{
+		Float64s{},
+		[]int{-6, -10, 1},
+		nil,
+	},
+}
+
+func TestFloat64s_Sequence(t *testing.T) {
+	for _, test := range float64sSequenceTests {
+		t.Run("", func(t *testing.T) {
+			defer assertImmutableFloat64s(t, &test.ss)()
+			assert.Equal(t, test.expected, test.ss.Sequence(test.params...))
+		})
+	}
+}
diff --git a/pie/ints_test.go b/pie/ints_test.go
index d8a2f74..45ef0a3 100644
--- a/pie/ints_test.go
+++ b/pie/ints_test.go
@@ -437,8 +437,8 @@ func TestInts_ToStrings(t *testing.T) {
 
 func TestInts_Append(t *testing.T) {
 	assert.Equal(t,
-		Ints{}.Append(),
-		Ints{},
+		len(Ints{}.Append()),
+		0,
 	)
 
 	assert.Equal(t,
@@ -929,3 +929,102 @@ func TestInts_Diff(t *testing.T) {
 		})
 	}
 }
+
+var intsSequenceTests = []struct {
+	ss       Ints
+	params   []int
+	expected Ints
+}{
+	// n
+	{
+		nil,
+		[]int{-1},
+		nil,
+	},
+	{
+		nil,
+		[]int{0},
+		nil,
+	},
+	{
+		nil,
+		[]int{3},
+		Ints{0, 1, 2},
+	},
+	{
+		Ints{},
+		[]int{3},
+		Ints{0, 1, 2},
+	},
+	// range
+	{
+		nil,
+		[]int{2, 2},
+		nil,
+	},
+	{
+		Ints{},
+		[]int{3, 2},
+		nil,
+	},
+	{
+		nil,
+		[]int{0, 3},
+		Ints{0, 1, 2},
+	},
+	{
+		Ints{},
+		[]int{3, 6},
+		Ints{3, 4, 5},
+	},
+	{
+		Ints{},
+		[]int{-5, 0},
+		Ints{-5, -4, -3, -2, -1},
+	},
+	{
+		Ints{},
+		[]int{-5, -10},
+		nil,
+	},
+	// range with step
+	{
+		nil,
+		[]int{3, 3, 1},
+		nil,
+	},
+	{
+		Ints{},
+		[]int{3, 6, 2},
+		Ints{3, 5},
+	},
+	{
+		Ints{},
+		[]int{3, 7, 2},
+		Ints{3, 5},
+	},
+	{
+		Ints{},
+		[]int{-10, -6, 1},
+		Ints{-10, -9, -8, -7},
+	},
+	{
+		Ints{},
+		[]int{-6, -10, -1},
+		Ints{-6, -7, -8, -9},
+	},
+	{
+		Ints{},
+		[]int{-6, -10, 1},
+		nil,
+	},
+}
+
+func TestInts_Sequence(t *testing.T) {
+	for _, test := range intsSequenceTests {
+		t.Run("", func(t *testing.T) {
+			defer assertImmutableInts(t, &test.ss)()
+			assert.Equal(t, test.expected, test.ss.Sequence(test.params...))
+		})
+	}
+}
diff --git a/pie/strings_test.go b/pie/strings_test.go
index 2da7278..39ebdee 100644
--- a/pie/strings_test.go
+++ b/pie/strings_test.go
@@ -951,3 +951,30 @@ func TestStrings_Diff(t *testing.T) {
 		})
 	}
 }
+
+// Make sure that Append never alters the receiver, or other
+// slices sharing the same memory, unlike the built-in append.
+func TestAppendNonDestructive(t *testing.T) {
+	ab := Strings{"A", "B"}
+	if x, expected := ab.Join(""), "AB"; x != expected {
+		t.Errorf("Expected %q, got %q", expected, x)
+	}
+
+	abc := ab.Append("C")
+	aby := ab.Append("Y")
+	if x, expected := abc.Join(""), "ABC"; x != expected {
+		t.Errorf("Expected %q, got %q", expected, x)
+	}
+	if x, expected := aby.Join(""), "ABY"; x != expected {
+		t.Errorf("Expected %q, got %q", expected, x)
+	}
+
+	abcd := abc.Append("D")
+	abcz := abc.Append("Z")
+	if x, expected := abcd.Join(""), "ABCD"; x != expected {
+		t.Errorf("Expected %q, got %q", expected, x)
+	}
+	if x, expected := abcz.Join(""), "ABCZ"; x != expected {
+		t.Errorf("Expected %q, got %q", expected, x)
+	}
+}