2016-10-31 38 views
16

我需要从一个字符串中提取数字并将它们放入一个列表中,但是对此有一些规则,例如标识提取的数字是整数还是浮点数。是否有一种简单的方法从一个字符串中提取数字,遵循特定的规则?

这个任务听起来很简单,但随着时间的推移,我发现自己越来越困惑,并且可以真正做到一些指导。


采取以下测试字符串作为一个例子:

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

的规则解析所述字符串时遵循如下:

  • 号码不能用字母来preceeded。

  • 如果它找到一个数字并且是而不是后跟一个小数点,那么该数字就是一个整数。

  • 如果它发现了一些和后跟一个小数点然后数为浮点数,例如5.

  • 〜如果更多的数字跟随在小数点然后数目仍然是一个浮动,例如5.40

  • 〜甲进一步发现小数点应该然后分手的数量,例如5.40.3变为(5.40浮点型)和(3浮动)

  • 在例如一个字母的以下小数点的情况下,例如3.H然后仍添加3.作为浮动到列表中(即使在技术上它是无效的)

实施例1

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

enter image description here

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

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

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

实施例2:

Anoth3r Te5.t串0.4 ABC 8.1Q 123.45.67.8.9

enter image description here

  • 4(整数)
  • 8.1(浮动)
  • 123.45(浮动)
  • 67.8(浮点)
  • 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。

+0

看看'System.Masks.MatchesMask'函数。我没有尝试,但这可能可以帮助你。 –

+4

@DavidHeffernan考虑到我写出我认为是一个有效问题的时间(你真的不知道问题是什么?),这不是一个公平的假设,并且也展示了我的进步和努力,以最好的我的能力。如果我想要一个人为我做这一切,那么我不会付出太多的努力,所以请不要假设我想要一个复制和粘贴的答案,我只需要一些指导来帮助我在路上,你只能从程序员的角度来学习而不是复制和粘贴,所以请不要假设我期望有人为我完成这项工作。 – Craig

+0

那么你的问题是什么。非常具体。 –

回答

13

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

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

例如,当您处于状态“开始整数”并且符合空格字符时,您将生成整数值,并且FSM将进入“任何想要的状态”状态。

如果处于状态“已启动整数”且符合'。',FSM将进入状态“浮点数或整数列表已启动”等。

+1

状态机是最佳选择。 –

+0

哇,看起来好像我低估了任务,如果这是涉及到的那种事情。我以为我可以简单地迭代字符串中的每个字符并挑选出有效的数字:) – Craig

+0

是的,你可以,但根据状态你必须以不同的方式解释字符。就像MBo所描述的一样。 –

5

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

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. 
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

你已经得到了答案和评论,建议使用状态机,我完全支持。从你在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”,并在你的代码,你只检查最接近的前字符preceeded。但如果它会读“P71”呢?我解释说,即使“1”的前一个字符是“7”,也应该像“7”一样省略“1”。这是ReadingText状态的主要原因,仅在空间或时间段结束。

+0

这么多的答案和评论需要我花一段时间才能让它全部沉入。至于你基于“P71”的假设,那么是的两个数字将被忽略,因为字符串不是以数字开头的。 – Craig

相关问题