2016-10-31 65 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

相關問題