是否有一种简单的方法可以按照某些规则从字符串中提取数字?

时间:2016-10-31 13:56:56

标签: delphi lazarus freepascal

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

这项任务听起来很简单,但随着时间的推移,我发现自己越来越困惑,并且可以通过一些指导来实现。

以下面的测试字符串为例:

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.作为Float添加到列表中(即使从技术上讲它无效)

示例1

为了使这一点更加清晰,在上面引用所需输出的测试字符串应该如下:

enter image description here

从上图中,浅蓝色表示浮点数,浅红色表示单个整数(但请注意浮点数如何连接在一起分成单独的浮点数。)

  
      
  • 45.826(Float)
  •   
  • 53.91(Float)
  •   
  • 7(整数)
  •   
  • 5(整数)
  •   
  • 66。 (浮点型)
  •   
  • 4(整数)
  •   
  • 5.40(Float)
  •   
  • 3。 (浮点型)
  •   

注意66之间有故意的空格。和3。以上是由于数字的格式化方式。

示例2:

  

Anoth3r Te5.t string .4 abc 8.1Q 123.45.67.8.9

enter image description here

  
      
  • 4(整数)
  •   
  • 8.1(Float)
  •   
  • 123.45(Float)
  •   
  • 67.8(Float)
  •   
  • 9(整数)
  •   

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

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 个答案:

答案 0 :(得分:14)

您的规则相当复杂,因此您可以尝试构建有限状态机(FSM,DFA - Deterministic finite automaton)。

每个字符都会导致状态之间的转换。

例如,当你处于状态&#34;整数开始&#34;并且遇到空间字符,你产生整数值,FSM进入状态&#34;任何想要的东西&#34;。

如果你处于状态&#34;整数开始&#34;并且会见&#39;。&#39;,FSM进入状态&#34;浮动或整数列表开始&#34;等等。

答案 1 :(得分:6)

答案非常接近,但有几个基本错误。给你一些提示(没有为你编写代码):在while循环中你必须总是递增(增量不应该是它在哪里,否则你得到一个无限循环)你必须检查你还没有到达结束字符串(否则你得到一个异常),最后你的while循环不应该依赖于CH1,因为它永远不会改变(再次导致无限循环)。但我最好的建议是使用调试器跟踪代码 - 这就是它的用途。那么你的错误就会变得明显。

答案 2 :(得分:3)

你的代码中有很多基本错误,我决定纠正你的作业。这仍然不是一个好方法,但至少删除了基本错误。注意阅读评论!

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;

答案 3 :(得分: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状态的主要原因,它仅在空间或周期结束。

答案 4 :(得分: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.