【问题标题】:Get last set of numbers from string, do math, rebuild back into string?从字符串中获取最后一组数字,做数学,重建回字符串?
【发布时间】:2012-05-04 16:35:14
【问题描述】:

我有一个表示“帐号”的字段,该字段在大多数情况下都不是数字。我需要对这些“数字”进行一些自动递增。显然不适合做数学。我们决定对我们有用的规则是我们想要找到最右边的一组数字并将它们自动递增一个并返回重建的字符串(即使这使它成为一个字符更长)。

这些数字的一些例子是:

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

我正在使用 C#/.NET 4.0。我将 Regex 列为标签,但这不是必需的。此解决方案不必在正则表达式中。

有什么好的方法可以做到这一点吗?理想的性能不是主要问题。我宁愿为此提供清晰且易于理解/维护的代码,除非它全部包含在正则表达式中。

谢谢!

【问题讨论】:

  • 致那些投票结束并否决我的人:为什么?
  • 抱歉挖掘了这个。如果它还支持 SI-00001、SI0001 和 0001 等格式,这将非常有帮助。它会删除前面的 0 并返回 SI-2、SI2 和 2。有人可以帮我解决这个问题吗?谢谢!
  • @Binsoi:您应该发布一个新问题并参考这个问题,说明您的问题与这个问题非常相似,但请注意您的问题有一些不同的要求。对不起,我帮不了你,但我对正则表达式很糟糕。 :-(
  • 好的,我会的。谢谢!没关系没问题:)
  • 感谢您的建议!我所做的是 Regex.Replace(SI-001, @"(? (int.Parse(m.Value) + 1).ToString().PadLeft(match.Length, '0'));它给了我 SI-002。 :)

标签: c# regex .net-4.0


【解决方案1】:
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
}

解释正则表达式:从(字符串的开头)或(非数字)开始,匹配一个或多个数字,后跟零个或多个非数字,然后是字符串的结尾。

当然,如果提取的数字有前导零,就会出错。我将把这个作为练习留给读者。

使用 MatchEvaluator(正如@LB 在他们的回答中所建议的那样)这会变得更轻松:

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

【讨论】:

  • 我喜欢这个正则表达式如何识别最后一组数字本身,而不是让它添加繁琐的代码。
  • 我选择这个答案是因为我觉得它在 Regex 和 C# 中具有最佳的功能平衡。这会产生一小段代码,只要您能弄清楚正则表达式本身,它就非常不言自明,这并不太复杂。谢谢!
【解决方案2】:

如果我理解正确,您想在某个字符串中最右边的数字上加一。

您可以按照其他人的建议使用正则表达式,但由于您正在尝试做一些非常具体的事情,因此正则表达式将证明比仅仅为您所做的实现算法要慢。

您可以针对 Regex 解决方案对此进行测试,并亲眼看看这会快很多:

我跑了 100 万次,并用秒表计时。

结果:

正则表达式 - 10,808,533 个滴答声

我的方式 - 253,355 滴答声

大约快 40 倍!!!

结论: 针对具体问题的具体解决方案。

我的方法要快很多。

这是代码:

    // 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;
    }

【讨论】:

  • 谢谢。有什么理由现在没有将其选为正确答案?显然比其他解决方案要好。
  • 1) 更多解决方案仍在出现。2) 我还没有决定我认为“最佳”解决方案是什么。你的性能可能比其他人好得多,但代码可维护性也是一个因素,我担心初级开发人员是否可以维护你的解决方案。从性能的角度来看,这确实是一个很好的解决方案。但正如我最初所说,该解决方案的性能在我对该解决方案的优先级中较低。
  • 我看不出 jr. 的任何原因。开发人员不会通过一些文档、评论和更好的命名来理解这一点。我在开头添加了一些注释来解释该算法。
  • 虽然分数/接受的答案对 SO 很重要,但关于仅在 30 分钟后不选择答案的评论是相当粗鲁的 IMO。对 OP 的共同礼貌在 SO 上也很重要(除其他事项外,我可以对您的 cmets 做出回应......)
  • @JustinPihony 我并没有急于让他选择我的答案,我只是在问缺少什么。
【解决方案3】:

假设您不想替换 1 位数字。

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

【讨论】:

  • 假设值为 GS3333R2C1234g... 现在会发生什么?您有 2 个匹配项...应该都增加吗?
  • 只应更改最后一个匹配项。我们的想法是尽我们所能尝试并提供一个自动递增的帐号。当有多组数字时,将所有数字组视为静态/固定,最右边的组除外。
  • 虽然这个答案的正则表达式不符合我的需求(如上面的 cmets 中所述),但这是 MatchEvaluator 的一个很好的用途,我确实在我的代码中使用了它。谢谢!
【解决方案4】:

我建议如下:

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());
}

【讨论】:

    【解决方案5】:

    你可以使用这样的正则表达式:

    (\d*)
    

    这将使用Matches method 对所有数字进行分组。然后,您可以获取最后一组并从该组进行修改。

    然后你可以使用匹配索引和长度来重建你的字符串。

    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));
    

    我没有进行错误检查。我会留给你的

    【讨论】:

      【解决方案6】:

      如果您想要一个将结果拼接在一起的简单正则表达式:

      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;
      }
      

      注意事项:

      • 它使用 RegexOptions.RightToLeft 在末尾开始搜索,比查找所有匹配项并获取最后一个匹配项更有效。
      • 它使用“[0-9]”而不是“\d”来避免Turkey Test 问题。

      如果你想使用 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());
      }
      

      对于它的价值,我最初不小心误读了这个并认为你想要进位(即“A3G999 -> A4G000”)。这更有趣,需要进位状态:

      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;
      }
      

      测试用例:

      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");
      

      【讨论】:

      • 有趣。我绝对不想要 ...WithCarry 功能,但这是一个有趣的问题。现在,如果我只能让商务人士相信数字应该是 0-9 或者一些点。
      【解决方案7】:
      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); 
      }
      

      输出:

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

      注释:

      • @LB 使用 lambda 表达式作为 MatchEvaluator FTW!

      • 根据@spender 的回答,前瞻 - (?=\D*$) - 确保仅匹配最后一组数字(但后瞻 - (?&lt;=(\D|^)) - 不需要)。

      • @JeffMoser 使用的 RightToLeft 选项允许它匹配最后一组数字first,但是没有静态的 Replace 方法允许您 (1) 指定 RegexOptions, (2) 使用 MatchEvaluator,以及 (3) 限制替换次数。您必须先实例化一个 Regex 对象:

       

      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); 
      }
      

      输出:

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

      【讨论】:

        【解决方案8】:

        您可以尝试使用String.Split。你可以使用类似的东西:

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

        然后您可以在数组上循环以找到最后一个数字(从NameSplit.length 循环到1Int32.TryParse 找到的第一个数字),增加该数字,然后再次将数组与String.Concat 连接在一起.

        它可能会比 RegEx 效率低,但我认为对于不懂 RegEx 的人来说更容易理解..

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2021-07-18
          • 2011-07-27
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多