有没有一种简单的方法按照特定规则从字符串中提取数字?

16

我需要从一个字符串中提取数字并将它们放入列表中,但是有一些规则需要遵守,例如确定提取的数字是整数还是浮点数。

这个任务听起来很简单,但随着时间的推移,我越来越感到困惑,真的需要一些指导。


以下是一个测试字符串的例子:

There are test values: P7 45.826.53.91.7, .5, 66.. 4 and 5.40.3.

解析字符串时应遵循以下规则:

  • 数字前不能有字母。

  • 如果找到一个数字,且后面没有小数点,则该数字为整数。

  • 如果找到一个数字,并小数点,则该数字为浮点数,例如5.

  • ~ 如果小数点后面跟着更多数字,则该数字仍为浮点数,例如5.40

  • ~ 如果发现另一个小数点,则将该数字分成两个部分,例如5.40.3会变为(5.40 Float)和(3 Float)

  • 如果在小数点后面出现字母,例如3.H,则仍将3.添加到列表中作为浮点数(即使从技术上讲它不是有效的)

示例1

为了使这个规则更加清晰,以引用的测试字符串为例,期望输出如下:

enter image description here

从上面的图片可以看到,浅蓝色表示浮点数,浅红色表示单个整数(注意连在一起的浮点数是如何分开成单独的浮点数的)。

  • 45.826 (Float)
  • 53.91 (Float)
  • 7 (Integer)
  • 5 (Integer)
  • 66 . (Float)
  • 4 (Integer)
  • 5.40 (Float)
  • 3 . (Float)

请注意,由于数字的格式化方式不同,66 .和3 .之间有故意添加的空格。

示例2:

Anoth3r Te5.t string .4 abc 8.1Q 123.45.67.8.9

enter image description here

  • 4 (Integer)
  • 8.1 (Float)
  • 123.45 (Float)
  • 67.8 (Float)
  • 9 (Integer)

为了更好地理解,我创建了一个新项目进行测试,如下所示:

enter image description here


接下来进入实际任务。我想也许我可以从字符串中读取每个字符,并根据上述规则识别出有效的数字,然后将它们放入一个列表中。

就我目前的能力而言,以下是我最好的努力:

enter image description here

代码如下:

unit Unit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls;

