From b5923248c8f070ab8ef8538738eb63c3e32e530c Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Fri, 11 Apr 2025 12:15:47 -0700 Subject: [PATCH 1/2] add FirstMatchIndex --- slice.go | 10 +++++ slice_test.go | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/slice.go b/slice.go index 2910ac1..a2a8026 100644 --- a/slice.go +++ b/slice.go @@ -56,6 +56,16 @@ func CountMatchingElements[T any](s []T, filter func(T) bool) int { return c } +// FirstMatchIndex returns -1 if there are no matches +func FirstMatchIndex[T any](s []T, filter func(T) bool) int { + for i, e := range s { + if filter(e) { + return i + } + } + return -1 +} + // CombineSlices may return the first slice if it is the only slice with elements. A copy // is only made if it has to be made. func CombineSlices[T any](first []T, more ...[]T) []T { diff --git a/slice_test.go b/slice_test.go index 3a1840a..a269823 100644 --- a/slice_test.go +++ b/slice_test.go @@ -571,3 +571,103 @@ func TestIntersectSlices(t *testing.T) { assert.Equal(t, expected, result) }) } + +func TestFirstMatchIndex(t *testing.T) { + t.Parallel() + t.Run("int slice with matches", func(t *testing.T) { + numbers := []int{1, 3, 5, 7, 9} + + // Test finding the first even number (should be -1 since none exist) + t.Log("Looking for first even number in", numbers) + index := generic.FirstMatchIndex(numbers, func(n int) bool { + return n%2 == 0 + }) + assert.Equal(t, -1, index, "Should return -1 when no matches found") + + // Test finding the first number greater than 4 + t.Log("Looking for first number > 4 in", numbers) + index = generic.FirstMatchIndex(numbers, func(n int) bool { + return n > 4 + }) + assert.Equal(t, 2, index, "Should return index 2 for first number > 4 (value 5)") + }) + + t.Run("string slice with matches", func(t *testing.T) { + words := []string{"apple", "banana", "cherry", "date", "elderberry"} + + // Test finding the first word starting with 'c' + t.Log("Looking for first word starting with 'c' in", words) + index := generic.FirstMatchIndex(words, func(s string) bool { + return len(s) > 0 && s[0] == 'c' + }) + assert.Equal(t, 2, index, "Should return index 2 for first word starting with 'c' (cherry)") + + // Test finding the first word with length > 6 + t.Log("Looking for first word with length > 6 in", words) + index = generic.FirstMatchIndex(words, func(s string) bool { + return len(s) > 6 + }) + assert.Equal(t, 4, index, "Should return index 4 for first word with length > 6 (elderberry)") + }) + + t.Run("empty slice", func(t *testing.T) { + emptySlice := []int{} + + t.Log("Testing with empty slice") + index := generic.FirstMatchIndex(emptySlice, func(n int) bool { + return n > 0 + }) + assert.Equal(t, -1, index, "Should return -1 for empty slice") + }) + + t.Run("no matches", func(t *testing.T) { + numbers := []int{2, 4, 6, 8, 10} + + t.Log("Looking for number > 100 in", numbers) + index := generic.FirstMatchIndex(numbers, func(n int) bool { + return n > 100 + }) + assert.Equal(t, -1, index, "Should return -1 when no matches found") + }) + + t.Run("match at first element", func(t *testing.T) { + numbers := []int{5, 4, 3, 2, 1} + + t.Log("Looking for first number > 3 in", numbers) + index := generic.FirstMatchIndex(numbers, func(n int) bool { + return n > 3 + }) + assert.Equal(t, 0, index, "Should return index 0 for first number > 3 (value 5)") + }) + + t.Run("match at last element", func(t *testing.T) { + numbers := []int{1, 2, 3, 4, 5} + + t.Log("Looking for number 5 in", numbers) + index := generic.FirstMatchIndex(numbers, func(n int) bool { + return n == 5 + }) + assert.Equal(t, 4, index, "Should return index 4 for value 5") + }) + + t.Run("custom struct type", func(t *testing.T) { + type Person struct { + Name string + Age int + } + + people := []Person{ + {Name: "Alice", Age: 25}, + {Name: "Bob", Age: 30}, + {Name: "Charlie", Age: 35}, + {Name: "David", Age: 40}, + } + + // Find first person older than 30 + t.Log("Looking for first person older than 30") + index := generic.FirstMatchIndex(people, func(p Person) bool { + return p.Age > 30 + }) + assert.Equal(t, 2, index, "Should return index 2 for first person older than 30 (Charlie)") + }) +} From d59b265269754eb5b262a6386754363bdfe09e26 Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Fri, 11 Apr 2025 12:24:23 -0700 Subject: [PATCH 2/2] add ReplaceOrAppend --- slice.go | 12 ++++ slice_test.go | 169 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+) diff --git a/slice.go b/slice.go index a2a8026..e05f067 100644 --- a/slice.go +++ b/slice.go @@ -66,6 +66,18 @@ func FirstMatchIndex[T any](s []T, filter func(T) bool) int { return -1 } +// ReplaceFirstMatchOrAppend appends the new element unless there is a matching element +func ReplaceOrAppend[T any](s []T, n T, filter func(T) bool) []T { + i := FirstMatchIndex(s, filter) + c := CopySlice(s) + if i != -1 { + c[i] = n + } else { + c = append(c, n) + } + return c +} + // CombineSlices may return the first slice if it is the only slice with elements. A copy // is only made if it has to be made. func CombineSlices[T any](first []T, more ...[]T) []T { diff --git a/slice_test.go b/slice_test.go index a269823..c5c543f 100644 --- a/slice_test.go +++ b/slice_test.go @@ -671,3 +671,172 @@ func TestFirstMatchIndex(t *testing.T) { assert.Equal(t, 2, index, "Should return index 2 for first person older than 30 (Charlie)") }) } + +func TestReplaceOrAppend(t *testing.T) { + t.Run("replace existing int element", func(t *testing.T) { + t.Parallel() + numbers := []int{1, 3, 5, 7, 9} + + t.Log("Replacing first odd number with 10 in", numbers) + result := generic.ReplaceOrAppend(numbers, 10, func(n int) bool { + return n%2 != 0 // match first odd number + }) + + // Original slice should be unchanged + assert.Equal(t, []int{1, 3, 5, 7, 9}, numbers, "Original slice should remain unchanged") + + // Result should have the first element (index 0) replaced + assert.Equal(t, []int{10, 3, 5, 7, 9}, result, "First odd number should be replaced with 10") + }) + + t.Run("append new int element when no match", func(t *testing.T) { + t.Parallel() + numbers := []int{1, 3, 5, 7, 9} + + t.Log("Appending 10 when no even numbers exist in", numbers) + result := generic.ReplaceOrAppend(numbers, 10, func(n int) bool { + return n%2 == 0 // match first even number (none exists) + }) + + // Original slice should be unchanged + assert.Equal(t, []int{1, 3, 5, 7, 9}, numbers, "Original slice should remain unchanged") + + // Result should have the new element appended + assert.Equal(t, []int{1, 3, 5, 7, 9, 10}, result, "10 should be appended when no match found") + }) + + t.Run("replace string based on prefix", func(t *testing.T) { + t.Parallel() + words := []string{"apple", "banana", "cherry", "date"} + + t.Log("Replacing first word starting with 'c' in", words) + result := generic.ReplaceOrAppend(words, "cantaloupe", func(s string) bool { + return len(s) > 0 && s[0] == 'c' + }) + + // Original slice should be unchanged + assert.Equal(t, []string{"apple", "banana", "cherry", "date"}, words, "Original slice should remain unchanged") + + // Result should have "cherry" replaced with "cantaloupe" + assert.Equal(t, []string{"apple", "banana", "cantaloupe", "date"}, result, "First word starting with 'c' should be replaced") + }) + + t.Run("append string when no match", func(t *testing.T) { + t.Parallel() + words := []string{"apple", "banana", "cherry", "date"} + + t.Log("Appending a word starting with 'e' when none exists in", words) + result := generic.ReplaceOrAppend(words, "elderberry", func(s string) bool { + return len(s) > 0 && s[0] == 'e' + }) + + // Original slice should be unchanged + assert.Equal(t, []string{"apple", "banana", "cherry", "date"}, words, "Original slice should remain unchanged") + + // Result should have the new element appended + assert.Equal(t, []string{"apple", "banana", "cherry", "date", "elderberry"}, result, "Word should be appended when no match found") + }) + + t.Run("empty slice", func(t *testing.T) { + t.Parallel() + emptySlice := []int{} + + t.Log("Testing with empty slice, should append") + result := generic.ReplaceOrAppend(emptySlice, 42, func(n int) bool { + return n > 0 + }) + + // Original slice should be unchanged + assert.Equal(t, []int{}, emptySlice, "Original empty slice should remain unchanged") + + // Result should be a new slice with just the new element + assert.Equal(t, []int{42}, result, "New element should be appended to empty slice") + }) + + t.Run("replace element in last position", func(t *testing.T) { + t.Parallel() + numbers := []int{2, 4, 6, 8, 10} + + t.Log("Replacing last element in", numbers) + result := generic.ReplaceOrAppend(numbers, 20, func(n int) bool { + return n == 10 + }) + + // Original slice should be unchanged + assert.Equal(t, []int{2, 4, 6, 8, 10}, numbers, "Original slice should remain unchanged") + + // Result should have the last element replaced + assert.Equal(t, []int{2, 4, 6, 8, 20}, result, "Last element should be replaced") + }) + + t.Run("custom struct type", func(t *testing.T) { + t.Parallel() + type Person struct { + Name string + Age int + } + + people := []Person{ + {Name: "Alice", Age: 25}, + {Name: "Bob", Age: 30}, + {Name: "Charlie", Age: 35}, + } + + newPerson := Person{Name: "David", Age: 40} + + t.Log("Replacing first person with age > 30") + result := generic.ReplaceOrAppend(people, newPerson, func(p Person) bool { + return p.Age > 30 + }) + + // Original slice should be unchanged + assert.Equal(t, []Person{ + {Name: "Alice", Age: 25}, + {Name: "Bob", Age: 30}, + {Name: "Charlie", Age: 35}, + }, people, "Original slice should remain unchanged") + + // Charlie should be replaced with David + assert.Equal(t, []Person{ + {Name: "Alice", Age: 25}, + {Name: "Bob", Age: 30}, + {Name: "David", Age: 40}, + }, result, "First person with age > 30 should be replaced") + }) + + t.Run("append custom struct", func(t *testing.T) { + t.Parallel() + type Person struct { + Name string + Age int + } + + people := []Person{ + {Name: "Alice", Age: 25}, + {Name: "Bob", Age: 30}, + {Name: "Charlie", Age: 35}, + } + + newPerson := Person{Name: "David", Age: 40} + + t.Log("Appending new person when no match found") + result := generic.ReplaceOrAppend(people, newPerson, func(p Person) bool { + return p.Age > 50 // No match + }) + + // Original slice should be unchanged + assert.Equal(t, []Person{ + {Name: "Alice", Age: 25}, + {Name: "Bob", Age: 30}, + {Name: "Charlie", Age: 35}, + }, people, "Original slice should remain unchanged") + + // New person should be appended + assert.Equal(t, []Person{ + {Name: "Alice", Age: 25}, + {Name: "Bob", Age: 30}, + {Name: "Charlie", Age: 35}, + {Name: "David", Age: 40}, + }, result, "New person should be appended when no match found") + }) +}