go-sprout/sprout

View on GitHub
registry/slices/functions.go

Summary

Maintainability
A
1 hr
Test Coverage
package slices

import (
    "fmt"
    "reflect"
    "sort"
    "strings"

    "github.com/spf13/cast"

    "github.com/go-sprout/sprout/deprecated"
    "github.com/go-sprout/sprout/internal/helpers"
)

// List creates a list from the provided elements.
//
// Parameters:
//
//    values ...any - the elements to include in the list.
//
// Returns:
//
//    []any - the created list containing the provided elements.
//
// Example:
//
//    {{ 1, 2, 3 | list }} // Output: [1, 2, 3]
func (sr *SlicesRegistry) List(values ...any) []any {
    return values
}

// Append appends an element to a slice or array, returning an error if the
// operation isn't applicable.
//
// Parameters:
//
//    list any - the original list to append to.
//    v any - the element to append.
//
// Returns:
//
//    []any - the new list with the element appended.
//    error - protect against undesired behavior due to migration to new signature.
//
// Example:
//
//    {{ ["a", "b"] | append "c"  }} // Output: ["a", "b", "c"], nil
func (sr *SlicesRegistry) Append(args ...any) ([]any, error) {
    // ! BACKWARDS COMPATIBILITY: deprecated in v1.0 and removed in v1.1
    // ! Due to change in signature, this function still supports the old signature
    // ! to let users transition to the new signature.
    // * Old signature: Append(list any, v any)
    // * New signature: Append(v any, list any)
    if len(args) != 2 {
        return []any{}, deprecated.ErrArgsCount(2, len(args))
    }

    switch reflect.ValueOf(args[0]).Kind() {
    case reflect.Array, reflect.Slice, reflect.Invalid:
        // Old signature
        deprecated.SignatureWarn(sr.handler.Logger(), "append", "{{ append list v }}", "{{ list | append v }}")
        return sr.Append(append(args[1:], args[0])...)
    }

    // New signature
    list := args[1]
    v := args[0]
    // ! END OF BACKWARDS COMPATIBILITY

    if list == nil {
        return nil, fmt.Errorf("cannot append to nil")
    }

    valueOfList := reflect.ValueOf(list)
    tp := valueOfList.Kind()

    switch tp {
    case reflect.Slice:
        // If the list is already a slice, simply append the value
        result := make([]any, valueOfList.Len()+1)
        for i := 0; i < valueOfList.Len(); i++ {
            result[i] = valueOfList.Index(i).Interface()
        }
        result[len(result)-1] = v
        return result, nil

    case reflect.Array:
        // For arrays, we need to convert to a slice first
        length := valueOfList.Len()
        result := make([]any, length+1)
        for i := 0; i < length; i++ {
            result[i] = valueOfList.Index(i).Interface()
        }
        result[length] = v
        return result, nil

    default:
        return nil, fmt.Errorf("cannot append on type %s", tp)
    }
}

// Prepend prepends an element to a slice or array, returning an error if
// the operation isn't applicable.
//
// Parameters:
//
//    list any - the original list to prepend to.
//    v any - the element to prepend.
//
// Returns:
//
//    []any - the new list with the element prepended.
//    error - protect against undesired behavior due to migration to new signature.
//
// Example:
//
//    {{ ["b", "c"] | prepend "a" }} // Output: ["a", "b", "c"], nil
func (sr *SlicesRegistry) Prepend(args ...any) ([]any, error) {
    // ! BACKWARDS COMPATIBILITY: deprecated in v1.0 and removed in v1.1
    // ! Due to change in signature, this function still supports the old signature
    // ! to let users transition to the new signature.
    // * Old signature: Prepend(list any, v any)
    // * New signature: Prepend(v any, list any)
    if len(args) != 2 {
        return []any{}, deprecated.ErrArgsCount(2, len(args))
    }

    switch reflect.ValueOf(args[0]).Kind() {
    case reflect.Array, reflect.Slice, reflect.Invalid:
        // Old signature
        deprecated.SignatureWarn(sr.handler.Logger(), "prepend", "{{ prepend list v }}", "{{ list | prepend v }}")
        return sr.Prepend(append(args[1:], args[0])...)
    }

    // New signature
    list := args[1]
    v := args[0]
    // ! END OF BACKWARDS COMPATIBILITY

    if list == nil {
        return nil, fmt.Errorf("cannot prepend to nil")
    }

    valueOfList := reflect.ValueOf(list)
    tp := valueOfList.Kind()
    switch tp {
    case reflect.Slice, reflect.Array:
        length := valueOfList.Len()
        result := make([]any, length)
        for i := 0; i < length; i++ {
            result[i] = valueOfList.Index(i).Interface()
        }

        return append([]any{v}, result...), nil

    default:
        return nil, fmt.Errorf("cannot prepend on type %s", tp)
    }
}

