Jaxidian
Jaxidian

Reputation: 13511

Get last set of numbers from string, do math, rebuild back into string?

I have a field representing an "Account Number" that is anything but a number most of the time. I need to do some auto-incrementing of these "numbers". Clearly non-ideal for doing math with. The rule that we've decided works for us is that we want to find the right-most group of numbers and auto-increment them by one and return the rebuilt string (even if this makes it one character longer).

Some examples of the numbers are:

I'm working with C#/.NET 4.0. I listed Regex as a tag but that isn't a requirement. This solution need not be in Regular Expressions.

Any thoughts on a good way to do this? Ideal performance isn't a major concern. I'd rather have clear and easy-to-understand/maintain code for this unless it's all wrapped up in a Regex.

Thanks!

Upvotes: 8

Views: 4951

Answers (8)

spender
spender

Reputation: 120400

var src = "ap45245jpb1234h";
var match = Regex.Match(src, @"(?<=(\D|^))\d+(?=\D*$)");
if(match.Success)
{
    var number = int.Parse(match.Value) + 1;
    var newNum=string.Format(
      "{0}{1}{2}",
      src.Substring(0,match.Index),
      number,
      src.Substring(match.Index + match.Length));
    newNum.Dump(); //ap45245jpb1235h
}

Explaining the regex: starting either from (the start of the string) or (a non-digit), match one or more digits that are followed by zero or more non-digits then the end of the string.

Of course, if the extracted number has leading zeros, things will go wrong. I'll leave this as an exercise to the reader.

Using a MatchEvaluator (as suggested by @LB in their answer) this becomes somewhat lighter:

Regex.Replace(
    src,
    @"(?<=(\D|^))\d+(?=\D*$)",
    m => (int.Parse(m.Value)+1).ToString())

Upvotes: 6

Alan Moore
Alan Moore

Reputation: 75222

string[] src = { "AC1234", "GS3R2C1234", "1234", "A-1234", "AC1234g",
                 "GS3R2C1234g", "1234g", "A-1234g", "999", "GS3R2C9999g" };
foreach (string before in src)
{
  string after = Regex.Replace(before, @"\d+(?=\D*$)", 
      m => (Convert.ToInt64(m.Value) + 1).ToString());
  Console.WriteLine("{0} -> {1}", before, after); 
}

output:

AC1234 -> AC1235
GS3R2C1234 -> GS3R2C1235
1234 -> 1235
A-1234 -> A-1235
AC1234g -> AC1235g
GS3R2C1234g -> GS3R2C1235g
1234g -> 1235g
A-1234g -> A-1235g
999 -> 1000
GS3R2C9999g -> GS3R2C10000g

notes:

  • @LB's use of lambda expression as MatchEvaluator FTW!

  • From @spender's answer, the lookahead - (?=\D*$) - ensures that only the last group of digits is matched (but the lookbehind - (?<=(\D|^)) - isn't needed).

  • The RightToLeft option as used by @JeffMoser allows it to match the last group of digits first, but there's no static Replace method that allows you to (1) specify RegexOptions, (2) use a MatchEvaluator, and (3) limit the number of replacements. You have to instantiate a Regex object first:

 

string[] src = { "AC1234", "GS3R2C1234", "1234", "A-1234", "AC1234g",
                 "GS3R2C1234g", "1234g", "A-1234g", "999", "GS3R2C9999g" };
foreach (string before in src)
{
  Regex r = new Regex(@"\d+", RegexOptions.RightToLeft);
  string after = r.Replace(before, m => (Convert.ToInt64(m.Value) + 1).ToString(), 1);
  Console.WriteLine("{0} -> {1}", before, after); 
}

output:

AC1234 -> AC1235
GS3R2C1234 -> GS3R2C1235
1234 -> 1235
A-1234 -> A-1235
AC1234g -> AC1235g
GS3R2C1234g -> GS3R2C1235g
1234g -> 1235g
A-1234g -> A-1235g
999 -> 1000
GS3R2C9999g -> GS3R2C10000g

