我需要从字符串中提取数字并将它们放入列表中,但是有一些规则,例如识别提取的数字是整数还是浮点数。
这项任务听起来很简单,但随着时间的推移,我发现自己越来越困惑,并且可以通过一些指导来实现。
以下面的测试字符串为例:
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
为了使这一点更加清晰,在上面引用所需输出的测试字符串应该如下:
从上图中,浅蓝色表示浮点数,浅红色表示单个整数(但请注意浮点数如何连接在一起分成单独的浮点数。)
- 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
- 4(整数)
- 8.1(Float)
- 123.45(Float)
- 67.8(Float)
- 9(整数)
为了更好地了解,我在测试时创建了一个新项目,如下所示:
现在进入实际任务。我想也许我可以从字符串中读取每个字符,并根据上述规则识别有效数字,然后将它们拉入列表。
根据我的能力,这是我能管理的最好的:
代码如下:
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
由于不再有重复的数字,所以我得到了一点距离,但结果仍然明显错误。
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 :(得分: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.