Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: slices: add Cut, CutPrefix and CutSuffix #64095

Open
pgaskin opened this issue Nov 13, 2023 · 5 comments
Open

proposal: slices: add Cut, CutPrefix and CutSuffix #64095

pgaskin opened this issue Nov 13, 2023 · 5 comments
Labels
Milestone

Comments

@pgaskin
Copy link

pgaskin commented Nov 13, 2023

The strings and bytes packages have Cut, CutPrefix, and CutSuffix.

I propose adding the following functions to the slices package:

func Cut[S ~[]E, E comparable](s S, sep ...E) (before, after S, found bool)
func CutPrefix[S ~[]E, E comparable](s S, prefix ...E) (after S, found bool)
func CutSuffix[S ~[]E, E comparable](s S, suffix ...E) (before S, found bool)

For ergonomics, I chose to make sep/prefix/suffix variadic, since most uses I can think of (e.g., simple parsers) would generally have one, or a few elements at most.

@gopherbot gopherbot added this to the Proposal milestone Nov 13, 2023
@pgaskin
Copy link
Author

pgaskin commented Nov 13, 2023

Lightly tested prototypes:

func Cut[S ~[]E, E comparable](s S, prefix ...E) (before, after S, found bool) {
	if len(prefix) == 0 {
		return s[:0], s, true
	}
	if len(prefix) <= len(s) {
	i:
		for i := range s[:len(s)-len(prefix)+1] {
			for j, x := range prefix {
				if s[i+j] != x {
					continue i
				}
			}
			return s[:i], s[i+len(prefix):], true
		}
	}
	return s, nil, false
}

func CutPrefix[S ~[]E, E comparable](s S, prefix ...E) (after S, found bool) {
	if len(prefix) == 0 {
		return s, true
	}
	if len(prefix) > len(s) {
		return s, false
	}
	for j, x := range prefix {
		if s[j] != x {
			return s, false
		}
	}
	return s[len(prefix):], true
}

func CutSuffix[S ~[]E, E comparable](s S, suffix ...E) (before S, found bool) {
	if len(suffix) == 0 {
		return s, true
	}
	if len(suffix) > len(s) {
		return s, false
	}
	i := len(s) - len(suffix)
	for j, x := range suffix {
		if s[i+j] != x {
			return s, false
		}
	}
	return s[:i], true
}


func main() {
	fmt.Println(Cut(strings.Fields("a b c")))
	fmt.Println(Cut(strings.Fields("a b c"), "a"))
	fmt.Println(Cut(strings.Fields("a b c"), "b"))
	fmt.Println(Cut(strings.Fields("a b c"), "c"))
	fmt.Println(Cut(strings.Fields("a b c"), "a", "b"))
	fmt.Println(Cut(strings.Fields("a b c"), "b", "c"))
	fmt.Println(Cut(strings.Fields("a b c"), "a", "b", "c"))
	fmt.Println(Cut(strings.Fields("a b c"), "a", "b", "c", "d"))
	fmt.Println(Cut(strings.Fields("a b c"), "x"))
	fmt.Println()
	fmt.Println(CutPrefix(strings.Fields("a b c")))
	fmt.Println(CutPrefix(strings.Fields("a b c"), "a"))
	fmt.Println(CutPrefix(strings.Fields("a b c"), "a", "b"))
	fmt.Println(CutPrefix(strings.Fields("a b c"), "a", "b", "c"))
	fmt.Println(CutPrefix(strings.Fields("a b c"), "a", "b", "c", "d"))
	fmt.Println(CutPrefix(strings.Fields("a b c"), "x"))
	fmt.Println()
	fmt.Println(CutSuffix(strings.Fields("a b c")))
	fmt.Println(CutSuffix(strings.Fields("a b c"), "c"))
	fmt.Println(CutSuffix(strings.Fields("a b c"), "b", "c"))
	fmt.Println(CutSuffix(strings.Fields("a b c"), "a", "b", "c"))
	fmt.Println(CutSuffix(strings.Fields("a b c"), "x", "a", "b", "c"))
	fmt.Println(CutSuffix(strings.Fields("a b c"), "x"))
}

@mateusz834
Copy link
Member

mateusz834 commented Nov 13, 2023

Should this also include Func variants? Just as we do with other functions in the slices package.

func CutFunc[S ~[]E, E any](s S, sep ...E, eq func(E, E) bool) (before, after S, found bool)
func CutPrefixFunc[S ~[]E, E any](s S, prefix ...E, eq func(E, E) bool) (after S, found bool)
func CutSuffixFunc[S ~[]E, E any](s S, suffix ...E, eq func(E, E) bool) (before S, found bool)

@seankhliao
Copy link
Member

Is this a common to have slices with inline separator elements?

@pgaskin
Copy link
Author

pgaskin commented Nov 13, 2023

I use this sort of method frequently when writing simple parsers, especially those involving whitespace-delimited formats. For instance, you'd iterate over lines with bufio.Scanner, tokenize it with strings.Fields, then loop over it while the length is non-zero, eating one or more tokens at a time (yes, I know this approach isn't the most efficient, but it's readable, easy to write, and it's good enough for most cases).

This sort of function would also ease checking for and removing a prefix or suffix from an arbitrary slice without needing to add error-prone length checks everywhere.

@adonovan
Copy link
Member

Making it variadic is asymmetric with string.Cut and friends, and raises a momentary confusion (resolved by the types) about whether the function accepts a single prefix expressed as sequence of elements, or a set of possible prefixes. It would be clearer, while no less efficient, for the caller to construct the slice.

The design seems reasonable but I don't immediately recall many times that I've needed this function, perhaps because repeatedly searching for subslices feels like a sign that I'm using the wrong algorithm. (Substrings are different because they enjoy such highly optimized hardware and search algorithms.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: Incoming
Development

No branches or pull requests

5 participants