Search code examples
ruby-on-railsregexruby

Rails / Ruby method / regex to increment numeric part of string?


Related to an old PHP question here:

How to increment numeric part of a string by one?

I can have any random format of strings (invoice numbers) like this:

  • INV-005
  • INV-5
  • 5567
  • 5567-1A
  • 5567-001A
  • INVOICE
  • etc.

I would like to take these as an input and output:

  • INV-006
  • INV-6
  • 5568
  • 5567-2A
  • 5567-002A
  • INVOICE-1
  • etc.

The last numeric portion is incremented by one. In the case where there is no match it returns "" OR just adds a "-1" to the end. I can handle the logic on my end if its easier to just return "".

On the ruby / rails side it would seem to be like this:

"foo".gsub(/(o+)/, '\1\1\1')
#=> "foooooo"

I just can't seem to figure out the regex logic.


Solution

  • You are looking for the method String#succ.

    def increment_numeric(str)
      str.gsub(/\d+/) { |s| s.succ }
    end
    
    increment_numeric "INV-005"  #=> "INV-006"
    increment_numeric "INV-5"    #=> "INV-6"
    increment_numeric "5567"     #=> "5568"
    increment_numeric "5567-1A"  #=> "5568-2A"
    increment_numeric "567-001A" #=> "568-002A"
    increment_numeric "INVOICE"  #=> "INVOICE"
    

    The operative line can be written more succinctly as follows:

    str.gsub(/\d+/, &:succ)
    

    In view of the asker's comment below, the following might be more appropriate.

    R = /\d+\z|(?<=\d)\p{L}\z/
    
    def increment_numeric(str)
      str.sub(R) { |s| s.succ }
    end
    
    increment_numeric "INV-005"   #=> "INV-006"
    increment_numeric "INV-5"     #=> "INV-6"
    increment_numeric "5567"      #=> "5568"
    increment_numeric "5567-01"   #=> "5567-02"
    increment_numeric "5567-1A"   #=> "5567-1B"
    increment_numeric "567-001A"  #=> "567-001B"
    increment_numeric "A567-001B" #=> "A567-001C"
    increment_numeric "INVOICE-1" #=> "INVOICE-2"
    increment_numeric "INVOICE"   #=> "INVOICE"
    

    The regular expression reads can be written in free spacing mode to make it self-documenting:

    R =
    /
    \d+\z    # match one or more digits at the end of the string
    |        # or
    (?<=\d)  # assert that the following match is preceded by a digit
    \p{L}\z  # match one Unicode letter at the end of the string
    /x       # invoke free-spacing regex definition mode
    

    (?<=\d) is a positive lookbehind.

    There is at most one substitution so either gsub or sub can be used.