mirror of
https://github.com/IBM/fp-go.git
synced 2025-11-23 22:14:53 +02:00
406 lines
12 KiB
Go
406 lines
12 KiB
Go
// Copyright (c) 2023 - 2025 IBM Corp.
|
|
// All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package array
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// TestSliceBasicCases tests normal slicing operations
|
|
func TestSliceBasicCases(t *testing.T) {
|
|
data := []int{0, 1, 2, 3, 4, 5}
|
|
|
|
t.Run("normal slice from middle", func(t *testing.T) {
|
|
assert.Equal(t, []int{1, 2, 3}, Slice[int](1, 4)(data))
|
|
})
|
|
|
|
t.Run("slice from start", func(t *testing.T) {
|
|
assert.Equal(t, []int{0, 1, 2}, Slice[int](0, 3)(data))
|
|
})
|
|
|
|
t.Run("slice to end", func(t *testing.T) {
|
|
assert.Equal(t, []int{3, 4, 5}, Slice[int](3, 6)(data))
|
|
})
|
|
|
|
t.Run("slice single element", func(t *testing.T) {
|
|
assert.Equal(t, []int{2}, Slice[int](2, 3)(data))
|
|
})
|
|
|
|
t.Run("slice entire array", func(t *testing.T) {
|
|
assert.Equal(t, []int{0, 1, 2, 3, 4, 5}, Slice[int](0, 6)(data))
|
|
})
|
|
}
|
|
|
|
// TestSliceNegativeIndices tests negative index handling (counting from end)
|
|
func TestSliceNegativeIndices(t *testing.T) {
|
|
data := []int{0, 1, 2, 3, 4, 5}
|
|
|
|
t.Run("negative start index", func(t *testing.T) {
|
|
// -2 means length + (-2) = 6 - 2 = 4
|
|
assert.Equal(t, []int{4, 5}, Slice[int](-2, 6)(data))
|
|
})
|
|
|
|
t.Run("negative end index", func(t *testing.T) {
|
|
// -2 means length + (-2) = 6 - 2 = 4
|
|
assert.Equal(t, []int{0, 1, 2, 3}, Slice[int](0, -2)(data))
|
|
})
|
|
|
|
t.Run("both negative indices", func(t *testing.T) {
|
|
// -4 = 2, -2 = 4
|
|
assert.Equal(t, []int{2, 3}, Slice[int](-4, -2)(data))
|
|
})
|
|
|
|
t.Run("negative index beyond array start", func(t *testing.T) {
|
|
// -10 would be -4, clamped to 0
|
|
assert.Equal(t, []int{0, 1, 2}, Slice[int](-10, 3)(data))
|
|
})
|
|
|
|
t.Run("negative end index beyond array start", func(t *testing.T) {
|
|
// -10 would be -4, clamped to 0
|
|
assert.Equal(t, []int{}, Slice[int](0, -10)(data))
|
|
})
|
|
}
|
|
|
|
// TestSliceEmptyArray tests slicing on empty arrays (totality proof)
|
|
func TestSliceEmptyArray(t *testing.T) {
|
|
empty := []int{}
|
|
|
|
t.Run("slice empty array with zero indices", func(t *testing.T) {
|
|
assert.Equal(t, []int{}, Slice[int](0, 0)(empty))
|
|
})
|
|
|
|
t.Run("slice empty array with positive indices", func(t *testing.T) {
|
|
assert.Equal(t, []int{}, Slice[int](0, 5)(empty))
|
|
})
|
|
|
|
t.Run("slice empty array with negative indices", func(t *testing.T) {
|
|
assert.Equal(t, []int{}, Slice[int](-1, -1)(empty))
|
|
})
|
|
|
|
t.Run("slice empty array with mixed indices", func(t *testing.T) {
|
|
assert.Equal(t, []int{}, Slice[int](-5, 5)(empty))
|
|
})
|
|
}
|
|
|
|
// TestSliceOutOfBounds tests out-of-bounds scenarios (totality proof)
|
|
func TestSliceOutOfBounds(t *testing.T) {
|
|
data := []int{0, 1, 2, 3, 4}
|
|
|
|
t.Run("start index beyond array length", func(t *testing.T) {
|
|
assert.Equal(t, []int{}, Slice[int](10, 15)(data))
|
|
})
|
|
|
|
t.Run("end index beyond array length", func(t *testing.T) {
|
|
assert.Equal(t, []int{2, 3, 4}, Slice[int](2, 100)(data))
|
|
})
|
|
|
|
t.Run("both indices beyond array length", func(t *testing.T) {
|
|
assert.Equal(t, []int{}, Slice[int](10, 20)(data))
|
|
})
|
|
|
|
t.Run("start equals array length", func(t *testing.T) {
|
|
assert.Equal(t, []int{}, Slice[int](5, 10)(data))
|
|
})
|
|
|
|
t.Run("end equals array length", func(t *testing.T) {
|
|
assert.Equal(t, []int{3, 4}, Slice[int](3, 5)(data))
|
|
})
|
|
}
|
|
|
|
// TestSliceInvalidRanges tests invalid range scenarios (totality proof)
|
|
func TestSliceInvalidRanges(t *testing.T) {
|
|
data := []int{0, 1, 2, 3, 4}
|
|
|
|
t.Run("start equals end", func(t *testing.T) {
|
|
assert.Equal(t, []int{}, Slice[int](2, 2)(data))
|
|
})
|
|
|
|
t.Run("start greater than end", func(t *testing.T) {
|
|
assert.Equal(t, []int{}, Slice[int](4, 2)(data))
|
|
})
|
|
|
|
t.Run("start greater than end with negative indices", func(t *testing.T) {
|
|
// -1 = 4, -3 = 2
|
|
assert.Equal(t, []int{}, Slice[int](-1, -3)(data))
|
|
})
|
|
|
|
t.Run("zero range at start", func(t *testing.T) {
|
|
assert.Equal(t, []int{}, Slice[int](0, 0)(data))
|
|
})
|
|
|
|
t.Run("zero range at end", func(t *testing.T) {
|
|
assert.Equal(t, []int{}, Slice[int](5, 5)(data))
|
|
})
|
|
}
|
|
|
|
// TestSliceEdgeCases tests additional edge cases (totality proof)
|
|
func TestSliceEdgeCases(t *testing.T) {
|
|
t.Run("single element array - slice all", func(t *testing.T) {
|
|
data := []int{42}
|
|
assert.Equal(t, []int{42}, Slice[int](0, 1)(data))
|
|
})
|
|
|
|
t.Run("single element array - slice none", func(t *testing.T) {
|
|
data := []int{42}
|
|
assert.Equal(t, []int{}, Slice[int](1, 1)(data))
|
|
})
|
|
|
|
t.Run("single element array - negative indices", func(t *testing.T) {
|
|
data := []int{42}
|
|
assert.Equal(t, []int{42}, Slice[int](-1, 1)(data))
|
|
})
|
|
|
|
t.Run("large array slice", func(t *testing.T) {
|
|
data := MakeBy(1000, func(i int) int { return i })
|
|
result := Slice[int](100, 200)(data)
|
|
assert.Equal(t, 100, len(result))
|
|
assert.Equal(t, 100, result[0])
|
|
assert.Equal(t, 199, result[99])
|
|
})
|
|
}
|
|
|
|
// TestSliceWithDifferentTypes tests that Slice works with different types (totality proof)
|
|
func TestSliceWithDifferentTypes(t *testing.T) {
|
|
t.Run("string slice", func(t *testing.T) {
|
|
data := []string{"a", "b", "c", "d", "e"}
|
|
assert.Equal(t, []string{"b", "c", "d"}, Slice[string](1, 4)(data))
|
|
})
|
|
|
|
t.Run("float slice", func(t *testing.T) {
|
|
data := []float64{1.1, 2.2, 3.3, 4.4, 5.5}
|
|
assert.Equal(t, []float64{2.2, 3.3}, Slice[float64](1, 3)(data))
|
|
})
|
|
|
|
t.Run("bool slice", func(t *testing.T) {
|
|
data := []bool{true, false, true, false}
|
|
assert.Equal(t, []bool{false, true}, Slice[bool](1, 3)(data))
|
|
})
|
|
|
|
t.Run("struct slice", func(t *testing.T) {
|
|
type Point struct{ X, Y int }
|
|
data := []Point{{1, 2}, {3, 4}, {5, 6}}
|
|
assert.Equal(t, []Point{{3, 4}}, Slice[Point](1, 2)(data))
|
|
})
|
|
|
|
t.Run("pointer slice", func(t *testing.T) {
|
|
a, b, c := 1, 2, 3
|
|
data := []*int{&a, &b, &c}
|
|
result := Slice[*int](1, 3)(data)
|
|
assert.Equal(t, 2, len(result))
|
|
assert.Equal(t, 2, *result[0])
|
|
assert.Equal(t, 3, *result[1])
|
|
})
|
|
}
|
|
|
|
// TestSliceNilArray tests behavior with nil arrays (totality proof)
|
|
func TestSliceNilArray(t *testing.T) {
|
|
var nilArray []int
|
|
|
|
t.Run("slice nil array with zero indices", func(t *testing.T) {
|
|
assert.Equal(t, []int{}, Slice[int](0, 0)(nilArray))
|
|
})
|
|
|
|
t.Run("slice nil array with positive indices", func(t *testing.T) {
|
|
assert.Equal(t, []int{}, Slice[int](0, 5)(nilArray))
|
|
})
|
|
|
|
t.Run("slice nil array with negative indices", func(t *testing.T) {
|
|
assert.Equal(t, []int{}, Slice[int](-1, 1)(nilArray))
|
|
})
|
|
|
|
t.Run("slice nil array with out of bounds indices", func(t *testing.T) {
|
|
assert.Equal(t, []int{}, Slice[int](10, 20)(nilArray))
|
|
})
|
|
}
|
|
|
|
// TestSliceComposition tests that Slice can be composed with other functions
|
|
func TestSliceComposition(t *testing.T) {
|
|
data := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
|
|
|
t.Run("compose multiple slices", func(t *testing.T) {
|
|
// First slice [2:8], then slice [1:4] of result
|
|
slice1 := Slice[int](2, 8)
|
|
slice2 := Slice[int](1, 4)
|
|
result := slice2(slice1(data))
|
|
// [2,3,4,5,6,7] -> [3,4,5]
|
|
assert.Equal(t, []int{3, 4, 5}, result)
|
|
})
|
|
|
|
t.Run("slice then map", func(t *testing.T) {
|
|
sliced := Slice[int](2, 5)(data)
|
|
mapped := Map(func(x int) int { return x * 2 })(sliced)
|
|
assert.Equal(t, []int{4, 6, 8}, mapped)
|
|
})
|
|
|
|
t.Run("slice then filter", func(t *testing.T) {
|
|
sliced := Slice[int](0, 6)(data)
|
|
filtered := Filter(func(x int) bool { return x%2 == 0 })(sliced)
|
|
assert.Equal(t, []int{0, 2, 4}, filtered)
|
|
})
|
|
}
|
|
|
|
// TestSliceImmutability tests that Slice doesn't modify the original array
|
|
func TestSliceImmutability(t *testing.T) {
|
|
original := []int{0, 1, 2, 3, 4}
|
|
originalCopy := []int{0, 1, 2, 3, 4}
|
|
|
|
t.Run("slicing doesn't modify original", func(t *testing.T) {
|
|
result := Slice[int](1, 4)(original)
|
|
assert.Equal(t, []int{1, 2, 3}, result)
|
|
assert.Equal(t, originalCopy, original)
|
|
})
|
|
|
|
t.Run("slice shares underlying array with original", func(t *testing.T) {
|
|
// Note: Go's slice operation creates a view of the underlying array,
|
|
// not a deep copy. This is expected behavior and matches Go's built-in slice semantics.
|
|
result := Slice[int](1, 4)(original)
|
|
result[0] = 999
|
|
// The original array is affected because slices share the underlying array
|
|
assert.Equal(t, 999, original[1], "Slices share underlying array (expected Go behavior)")
|
|
})
|
|
}
|
|
|
|
// TestSliceTotality is a comprehensive test proving Slice is a total function
|
|
// A total function is defined for all possible inputs and never panics
|
|
func TestSliceTotality(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
data []int
|
|
low int
|
|
high int
|
|
panic bool // Should always be false for a total function
|
|
}{
|
|
// Normal cases
|
|
{"normal range", []int{1, 2, 3, 4, 5}, 1, 3, false},
|
|
{"full range", []int{1, 2, 3}, 0, 3, false},
|
|
{"empty result", []int{1, 2, 3}, 1, 1, false},
|
|
|
|
// Edge cases with empty/nil arrays
|
|
{"empty array", []int{}, 0, 0, false},
|
|
{"empty array with indices", []int{}, 1, 5, false},
|
|
{"nil array", nil, 0, 5, false},
|
|
|
|
// Negative indices
|
|
{"negative low", []int{1, 2, 3, 4, 5}, -2, 5, false},
|
|
{"negative high", []int{1, 2, 3, 4, 5}, 0, -1, false},
|
|
{"both negative", []int{1, 2, 3, 4, 5}, -3, -1, false},
|
|
{"negative beyond bounds", []int{1, 2, 3}, -100, -50, false},
|
|
|
|
// Out of bounds
|
|
{"low beyond length", []int{1, 2, 3}, 10, 20, false},
|
|
{"high beyond length", []int{1, 2, 3}, 1, 100, false},
|
|
{"both beyond length", []int{1, 2, 3}, 10, 20, false},
|
|
|
|
// Invalid ranges
|
|
{"low equals high", []int{1, 2, 3}, 2, 2, false},
|
|
{"low greater than high", []int{1, 2, 3}, 3, 1, false},
|
|
{"negative invalid range", []int{1, 2, 3, 4, 5}, -1, -3, false},
|
|
|
|
// Extreme values
|
|
{"very large indices", []int{1, 2, 3}, 1000000, 2000000, false},
|
|
{"very negative indices", []int{1, 2, 3}, -1000000, -500000, false},
|
|
{"mixed extreme", []int{1, 2, 3}, -1000000, 1000000, false},
|
|
|
|
// Zero values
|
|
{"zero indices", []int{1, 2, 3}, 0, 0, false},
|
|
{"zero low", []int{1, 2, 3}, 0, 3, false},
|
|
{"zero high", []int{1, 2, 3}, 0, 0, false},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// This test proves totality by ensuring no panic occurs
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
if !tc.panic {
|
|
t.Errorf("Slice panicked unexpectedly: %v", r)
|
|
}
|
|
} else {
|
|
if tc.panic {
|
|
t.Errorf("Slice should have panicked but didn't")
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Execute the function - if it's total, it will never panic
|
|
result := Slice[int](tc.low, tc.high)(tc.data)
|
|
|
|
// Additional verification: result should always be a valid slice
|
|
assert.NotNil(t, result, "Result should never be nil")
|
|
assert.True(t, len(result) >= 0, "Result length should be non-negative")
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSlicePropertyBased tests mathematical properties of Slice
|
|
func TestSlicePropertyBased(t *testing.T) {
|
|
data := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
|
|
|
t.Run("identity: Slice(0, len) returns copy of array", func(t *testing.T) {
|
|
result := Slice[int](0, len(data))(data)
|
|
assert.Equal(t, data, result)
|
|
})
|
|
|
|
t.Run("empty: Slice(i, i) always returns empty", func(t *testing.T) {
|
|
for i := 0; i <= len(data); i++ {
|
|
result := Slice[int](i, i)(data)
|
|
assert.Equal(t, []int{}, result)
|
|
}
|
|
})
|
|
|
|
t.Run("length property: len(Slice(i, j)) = max(0, min(j, len) - max(i, 0))", func(t *testing.T) {
|
|
testCases := []struct{ low, high, expected int }{
|
|
{0, 5, 5},
|
|
{2, 7, 5},
|
|
{5, 5, 0},
|
|
{3, 2, 0}, // invalid range
|
|
{-2, 10, 2}, // -2 becomes 8, so slice [8:10] has length 2
|
|
{0, 100, 10},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
result := Slice[int](tc.low, tc.high)(data)
|
|
assert.Equal(t, tc.expected, len(result),
|
|
"Slice(%d, %d) should have length %d", tc.low, tc.high, tc.expected)
|
|
}
|
|
})
|
|
|
|
t.Run("concatenation: Slice(0,i) + Slice(i,len) = original", func(t *testing.T) {
|
|
for i := 0; i <= len(data); i++ {
|
|
left := Slice[int](0, i)(data)
|
|
right := Slice[int](i, len(data))(data)
|
|
concatenated := append(left, right...)
|
|
assert.Equal(t, data, concatenated)
|
|
}
|
|
})
|
|
|
|
t.Run("subset property: all elements in slice are in original", func(t *testing.T) {
|
|
result := Slice[int](2, 7)(data)
|
|
for _, elem := range result {
|
|
found := false
|
|
for _, orig := range data {
|
|
if elem == orig {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "Element %d should be in original array", elem)
|
|
}
|
|
})
|
|
}
|