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: strings: add PrefixUntil and SuffixAfter #40135

Open
ainar-g opened this issue Jul 9, 2020 · 6 comments
Open

proposal: strings: add PrefixUntil and SuffixAfter #40135

ainar-g opened this issue Jul 9, 2020 · 6 comments
Labels
Projects
Milestone

Comments

@ainar-g
Copy link
Contributor

@ainar-g ainar-g commented Jul 9, 2020

In many projects I've seen code like this:

idx = strings.Index(username, "@")
if idx != -1 {
  name = username[:idx]
} else {
  name = username
}
idx = strings.LastIndex(address, "@")
if idx != -1 {
  host = address[idx+1:]
} else {
  host = address
}

I think this operation—getting a prefix or suffix based on a substring—is common enough to think about adding helpers for them. So I propose to add functions PrefixUntil and SuffixAfter (names up for debate) to package strings. Something like:

// PrefixUntil returns the prefix of the string s until the first
// occurrence of the substring substr, excluding the substring itself.
// It returns s if substr was not found.
func PrefixUntil(s, substr string) (prefix string) {
	var idx = Index(s, substr)
	if idx == -1 {
		return s
	}

	return s[:idx]
}

// SuffixAfter returns the suffix of the string s after the last
// occurrence of the substring substr, excluding the substring itself.
// It returns s if substr was not found.
func SuffixAfter(s, substr string) (suffix string) {
	var idx = LastIndex(s, substr)
	if idx == -1 {
		return s
	}

	return s[idx+len(substr):]
}

So that the code could be rewritten as:

name = strings.PrefixUntil(username, "@")
host = strings.SuffixAfter(address, "@")
@gopherbot gopherbot added this to the Proposal milestone Jul 9, 2020
@gopherbot gopherbot added the Proposal label Jul 9, 2020
@ianlancetaylor ianlancetaylor added this to Incoming in Proposals Jul 9, 2020
@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Jul 9, 2020

It would help if you could identify some cases in the standard library, and/or in other popular Go packages, where the new functions would be used. That is, give some supporting information for "in many projects I've seen...." Thanks.

@ainar-g
Copy link
Contributor Author

@ainar-g ainar-g commented Jul 9, 2020

@ianlancetaylor A simple grep query shows lots, actually. Here's one from package encoding/xml:

if i := strings.LastIndex(prefix, "/"); i >= 0 {
	prefix = prefix[i+1:]
}

Here's one from path:

if i := strings.LastIndex(path, "/"); i >= 0 {
	path = path[i+1:]
}

net/url:

i := strings.LastIndex(authority, "@")
if i < 0 {
	host, err = parseHost(authority)
} else {
	host, err = parseHost(authority[i+1:])
}

net/http (IndexByte, but the meaning is the same):

if i := strings.IndexByte(resp.Status, ' '); i != -1 {
	statusCode = resp.Status[:i]
}

go/types:

if i := strings.LastIndex(name, "/"); i >= 0 {
	name = name[i+1:]
}

test/fixedbugs/issue32778.go, a copy of the code from the original issue, which in turn, I assume, is a version of the code the reporter of that issue saw:

if i := strings.LastIndexByte(string(n), '.'); i >= 0 {
	return Name(n[i+1:])
}
return Name(n)

Lots of that in test/run.go.

I discovered these using:

$ grep -A 5 -e 'strings.\(Last\)\?Index(' -r

And then sifting the results manually.

@networkimprov
Copy link

@networkimprov networkimprov commented Jul 9, 2020

This looks like a variation on strings.SplitN(str, "@", 2). So maybe SplitFirst & SplitLast?

@ainar-g
Copy link
Contributor Author

@ainar-g ainar-g commented Jul 9, 2020

@networkimprov Those functions don't really split (turn one into many) strings. And the proposed logic is different from that of the Split.* functions. Perhaps FirstPrefix and LastSuffix.

@jimmyfrasche
Copy link
Member

@jimmyfrasche jimmyfrasche commented Jul 9, 2020

@ainar-g I believe these would be equivalent

name = strings.PrefixUntil(username, "@")
name, _ = strings.SplitFirst(username, "@")
@martisch
Copy link
Member

@martisch martisch commented Jul 10, 2020

If I should rather Split off the below into its own proposal I can but it seems the frequency of both patterns (prefix, suffix and partition) together makes for a potential better outlook to be relevant enough for addition.

Adding more general strings.SplitFirst and strings.SplitLast:

Much code I have optimised in parsing functions involves splitting around an identifier and uses Index and LastIndex.
so a key, value, ok = strings.SplitFirst(kv, separator) (and same for Last) would be nice and would cover the use cases of strings.PrefixUntil and SuffixAfter too.

Why ok as a return? To distinguish the case of separator appearing in the source string or not. Knowing value == "" does not provide that.

I can give Googlers references to internal CLs. Here is one for std lib:
https://go-review.googlesource.com/c/go/+/231738/1/src/encoding/asn1/common.go

Why not strings.Split(N)? these allocate a return slice and there is no strings.SplitLastN.

Alternative: strings.Partition like in Python:

Another take on this could be to align this with str.partition from Python:
https://docs.python.org/3/library/stdtypes.html#str.partition

str.partition(sep)
Split the string at the first occurrence of sep, and return a 3-tuple containing the part before the separator, the separator itself, and the part after the separator. If the separator is not found, return a 3-tuple containing the string itself, followed by two empty strings.

So we could discuss strings.Partition(s, sep string) (first, split, tail) and strings.PartitionLast(s, sep string) (head, split, last string) as an alternative with similar semantics as SplitFirst and SplitLast.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Proposals
Incoming
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
6 participants
You can’t perform that action at this time.