// Concat merges multiple lists into a single list.
//
// Parameters:
//
//    lists ...any - the lists to concatenate.
//
// Returns:
//
//    any - a single concatenated list containing elements from all provided lists.
//
// Example:
//
//    {{ ["c", "d"] | concat ["a", "b"] }} // Output: ["a", "b", "c", "d"]
func (sr *SlicesRegistry) Concat(lists ...any) any {
    // Estimate the total length to preallocate the result slice
    var totalLen int
    for _, list := range lists {
        if list == nil {
            continue
        }

        tp := reflect.TypeOf(list).Kind()
        if tp == reflect.Slice || tp == reflect.Array {
            totalLen += reflect.ValueOf(list).Len()
        }
    }

    // Preallocate the result slice
    res := make([]any, 0, totalLen)

    for _, list := range lists {
        if list == nil {
            continue
        }

        tp := reflect.TypeOf(list).Kind()
        if tp == reflect.Slice || tp == reflect.Array {
            valueOfList := reflect.ValueOf(list)
            for i := 0; i < valueOfList.Len(); i++ {
                res = append(res, valueOfList.Index(i).Interface())
            }
        }
    }

    return res
}

// Chunk divides a list into chunks of specified size, returning an error
// if the list is nil or not a slice/array.
//
// Parameters:
//
//    size int - the maximum size of each chunk.
//    list any - the list to chunk.
//
// Returns:
//
//    [][]any - a list of chunks.
//    error - error if the list is nil or not a slice/array.
//
// Example:
//
//    {{ ["a", "b", "c", "d"] | chunk 2 }} // Output: [["a", "b"], ["c", "d"]], nil
func (sr *SlicesRegistry) Chunk(size int, list any) ([][]any, error) {
    if list == nil {
        return nil, fmt.Errorf("cannot chunk nil")
    }

    valueOfList := reflect.ValueOf(list)
    tp := valueOfList.Kind()
    switch tp {
    case reflect.Slice, reflect.Array:
        length := valueOfList.Len()

        chunkCount := (length + size - 1) / size
        result := make([][]any, chunkCount)

        for i := 0; i < chunkCount; i++ {
            start := i * size
            end := start + size

            if end > length {
                end = length
            }

            chunkLength := end - start
            result[i] = make([]any, chunkLength)

            for j := 0; j < chunkLength; j++ {
                result[i][j] = valueOfList.Index(start + j).Interface()
            }
        }

        return result, nil

    default:
        return nil, fmt.Errorf("cannot chunk type %s", tp)
    }
}

// Uniq returns a new slice containing unique elements of the given list,
// preserving order.
//
// Parameters:
//
//    list any - the list from which to remove duplicates.
//
// Returns:
//
//    []any - a list containing only the unique elements.
//    error - error if the list is nil or not a slice/array.
//
// Example:
//
//    {{ ["a", "b", "a", "c"] | uniq }} // Output: ["a", "b", "c"], nil
func (sr *SlicesRegistry) Uniq(list any) ([]any, error) {
    if list == nil {
        return nil, fmt.Errorf("cannot uniq nil")
    }

    valueOfList := reflect.ValueOf(list)
    tp := valueOfList.Kind()

    switch tp {
    case reflect.Slice, reflect.Array:
        length := valueOfList.Len()
        result := make([]any, 0, length)
        // This allows for O(1) average-time complexity checks to see
        // if an item has already been encountered, which is much faster than
        // scanning a slice (O(n) time complexity).
        seen := make(map[any]bool, length)

        for i := 0; i < length; i++ {
            item := valueOfList.Index(i).Interface()
            if !seen[item] {
                seen[item] = true
                result = append(result, item)
            }
        }

        return result, nil
    default:
        return nil, fmt.Errorf("cannot find uniq on type %s", tp)
    }
}