Upvotes: 1

Jeff Moser
Jeff Moser

Reputation: 20043

If you want a simple Regex that stitches together the result:

private static readonly Regex _ReverseDigitFinder = new Regex("[0-9]+", RegexOptions.RightToLeft);
public static string IncrementAccountNumber(string accountNumber) {
    var lastDigitsMatch = _ReverseDigitFinder.Match(accountNumber);
    var incrementedPart = (Int64.Parse(lastDigitsMatch.Value) + 1).ToString();
    var prefix = accountNumber.Substring(0, lastDigitsMatch.Index);
    var suffix = accountNumber.Substring(lastDigitsMatch.Index + lastDigitsMatch.Length);
    return prefix + incrementedPart + suffix;
}

Notes:

  • It uses the RegexOptions.RightToLeft to start the search at the end and is more efficient that finding all matches and taking the last one.
  • It uses "[0-9]" instead of "\d" to avoid Turkey Test issues.

If you want to use LINQ:

private static readonly Regex _ReverseAccountNumberParser = new Regex("(?<digits>[0-9]+)|(?<nonDigits>[^0-9]+)", RegexOptions.RightToLeft);

public static string IncrementAccountNumber(string accountNumber) {
    bool hasIncremented = false;
    return String.Join("", 
                    _ReverseAccountNumberParser
                        .Matches(accountNumber)
                        .Cast<Match>()
                        .Select(m => {
                            var nonDigits = m.Groups["nonDigits"].Value;
                            if(nonDigits.Length > 0) {
                                return nonDigits;
                            }

                            var digitVal = Int64.Parse(m.Groups["digits"].Value);
                            if(!hasIncremented) {
                                digitVal++;
                            }
                            hasIncremented = true;
                            return digitVal.ToString();
                        })
                        .Reverse());
}

For what it's worth, I accidentally misread this initially and thought you wanted carry bits (i.e. "A3G999 -> A4G000"). This is more interesting and requires carry state:

public static string IncrementAccountNumberWithCarry(string accountNumber) {
    bool hasIncremented = false;
    bool needToCarry = false;
    var result = String.Join("",
                    _ReverseAccountNumberParser
                        .Matches(accountNumber)
                        .Cast<Match>()
                        .Select(m => {
                            var nonDigits = m.Groups["nonDigits"].Value;
                            if (nonDigits.Length > 0) {
                                return nonDigits;
                            }

                            var oldDigitVal = m.Groups["digits"].Value;
                            var digitVal = Int64.Parse(oldDigitVal);

                            if(needToCarry) {
                                digitVal++;
                            }

                            if (!hasIncremented) {
                                digitVal++;
                                hasIncremented = true;
                            }

                            var newDigitVal = digitVal.ToString();
                            needToCarry = newDigitVal.Length > oldDigitVal.Length;
                            if(needToCarry) {
                                newDigitVal = newDigitVal.Substring(1);
                            }

                            return newDigitVal;
                        })
                        .Reverse());
    if(needToCarry) {
        result = "1" + result;
    }

    return result;
}

Test cases:

Debug.Assert(IncrementAccountNumber("AC1234") == "AC1235");
Debug.Assert(IncrementAccountNumber("GS3R2C1234") == "GS3R2C1235");
Debug.Assert(IncrementAccountNumber("1234") == "1235");
Debug.Assert(IncrementAccountNumber("A-1234") == "A-1235");
Debug.Assert(IncrementAccountNumber("AC1234g") == "AC1235g");
Debug.Assert(IncrementAccountNumber("GS3R2C1234g") == "GS3R2C1235g");
Debug.Assert(IncrementAccountNumber("1234g") == "1235g");
Debug.Assert(IncrementAccountNumber("A-1234g") == "A-1235g");
Debug.Assert(IncrementAccountNumber("999") == "1000");
Debug.Assert(IncrementAccountNumber("GS3R2C9999g") == "GS3R2C10000g");
Debug.Assert(IncrementAccountNumberWithCarry("GS3R2C9999g") == "GS3R3C0000g");
Debug.Assert(IncrementAccountNumberWithCarry("999") == "1000");

