rafee
rafee

Reputation: 1771

How to trim last character from Golang Strings.Builder object?

I am trying to write a byte to a golang strings.builder object, call a function and then delete last added byte. But I can't find a way to trim the last byte in strings.builder object passed by reference.

func iPAdrressHelper(s string, steps int, sb *strings.Builder, result *[]string) {
// lots of code
    sb.WriteByte(s[i])
    sb.WriteByte('.')
    iPAdrressHelper(s[i+1:], steps, sb, result)
    // The following function is not available
    sb.DeleteByte('.')
//other code
}

Upvotes: 0

Views: 6856

Answers (2)

Fabricio
Fabricio

Reputation: 7925

One way could be getting the string, reseting the builder and writing to it again.

buf := sb.String()
buf = strings.TrimSuffix(buf, ".")
sb.Reset()
sb.WriteString(buf)

Upvotes: 0

Bruno Reis
Bruno Reis

Reputation: 37822

There's no way to "remove" a character from a strings.Builder object.

What you can do is create the final string by calling sb.String(), and then slice the result to remove the last character:

str := sb.String()
if len(str) > 0 {
  str = str[:len(str)-1]
}

Or you could simply not add that final "." in the first place by detecting whether you're at the end or not.

And as a side note, it seems like you're simply trying to format an IP Address? If that's the case, check the type net.IP and the function func net.IPv4(...) IP. You can easily do something like:

str := net.IPv4(10,0,0,1).String()

And as a second side note, that recursive code feels "forced". It seems to me that a simple loop would likely not only be incredibly more efficient, but also more readable.


EDIT: based on your comment, it seems like this is an "interview problem". Link in the comments for anyone else interested.

Problem: given a string s, return all the possible valid IPs by adding three dots.

This is a classic backtracking problem. You can frame it as a tree you have to explore, and you can stop early as soon as one path would provably lead to an invalid result. The tree is that of the "position of the dots".

As a general approach, here's how these problems can be solved.

You create a recursion that has a base case when the "problem is solved", and you return a singleton with that solution. A singleton is a collection with just one element, in this case the valid solution. In some problems it may be helpful to check if the solution is invalid and return an empty collection. Here, it isn't necessary.

If the problem "is not solved", then you generate each "next partial solution", and call the function recursively for the "remaining problems", and combine their results. You finally return that collection of combined results.

Something like this:

func recursiveHelper(s string, parts []string) []string {
  if len(s) == 0 {
    // Base case, problem solved.
    // We assume that if we ever get to
    // this point, the solution is valid.
    return strings.Join(parts, ".") 
  }
  // We're not at the end, so let's explore
  // the tree, collect the results and
  // return then
  var res []string

  // Each candidate solution must consume
  // 1 to 3 characters from the string, 
  // which must be a valid number between
  // 0 and 255, with no leading zeroes. 
  // Say you have a function that generates
  // those candidates (exercise: write it). 
  // You could also inline that code
  // here instead, if it's simple enough and
  // you're short on time. 
  for _, c := range genCandidates(s, parts) {
    res = append(res, recursiveHelper(s[len(c):], append(parts, c))...)
  }
  return res
} 

The reason it works is that if the part of the tree you're exploring doesn't generate any candidates, it returns an empty slice. That completely stops the exploration of that branch of the tree - the backtracking. When you append the empty slice into the current set of solutions being built, it doesn't change the current set of solution, so you don't get "garbage" into the results slice, ever. Finally, since the helper function is only ever called with an empty string if the call was a result of a VALID candidate, the final call into the base case must yield a valid solution.

More generally, that is, if you see a more complicated backtracking problem that has more complex state, you define a State type that has some helper functions:

type State struct {...}
func (s State) Next() []State {
  // return a slice of potential
  // candidate states
}
func (s State) Invalid() bool {
  // true if this (partial or not) State
  // is already provably invalid
}
func (s State) Valid() bool {
  // true if this is valid.
  // Valid means it solves the problem.
  // Note that this is NOT the same as
  // !s.Invalid(). It's like you got 3 kinds of
  // states: you know for sure it's valid, 
  // you know for sure it's invalid (and won't ever be made valid), or
  // it may be able to produce valid children. 
}

You could even define State as an interface for a more "generic" backtracking algo. With that, here's how you solve any backtracking problem:

func backtrack(st State, fn func(State)) {
  if st.Valid() {
    // found a solution, yield it
    fn(st)
  }
  // find child states
  for _, c := range st.Next() {
    if c.Invalid() {
      // quickly discard this entire branch
      continue
    }
    // continue exploring the tree
    backtrack(c, fn)
  } 
} 

You'd call it from your main solution function like this, assuming that you fill that State code to work with IP adresses etc:

initialState := State{...} 
var ips []string
backtrack(initialState, func (sol State) {
  // you'll need a function that transforms
  // a solution state into the IP address
  // it represents
  ip := sol.String()
  ips = append(ips, ip)
})
return ips

That's all!

Upvotes: 3

Related Questions