// Compact removes nil or zero-value elements from a list.
//
// Parameters:
//
//    list any - the list to compact.
//
// Returns:
//
//    []any - the list without nil or zero-value elements.
//    error - error if the list is nil or not a slice/array.
//
// Example:
//
//    {{ [0, 1, nil, 2, "", 3] | compact }} // Output: [1, 2, 3], nil
func (sr *SlicesRegistry) Compact(list any) ([]any, error) {
    if list == nil {
        return nil, fmt.Errorf("cannot compact nil")
    }

    valueOfList := reflect.ValueOf(list)
    tp := valueOfList.Kind()

    switch tp {
    case reflect.Slice, reflect.Array:
        length := valueOfList.Len()
        result := make([]any, 0, length)

        for i := 0; i < length; i++ {
            item := valueOfList.Index(i).Interface()
            if !helpers.Empty(item) {
                result = append(result, item)
            }
        }

        return result, nil
    default:
        return nil, fmt.Errorf("cannot compact on type %s", tp)
    }
}

// Flatten flattens a nested list into a single list of elements.
//
// Parameters:
//
//    list any - the list to flatten.
//
// Returns:
//
//    []any - the flattened list.
//    error - error if the list is nil or not a slice/array.
//
// Example:
//
//    {{ flatten [[1, 2], [3, 4], 5] }} // Output: [1, 2, 3, 4, 5]
func (sr *SlicesRegistry) Flatten(list any) ([]any, error) {
    return sr.FlattenDepth(-1, list)
}

// FlattenDepth flattens a nested list into a single list of elements up to a
// specified depth.
//
// Parameters:
//
//    deep int - the maximum depth to flatten to; -1 for infinite depth.
//    list any - the list to flatten.
//
// Returns:
//
//    []any - the flattened list.
//    error - error if the list is nil or not a slice/array.
//
// Example:
//
//    {{ [[1, 2, [3], 4], 5] | flattenDepth 1 }} // Output: [1, 2, [3], 4, 5]
func (sr *SlicesRegistry) FlattenDepth(deep int, list any) ([]any, error) {
    if list == nil {
        return nil, fmt.Errorf("cannot flatten nil")
    }

    valueOfList := reflect.ValueOf(list)
    tp := valueOfList.Kind()

    switch tp {
    case reflect.Slice, reflect.Array:
        return sr.flattenSlice(valueOfList, deep), nil
    default:
        return nil, fmt.Errorf("cannot flatten on type %s", tp)
    }
}

// Slice extracts a slice from a list between two indices.
//
// Parameters:
//
//    list any - the list to slice.
//    indices ...any - the start and optional end indices; if end is omitted,
//
// slices to the end.
//
// Returns:
//
//    any - the sliced part of the list.
//    error - protect against undesired behavior due to migration to new signature.
//
// Example:
//
//    {{ [1, 2, 3, 4, 5] | slice 1, 3 }} // Output: [2, 3], nil
func (sr *SlicesRegistry) Slice(args ...any) (any, error) {
    // ! BACKWARDS COMPATIBILITY: deprecated in v1.0 and removed in v1.1
    // ! Due to change in signature, this function still supports the old signature
    // ! to let users transition to the new signature.
    // * Old signature: Slice(list any, indices ...any)
    // * New signature: Slice(indices ...any, list any)
    if len(args) < 1 {
        return []any{}, deprecated.ErrArgsCount(2, len(args))
    }

    if len(args) == 1 {
        return args[0], nil
    }

    switch reflect.ValueOf(args[0]).Kind() {
    case reflect.Array, reflect.Slice, reflect.Invalid:
        // Old signature
        deprecated.SignatureWarn(sr.handler.Logger(), "slice", "{{ slice list 1 2 }}", "{{ list | slice 1 2 }}")
        return sr.Slice(append(args[1:], args[0])...)
    }

    // New signature
    list := args[len(args)-1]
    indices := args[:len(args)-1]
    // ! END OF BACKWARDS COMPATIBILITY

    if list == nil {
        return nil, fmt.Errorf("cannot slice nil")
    }

    valueOfList := reflect.ValueOf(list)
    tp := valueOfList.Kind()

    switch tp {
    case reflect.Slice, reflect.Array:
        length := valueOfList.Len()
        if length == 0 {
            return nil, nil
        }

        start, end := 0, length

        // Handle start index
        if len(indices) > 0 {
            start = cast.ToInt(indices[0])
            if start < 0 || start > length {
                return nil, fmt.Errorf("start index out of bounds")
            }
        }

        // Handle end index
        if len(indices) > 1 {
            end = cast.ToInt(indices[1])
            if end < start || end > length {
                return nil, fmt.Errorf("end index out of bounds")
            }
        }

        return valueOfList.Slice(start, end).Interface(), nil
    default:
        return nil, fmt.Errorf("last argument must be a slice but got %T", args[len(args)-1])
    }
}