Upvotes: 1

YoryeNathan
YoryeNathan

Reputation: 14502

If I understand you correctly, you would like to add one to the number which is right-most within a certain string.

You could use Regex as others suggested, but since you are trying to do something very specific, Regex will prove slower than implementing an algorithm just for what you do.

You can test this against the Regex solution, and see for yourself that this will be a lot faster:

I ran both 1 million times and timed it with Stopwatch.

Results:

Regex - 10,808,533 ticks

My way - 253,355 ticks

About 40 times faster!!!

Conclusion: Specific solutions for specific problems.

My way is A LOT faster.

And here's the code:

    // Goes through a string from end to start, looking for the last digit character.
    // It then adds 1 to it and returns the result string.
    // If the digit was 9, it turns it to 0 and continues,
    // So the digit before that would be added with one.
    // Overall, it takes the last numeric substring it finds in the string,
    // And replaces it with itself + 1.
    private static unsafe string Foo(string str)
    {
        var added = false;

        fixed (char* pt = str)
        {
            for (var i = str.Length - 1; i >= 0; i--)
            {
                var val = pt[i] - '0';

                // Current char isn't a digit
                if (val < 0 || val > 9)
                {
                    // Digits have been found and processed earlier
                    if (added)
                    {
                        // Add 1 before the digits,
                        // Because if the code reaches this,
                        // It means it was something like 999,
                        // Which should become 1000
                        str = str.Insert(i + 1, "1");
                        break;
                    }

                    continue;
                }

                added = true;

                // Digit isn't 9
                if (val < 9)
                {
                    // Set it to be itself + 1, and break
                    pt[i] = (char)(val + 1 + '0');
                    break;
                }

                // Digit is 9. Set it to be 0 and continue to previous characters
                pt[i] = '0';

                // Reached beginning of string and should add 1 before digits
                if (i == 0)
                {
                    str = str.Insert(0, "1");
                }
            }
        }

        return str;
    }

Upvotes: 3

Enrico
Enrico

Reputation: 655

You can try using String.Split. You could use something like:

NameSplit=AccountNumber.split(new Char[] {'a','b','....'z'});

Then you could loop on the array to find the last number (loop from NameSplit.length to 1, first numeric found by Int32.TryParse), increment that number, and then concatenate the array together again with String.Concat.

It would probably be less efficient than RegEx, but I think it would be easier to understand for people who don't understand RegEx..

Upvotes: 0

empi
empi

Reputation: 15881

I suggest the following:

string IncrementAccountNumber(string accountNumber)
{
    var matches = Regex.Matches(accountNumber, @"\d+");
    var lastMatch = matches[matches.Count - 1];
    var number = Int32.Parse(lastMatch.Value) + 1;
    return accountNumber.Remove(lastMatch.Index, lastMatch.Length).Insert(lastMatch.Index, number.ToString());
}

Upvotes: 2

Justin Pihony
Justin Pihony

Reputation: 67065

You could use a regex like this:

(\d*)

This will group all of the numbers using the Matches method. You can then get the last group and do your modification from that group.

Then you can use the match Index and Length to rebuild your string.

string input = "GS3R2C1234g";
string pattern = @"(\d*)";
var matches = Regex.Matches(input, pattern);
var lastMatch = matches[matches.Length - 1];
var value = int.Parse(lastMatch.Value);
value++;
var newValue = String.Format("{0}{1}{2}"input.Substring(0,lastMatch.Index), 
    value, input.Substring(lastMatch.Index+lastMatch.Length));

I have not put error checking in. I will leave that up to you

Upvotes: 1

L.B
L.B

Reputation: 116108

Assuming you don't want to replace 1 digit numbers.

string input = "GS3R2C1234g";
var output = Regex.Replace(input, @"\d{2,}$*", m => (Convert.ToInt64(m.Value) + 1).ToString());

Upvotes: 2

Related Questions