How to extract different variables from a regex without compiling each expression

Viewed 50

I have a struct representing sizes of computer objects. Objects of this struct are constructed from string values input by users; e.g. "50KB" would be tokenised into an int value of "50" and the string value "KB".

type SizeUnit string

const (
    B  = "B"
    KB = "KB"
    MB = "MB"
    GB = "GB"
    TB = "TB"
)

type ObjectSize struct {
    NumberOfUnits int
    Unit          SizeUnit
}

func NewObjectSizeFromString(input_str string) (*ObjectSize, error)

In the body of this function, I first check if the input value is in the valid format; i.e. any number of digits, followed by any one of "B", "KB", "MB", "GB" or "TB". I then extract the int and string components separately and return a pointer to a struct.

In order to do these three things though, I'm having to compile the regex three times. The first time to check the format of the input string

rg, err := regexp.Compile(`^[0-9]+B$|KB$|MB$|GB$|TB$`)

And then compile again to fetch the int component:

rg, err := regexp.Compile(`^[0-9]+`)
rg.FindString(input_str)

And then compile again to fetch the string/units component:

rg, err := regexp.Compile(`B$|KB$|MB$|GB$|TB$`)
rg.FindString(input_str)

Is there any way to get the two components from the input string with a single regex compilation?

The full code can be found on the Go Playground.

I should point out that this is an academic question as I'm experimenting with Go's regex library. For a simple use-case of this sort, I would probably use a simple for loop to parse the input string.

1 Answers

You can capture both the values with a single expression using regexp.FindStringSubmatch:

func NewObjectSizeFromString(input_str string) (*ObjectSize, error) {
    var defaultReturn *ObjectSize = nil
    full_search_pattern := `^([0-9]+)([KMGT]?B)$`

    rg, err := regexp.Compile(full_search_pattern)
    if err != nil {
        return defaultReturn, errors.New("Could not compile search expression")
    }
    matched := rg.FindStringSubmatch(input_str)
    if matched ==  nil {
        return defaultReturn, errors.New("Not in valid format")
    }
    i, err := strconv.ParseInt(matched[1], 10, 32)
    return &ObjectSize{int(i), SizeUnit(matched[2])}, nil
}

See the playground.

The ^([0-9]+)([KMGT]?B)$ regex matches

  • ^ - start of string
  • ([0-9]+) - Group 1 (this value will be held in matched[1]): one or more digits
  • ([KMGT]?B) - Group 2 (it will be in matched[2]): an optional K, M, G, T letter, and then a B letter
  • $ - end of string.

Note that matched[0] will hold the whole match.

Related