// Has checks if a specified element is present in a collection and handles
// type errors.
//
// Parameters:
//
//    element any - the element to search for in the collection.
//    list any - the collection in which to search for the element.
//
// Returns:
//
//    bool - true if the element is found, otherwise false.
//    error - error if the list is not a type that can be searched (not a slice or array).
//
// Example:
//
//    {{ [1, 2, 3, 4] | has 3 }} // Output: true, nil
func (sr *SlicesRegistry) Has(element any, list any) (bool, error) {
    if list == nil {
        return false, nil
    }
    valueOfList := reflect.ValueOf(list)
    tp := valueOfList.Kind()

    switch tp {
    case reflect.Slice, reflect.Array:
        length := valueOfList.Len()

        for i := 0; i < length; i++ {
            item := valueOfList.Index(i).Interface()
            if reflect.DeepEqual(element, item) {
                return true, nil
            }
        }

        return false, nil
    default:
        return false, fmt.Errorf("cannot find has on type %s", tp)
    }
}

// Without returns a new list excluding specified elements.
//
// Parameters:
//
//    list any - the original list.
//    omit ...any - elements to exclude from the new list.
//
// Returns:
//
//    []any - the list excluding the specified elements.
//    error - protect against undesired behavior due to migration to new signature.
//
// Example:
//
//    {{ [1, 2, 3, 4] | without 2, 4 }} // Output: [1, 3], nil
func (sr *SlicesRegistry) Without(args ...any) ([]any, error) {
    // ! BACKWARDS COMPATIBILITY: deprecated in v1.0 and removed in v1.1
    // ! Due to change in signature, this function still supports the old signature
    // ! to let users transition to the new signature.
    // * Old signature: Without(list any, omit ...any)
    // * New signature: Without(omit ...any, list any)
    if len(args) < 2 {
        return []any{}, deprecated.ErrArgsCount(2, len(args))
    }

    switch reflect.ValueOf(args[0]).Kind() {
    case reflect.Array, reflect.Slice, reflect.Invalid:
        // Old signature
        deprecated.SignatureWarn(sr.handler.Logger(), "without", "{{ without list 1 2 }}", "{{ list | without 1 2 }}")
        return sr.Without(append(args[1:], args[0])...)
    }

    // New signature
    list := args[len(args)-1]
    omit := args[:len(args)-1]
    // ! END OF BACKWARDS COMPATIBILITY

    if list == nil {
        return nil, fmt.Errorf("cannot without nil")
    }

    valueOfList := reflect.ValueOf(list)
    tp := valueOfList.Kind()

    switch tp {
    case reflect.Slice, reflect.Array:
        length := valueOfList.Len()
        omitSet := make(map[any]struct{}, len(omit))

        // Populate the set of items to omit
        for _, o := range omit {
            omitSet[o] = struct{}{}
        }

        result := make([]any, 0, length)

        for i := 0; i < length; i++ {
            item := valueOfList.Index(i).Interface()
            if _, found := omitSet[item]; !found {
                result = append(result, item)
            }
        }

        return result, nil
    default:
        return nil, fmt.Errorf("last argument must be a slice but got %T", args[len(args)-1])
    }
}