type
  TForm1 = class(TForm)
    btnParseString: TButton;
    edtTestString: TEdit;
    Label1: TLabel;
    Label2: TLabel;
    Label3: TLabel;
    lstDesiredOutput: TListBox;
    lstActualOutput: TListBox;
    procedure btnParseStringClick(Sender: TObject);
  private
    FDone: Boolean;
    FIdx: Integer;
    procedure ParseString(const Str: string; var OutValue, OutKind: string);
  public
    { public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

{ TForm1 }

procedure TForm1.ParseString(const Str: string; var OutValue, OutKind: string);
var
  CH1, CH2: Char;
begin
  Inc(FIdx);
  CH1 := Str[FIdx];

  case CH1 of
    '0'..'9': // Found a number
    begin
      CH2 := Str[FIdx - 1];
      if not (CH2 in ['A'..'Z']) then
      begin
        OutKind := 'Integer';

        // Try to determine float...

        //while (CH1 in ['0'..'9', '.']) do
        //begin
        //  case Str[FIdx] of
        //    '.':
        //    begin
        //      CH2 := Str[FIdx + 1];
        //      if not (CH2 in ['0'..'9']) then
        //      begin
        //        OutKind := 'Float';
        //        //Inc(FIdx);
        //      end;
        //    end;
        //  end;
        //end;
      end;
      OutValue := Str[FIdx];
    end;
  end;

  FDone := FIdx = Length(Str);
end;

procedure TForm1.btnParseStringClick(Sender: TObject);
var
  S, SKind: string;
begin
  lstActualOutput.Items.Clear;
  FDone := False;
  FIdx := 0;

  repeat
    ParseString(edtTestString.Text, S, SKind);
    if (S <> '') and (SKind <> '') then
    begin
      lstActualOutput.Items.Add(S + ' (' + SKind + ')');
    end;
  until
    FDone = True;
end;

end.

明显地,它不能给出期望的输出(已标记失败代码),而我的方法可能是错误的,但我感觉只需要在这里做一些改变就能得到可行的解决方案。

此时,尽管认为答案很接近,但我发现自己相当困惑和迷失,任务变得越来越令人恼火,我真的会非常感激一些帮助。


编辑1

在这里,我更接近了一点,因为不再有重复的数字了,但结果仍然明显错误。

enter image description here

unit Unit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls;

type
  TForm1 = class(TForm)
    btnParseString: TButton;
    edtTestString: TEdit;
    Label1: TLabel;
    Label2: TLabel;
    Label3: TLabel;
    lstDesiredOutput: TListBox;
    lstActualOutput: TListBox;
    procedure btnParseStringClick(Sender: TObject);
  private
    FDone: Boolean;
    FIdx: Integer;
    procedure ParseString(const Str: string; var OutValue, OutKind: string);
  public
    { public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

{ TForm1 }

// Prepare to pull hair out!
procedure TForm1.ParseString(const Str: string; var OutValue, OutKind: string);
var
  CH1, CH2: Char;
begin
  Inc(FIdx);
  CH1 := Str[FIdx];

  case CH1 of
    '0'..'9': // Found the start of a new number
    begin
      CH1 := Str[FIdx];

      // make sure previous character is not a letter
      CH2 := Str[FIdx - 1];
      if not (CH2 in ['A'..'Z']) then
      begin
        OutKind := 'Integer';

        // Try to determine float...
        //while (CH1 in ['0'..'9', '.']) do
        //begin
        //  OutKind := 'Float';
        //  case Str[FIdx] of
        //    '.':
        //    begin
        //      CH2 := Str[FIdx + 1];
        //      if not (CH2 in ['0'..'9']) then
        //      begin
        //        OutKind := 'Float';
        //        Break;
        //      end;
        //    end;
        //  end;
        //  Inc(FIdx);
        //  CH1 := Str[FIdx];
        //end;
      end;
      OutValue := Str[FIdx];
    end;
  end;

  OutValue := Str[FIdx];
  FDone := Str[FIdx] = #0;
end;

procedure TForm1.btnParseStringClick(Sender: TObject);
var
  S, SKind: string;
begin
  lstActualOutput.Items.Clear;
  FDone := False;
  FIdx := 0;

  repeat
    ParseString(edtTestString.Text, S, SKind);
    if (S <> '') and (SKind <> '') then
    begin
      lstActualOutput.Items.Add(S + ' (' + SKind + ')');
    end;
  until
    FDone = True;
end;

end.
我的问题是如何从字符串中提取数字,将它们添加到列表中并确定数字是整数还是浮点数?
左侧淡绿色列表框(期望输出)显示了结果应该是什么,右侧淡蓝色列表框(实际输出)显示了实际获得的内容。
请给予建议。谢谢。
注意,我重新添加了Delphi标签,因为我使用XE7,请不要删除它,尽管这个特定的问题在Lazarus中,但我的最终解决方案应该适用于XE7和Lazarus。

5
@DavidHeffernan,这并不公平,因为我花了很长时间写出了我认为是一个有效的问题(你真的不知道问题是什么吗?),并展示了我最大的进展和努力。如果我想要别人替我完成所有工作,那么我就不会在此之前投入如此多的精力。所以请不要仅仅假设我想要一个复制粘贴的答案,我只需要一些指导来帮助我,作为一个程序员,你只有通过学习而不是抄袭才能成长,所以请不要假设我期望有人替我完成工作。 - Craig
1
我很同情,而且很明显你在付出很多努力。 - David Heffernan
2
在哪个星球上,45.826.53.91.7解析为45.826、53.91和7了?你如何确定它不是45、826.53和91.7或者45、826、53.91和7呢?你从哪里获得这些充满随机噪声的数据? - Ken White
3
@Craig,你尝试给自己设定挑战来帮助学习,这值得称赞。但也许你需要先学习一个更重要的课程:大多数程序员会陷入让事情变得过于复杂和困难的陷阱中,而你也犯了这个错误。通过允许符号“.”同时作为小数点和项分隔符,你把原本可能成为一个有趣的解析练习的问题转化成了一个不切实际的问题,很难从中学到任何有用的东西。最重要的课程:不要把事情搞得过于复杂。 - Disillusioned
1
PS:如果你想通过解决编程挑战来学习编程,请搜索像Hackerrank和Codinggame这样的网站。这些网站提供各种更为合理的挑战,通常需要应用更多真实世界的算法。它们可能不支持Delphi,但很有可能支持Pascal。即使你被迫使用不同的语言,算法原则也是相同的,无论使用哪种语言。 - Disillusioned
显示剩余21条评论
5个回答

14
您的规则相当复杂,因此可以尝试构建有限状态机(FSM,DFA -确定性有限自动机)。
每个字符都会导致状态之间的转换。
例如,当您处于“整数已开始”的状态并遇到空格字符时,就会产生整数值,并且FSM进入“任何所需”状态。
如果您处于“整数已开始”状态并遇到“。”,FSM会进入“开始浮点或整数列表”状态,依此类推。

1
状态机是正确的选择。 - LU RD
哇,看起来我低估了这个任务,如果这是涉及的类型,那么我就错了很多。我以为我可以简单地迭代字符串中的每个字符并挑选出有效的数字 :) - Craig
是的,你可以这样做,但是根据状态不同,你必须以不同的方式解释这些字符。就像MBo所描述的那样。 - LU RD
在你的代码中,OutKind已经(至少部分地)代表了你的状态,因此你已经在实现有限状态机的道路上了,只是还没有意识到而已。有限状态机将这个想法形式化,并使代码比你的代码更清晰、更健壮。你可能需要更多的中间状态,并倾向于为每个状态单独编写代码,以降低错误风险并在发生错误时隔离它们。但你离目标并不远,所以不要绝望。 - Dsm
我不记得名字了,但我曾经使用过一些数学解析/公式引擎,将其集成到我的脚本中。已经有很多东西可以比你的试错方法更好地完成这项工作了。 - Jerry Dodge
现在已经完全阅读了链接页面,这似乎是一个不错的选择,但给出的示例似乎非常技术化,但概述和图像样本使其更容易理解。 - Craig

6
答案很接近,但存在几个基本错误。为了给您一些提示(不写代码):在while循环中,您必须始终增加计数器(增量不能放在其他位置,否则会导致无限循环),并且必须检查是否已到达字符串的末尾(否则会导致异常)。最后,您的while循环不应该依赖于CH1,因为它永远不会改变(这又会导致无限循环)。但是我最好的建议是使用调试器跟踪代码-那就是它存在的目的。然后您的错误将变得明显。

3
你收到了一些关于使用状态机的答案和评论,我完全支持这种做法。从你在Edit1中展示的代码来看,我发现你仍然没有实现一个状态机。根据评论,我猜测你不知道如何做到这一点,因此为了引导你朝这个方向努力,这里提供一种方法:
定义你需要处理的状态:
type
  TReadState = (ReadingIdle, ReadingText, ReadingInt, ReadingFloat);
  // ReadingIdle, initial state or if no other state applies
  // ReadingText, needed to deal with strings that includes digits (P7..)
  // ReadingInt, state that collects the characters that form an integer
  // ReadingFloat, state that collects characters that form a float

首先定义状态机的框架。为了尽可能地简化,我选择使用直观的过程式方法,其中包括一个主过程和四个子过程,每个子过程对应一个状态。

procedure ParseString(const s: string; strings: TStrings);
var
  ix: integer;
  ch: Char;
  len: integer;
  str,           // to collect characters which form a value
  res: string;   // holds a final value if not empty
  State: TReadState;

  // subprocedures, one for each state
  procedure DoReadingIdle(ch: char; var str, res: string);
  procedure DoReadingText(ch: char; var str, res: string);
  procedure DoReadingInt(ch: char; var str, res: string);
  procedure DoReadingFloat(ch: char; var str, res: string);

begin
  State := ReadingIdle;
  len := Length(s);
  res := '';
  str := '';
  ix := 1;
  repeat
    ch := s[ix];
    case State of
      ReadingIdle:  DoReadingIdle(ch, str, res);
      ReadingText:  DoReadingText(ch, str, res);
      ReadingInt:   DoReadingInt(ch, str, res);
      ReadingFloat: DoReadingFloat(ch, str, res);
    end;
    if res <> '' then
    begin
      strings.Add(res);
      res := '';
    end;
    inc(ix);
  until ix > len;
  // if State is either ReadingInt or ReadingFloat, the input string
  // ended with a digit as final character of an integer, resp. float,
  // and we have a pending value to add to the list
  case State of
    ReadingInt: strings.Add(str + ' (integer)');
    ReadingFloat: strings.Add(str + ' (float)');
  end;
end;

那就是骨架。主要逻辑在四个状态过程中。
  procedure DoReadingIdle(ch: char; var str, res: string);
  begin
    case ch of
      '0'..'9': begin
        str := ch;
        State := ReadingInt;
      end;
      ' ','.': begin
        str := '';
        // no state change
      end
      else begin
        str := ch;
        State := ReadingText;
      end;
    end;
  end;

  procedure DoReadingText(ch: char; var str, res: string);
  begin
    case ch of
      ' ','.': begin  // terminates ReadingText state
        str := '';
        State := ReadingIdle;
      end
      else begin
        str := str + ch;
        // no state change
      end;
    end;
  end;

  procedure DoReadingInt(ch: char; var str, res: string);
  begin
    case ch of
      '0'..'9': begin
        str := str + ch;
      end;
      '.': begin  // ok, seems we are reading a float
        str := str + ch;
        State := ReadingFloat;  // change state
      end;
      ' ',',': begin // end of int reading, set res
        res := str + ' (integer)';
        str := '';
        State := ReadingIdle;
      end;
    end;
  end;

  procedure DoReadingFloat(ch: char; var str, res: string);
  begin
    case ch of
      '0'..'9': begin
        str := str + ch;
      end;
      ' ','.',',': begin  // end of float reading, set res
        res := str + ' (float)';
        str := '';
        State := ReadingIdle;
      end;
    end;
  end;

国家程序应该是自解释的。但如果有不清楚的地方,请问一下。
你的测试字符串都按照你指定的值列出了结果。其中一个规则有点模糊,我的解释可能是错误的。

数字不能以字母开头

你提供的例子是“P7”,在你的代码中只检查了直接前面的字符。但如果它是“P71”呢?我理解为“1”应该像“7”一样被省略,即使“1”的前一个字符是“7”。这是ReadingText状态的主要原因,该状态仅在空格或句号处结束。

有这么多答案和评论,我需要一段时间来消化它们。至于你基于“P71”的假设,那么是的,由于该字符串没有以数字开头,因此两个数字都将被忽略。 - Craig

2

你的代码里有很多基本错误,我决定帮你改正作业。虽然这还不是一个好的解决方案,但至少基本错误已经被消除了。请注意阅读注释!

procedure TForm1.ParseString(const Str: string; var OutValue,
  OutKind: string);
//var
//  CH1, CH2: Char;      <<<<<<<<<<<<<<<< Don't need these
begin
  (*************************************************
   *                                               *
   * This only corrects the 'silly' errors. It is  *
   * NOT being passed off as GOOD code!            *
   *                                               *
   *************************************************)

  Inc(FIdx);
  // CH1 := Str[FIdx]; <<<<<<<<<<<<<<<<<< Not needed but OK to use. I removed them because they seemed to cause confusion...
  OutKind := 'None';
  OutValue := '';

  try
  case Str[FIdx] of
    '0'..'9': // Found the start of a new number
    begin
      // CH1 := Str[FIdx]; <<<<<<<<<<<<<<<<<<<< Not needed

      // make sure previous character is not a letter
      // >>>>>>>>>>> make sure we are not at beginning of file
      if FIdx > 1 then
      begin
        //CH2 := Str[FIdx - 1];
        if (Str[FIdx - 1] in ['A'..'Z', 'a'..'z']) then // <<<<< don't forget lower case!
        begin
          exit; // <<<<<<<<<<<<<<
        end;
      end;
      // else we have a digit and it is not preceeded by a number, so must be at least integer
      OutKind := 'Integer';

      // <<<<<<<<<<<<<<<<<<<<< WHAT WE HAVE SO FAR >>>>>>>>>>>>>>
      OutValue := Str[FIdx];
      // <<<<<<<<<<<<< Carry on...
      inc( FIdx );
      // Try to determine float...

      while (Fidx <= Length( Str )) and  (Str[ FIdx ] in ['0'..'9', '.']) do // <<<<< not not CH1!
      begin
        OutValue := Outvalue + Str[FIdx]; //<<<<<<<<<<<<<<<<<<<<<< Note you were storing just 1 char. EVER!
        //>>>>>>>>>>>>>>>>>>>>>>>>>  OutKind := 'Float';  ***** NO! *****
        case Str[FIdx] of
          '.':
          begin
            OutKind := 'Float';
            // now just copy any remaining integers - that is all rules ask for
            inc( FIdx );
            while (Fidx <= Length( Str )) and  (Str[ FIdx ] in ['0'..'9']) do // <<<<< note '.' excluded here!
            begin
              OutValue := Outvalue + Str[FIdx];
              inc( FIdx );
            end;
            exit;
          end;
            // >>>>>>>>>>>>>>>>>>> all the rest in unnecessary
            //CH2 := Str[FIdx + 1];
            //      if not (CH2 in ['0'..'9']) then
            //      begin
            //        OutKind := 'Float';
            //        Break;
            //      end;
            //    end;
            //  end;
            //  Inc(FIdx);
            //  CH1 := Str[FIdx];
            //end;

        end;
        inc( fIdx );
      end;

    end;
  end;

  // OutValue := Str[FIdx]; <<<<<<<<<<<<<<<<<<<<< NO! Only ever gives 1 char!
  // FDone := Str[FIdx] = #0; <<<<<<<<<<<<<<<<<<< NO! #0 does NOT terminate Delphi strings

  finally   // <<<<<<<<<<<<<<< Try.. finally clause added to make sure FDone is always evaluated.
            // <<<<<<<<<< Note there are better ways!
    if FIdx > Length( Str ) then
    begin
      FDone := TRUE;
    end;
  end;
end;

不需要傲慢,这不是作业。更仔细地阅读评论。 - rhody

1
这里有一个使用正则表达式的解决方案。我在Delphi中实现了它(在10.1中测试过,但应该也适用于XE8),我相信你可以为lazarus采用它,只是不确定哪些正则表达式库可以在那里工作。 正则表达式模式使用交替来匹配数字作为整数或浮点数,符合您的规则:
整数:
(\b\d+(?![.\d]))
  • 以单词边界开头(因此在-之前没有字母,数字或下划线 - 如果下划线是问题,则可以使用(?<![[:alnum:] ])
  • 然后匹配一个或多个数字
  • 它们既不后跟数字也不后跟点号

浮动:

(\b\d+(?:\.\d+)?)
  • 以单词边界开头(因此在其前面没有字母、数字或下划线——如果下划线是问题,可以使用(?<![[:alnum:]])代替)
  • 然后匹配一个或多个数字
  • 可选地匹配点后跟更多数字

简单的控制台应用程序看起来像

program Test;

{$APPTYPE CONSOLE}

uses
  System.SysUtils, RegularExpressions;

procedure ParseString(const Input: string);
var
  Match: TMatch;
begin
  WriteLn('---start---');
  Match := TRegex.Match(Input, '(\b\d+(?![.\d]))|(\b\d+(?:\.\d+)?)');
  while Match.Success do
  begin
    if Match.Groups[1].Value <> '' then
      writeln(Match.Groups[1].Value + '(Integer)')
    else
      writeln(Match.Groups[2].Value + '(Float)');
    Match := Match.NextMatch;
  end;
  WriteLn('---end---');
end;

begin
  ParseString('There are test values: P7 45.826.53.91.7, .5, 66.. 4 and 5.40.3.');
  ParseString('Anoth3r Te5.t string .4 abc 8.1Q 123.45.67.8.9');
  ReadLn;
end.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接