// Rest returns all elements of a list except the first.
//
// Parameters:
//
//    list any - the list to process.
//
// Returns:
//
//    []any - the list without the first element.
//    error - error if the list is nil or not a slice/array.
//
// Example:
//
//    {{ [1, 2, 3, 4] | rest }} // Output: [2, 3, 4], nil
func (sr *SlicesRegistry) Rest(list any) ([]any, error) {
    if list == nil {
        return nil, fmt.Errorf("cannot rest nil")
    }

    valueOfList := reflect.ValueOf(list)
    tp := valueOfList.Kind()

    switch tp {
    case reflect.Slice, reflect.Array:
        length := valueOfList.Len()
        if length == 0 {
            return nil, nil
        }

        result := make([]any, length-1)
        for i := 1; i < length; i++ {
            result[i-1] = valueOfList.Index(i).Interface()
        }

        return result, nil
    default:
        return nil, fmt.Errorf("cannot find rest on type %s", tp)
    }
}

// Initial returns all elements of a list except the last.
//
// Parameters:
//
//    list any - the list to process.
//
// Returns:
//
//    []any - the list without the last element.
//    error - error if the list is nil or not a slice/array.
//
// Example:
//
//    {{ [1, 2, 3, 4] | initial }} // Output: [1, 2, 3], nil
func (sr *SlicesRegistry) Initial(list any) ([]any, error) {
    if list == nil {
        return nil, fmt.Errorf("cannot initial nil")
    }

    valueOfList := reflect.ValueOf(list)
    tp := valueOfList.Kind()

    switch tp {
    case reflect.Slice, reflect.Array:
        length := valueOfList.Len()
        if length == 0 {
            return nil, nil
        }

        result := make([]any, length-1)
        for i := 0; i < length-1; i++ {
            result[i] = valueOfList.Index(i).Interface()
        }

        return result, nil
    default:
        return nil, fmt.Errorf("cannot find initial on type %s", tp)
    }
}

// First returns the first element of a list.
//
// Parameters:
//
//    list any - the list from which to take the first element.
//
// Returns:
//
//    any - the first element of the list.
//    error - error if the list is nil, empty, or not a slice/array.
//
// Example:
//
//    {{ [1, 2, 3, 4] | first }} // Output: 1, nil
func (sr *SlicesRegistry) First(list any) (any, error) {
    if list == nil {
        return nil, fmt.Errorf("cannot first nil")
    }

    valueOfList := reflect.ValueOf(list)
    tp := valueOfList.Kind()

    switch tp {
    case reflect.Slice, reflect.Array:
        length := valueOfList.Len()
        if length == 0 {
            return nil, nil
        }

        return valueOfList.Index(0).Interface(), nil
    default:
        return nil, fmt.Errorf("cannot find first on type %s", tp)
    }
}

// Last returns the last element of a list.
//
// Parameters:
//
//    list any - the list from which to take the last element.
//
// Returns:
//
//    any - the last element of the list.
//    error - error if the list is nil, empty, or not a slice/array.
//
// Example:
//
//    {{ [1, 2, 3, 4] | last }} // Output: 4, nil
func (sr *SlicesRegistry) Last(list any) (any, error) {
    if list == nil {
        return nil, fmt.Errorf("cannot last nil")
    }

    valueOfList := reflect.ValueOf(list)
    tp := valueOfList.Kind()

    switch tp {
    case reflect.Slice, reflect.Array:
        length := valueOfList.Len()
        if length == 0 {
            return nil, nil
        }

        return valueOfList.Index(length - 1).Interface(), nil
    default:
        return nil, fmt.Errorf("cannot find last on type %s", tp)
    }
}

// Reverse returns a new list with the elements in reverse order.
//
// Parameters:
//
//    list any - the list to reverse.
//
// Returns:
//
//    []any - the list in reverse order.
//    error - error if the list is nil or not a slice/array.
//
// Example:
//
//    {{ [1, 2, 3, 4] | reverse }} // Output: [4, 3, 2, 1], nil
func (sr *SlicesRegistry) Reverse(list any) ([]any, error) {
    if list == nil {
        return nil, fmt.Errorf("cannot reverse nil")
    }

    valueOfList := reflect.ValueOf(list)
    tp := valueOfList.Kind()

    switch tp {
    case reflect.Slice, reflect.Array:
        length := valueOfList.Len()

        // Create a new slice with the same length as the original
        nl := make([]any, length)
        for i := 0; i < length; i++ {
            nl[i] = valueOfList.Index(length - i - 1).Interface()
        }

        return nl, nil
    default:
        return nil, fmt.Errorf("cannot find reverse on type %s", tp)
    }
}

// SortAlpha sorts a list of strings in alphabetical order.
//
// Parameters:
//
//    list any - the list of strings to sort.
//
// Returns:
//
//    []string - the sorted list.
//
// Example:
//
//    {{ ["d", "b", "a", "c"] | sortAlpha }} // Output: ["a", "b", "c", "d"]
func (sr *SlicesRegistry) SortAlpha(list any) []string {
    kind := reflect.Indirect(reflect.ValueOf(list)).Kind()
    switch kind {
    case reflect.Slice, reflect.Array:
        strList := sr.StrSlice(list)
        sort.Strings(strList)
        return strList
    }

    return []string{helpers.ToString(list)}
}

// SplitList divides a string into a slice of substrings separated by the
// specified separator.
//
// ! FUTURE: Rename this function to be more explicit
//
// Parameters:
//
//    sep string - the delimiter used to split the string.
//    str string - the string to split.
//
// Returns:
//
//    []string - a slice containing the substrings obtained from splitting the input string.
//
// Example:
//
//    {{ "one, two, three" | splitList ", " }} // Output: ["one", "two", "three"]
func (sr *SlicesRegistry) SplitList(sep string, str string) []string {
    return strings.Split(str, sep)
}

// StrSlice converts a value to a slice of strings, handling various types
// including []string, []any, and other slice types.
//
// Parameters:
//
//    value any - the value to convert to a slice of strings.
//
// Returns:
//
//    []string - the converted slice of strings.
//
// Example:
//
//    {{ strSlice any["a", "b", "c"] }} // Output: ["a", "b", "c"]
func (sr *SlicesRegistry) StrSlice(value any) []string {
    return helpers.StrSlice(value)
}

// Until generates a slice of integers from 0 up to but not including 'count'.
// If 'count' is negative, it produces a descending slice from 0 down to 'count',
// inclusive, with a step of -1. The function leverages UntilStep to specify
// the range and step dynamically.
//
// Parameters:
//   count int - the endpoint (exclusive) of the range to generate.
//
// Returns:
//   []int - a slice of integers from 0 to 'count' with the appropriate step
//           depending on whether 'count' is positive or negative.
//
// Example:
//   {{ 5 | until }} // Output: [0 1 2 3 4]
//   {{ -3 | until }} // Output: [0 -1 -2]

func (sr *SlicesRegistry) Until(count int) []int {
    step := 1
    if count < 0 {
        step = -1
    }
    return sr.UntilStep(0, count, step)
}

// UntilStep generates a slice of integers from 'start' to 'stop' (exclusive),
// incrementing by 'step'. If 'step' is positive, the sequence increases; if
// negative, it decreases. The function returns an empty slice if the sequence
// does not make logical sense (e.g., positive step when start is greater than
// stop or vice versa).
//
// Parameters:
//
//    start int - the starting point of the sequence.
//    stop int - the endpoint (exclusive) of the sequence.
//    step int - the increment between elements in the sequence.
//
// Returns:
//
//    []int - a dynamically generated slice of integers based on the input
//            parameters, or an empty slice if the parameters are inconsistent
//            with the desired range and step.
//
// Example:
//
//    {{ 0, 10, 2 | untilStep }} // Output: [0 2 4 6 8]
//    {{ 10, 0, -2 | untilStep }} // Output: [10 8 6 4 2]
func (sr *SlicesRegistry) UntilStep(start, stop, step int) []int {
    return helpers.UntilStep(start, stop, step)
}