Есть ли упрощенный способ извлечения чисел из строки, следуя определенным правилам?
Мне нужно вывести числа из строки и поместить их в список, для этого есть некоторые правила, такие как определение того, является ли извлеченное число целым или плавающим.
Задача звучит достаточно просто, но со временем я становлюсь все более и более запутанным, и на самом деле можно сделать некоторые рекомендации.
В качестве примера возьмем следующую тестовую строку:
There are test values: P7 45.826.53.91.7, .5, 66.. 4 and 5.40.3.
При анализе строки следуют следующие правила:
-
цифры не могут быть превышены буквой.
-
Если он найдет число и не будет следовать десятичной точке, тогда число будет как целое.
-
Если он найдет число и за ним следует десятичная точка, тогда число будет float, например, 5.
-
~ Если число больше соответствует десятичной точке, тогда число по-прежнему является поплавком, например 5.40
-
~ Далее найденная десятичная точка должна затем разбить число, например 5.40.3 становится (5.40 Float) и (3 Float)
-
В случае буквы, например, после десятичной точки, например 3.H
, затем добавьте 3.
в качестве поплавка в список (даже если технически это недопустимо)
Пример 1
Чтобы сделать это немного более понятным, взяв тестовую строку, указанную выше, требуемый результат должен быть следующим:
![введите описание изображения здесь]()
Из приведенного выше изображения светло-синий цвет показывает числа Float, бледно-красный - одиночные целые числа (но обратите внимание также, как сгруппированные поплавки разбиваются на отдельные поплавки).
- 45.826 (Float)
- 53.91 (Float)
- 7 (целое число)
- 5 (целое число)
- 66. (С плавающей точкой)
- 4 (целое число)
- 5.40 (Float)
- 3. (С плавающей точкой)
Обратите внимание, что существуют преднамеренные пробелы между 66. и 3. выше из-за способа форматирования чисел.
Пример 2:
Anoth3r Te5.t строка .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.
Ответы
Ответ 1
Ваши правила довольно сложны, поэтому вы можете попытаться построить конечный автомат (FSM, DFA - Детерминированный конечный автомат).
Каждый char вызывает переход между состояниями.
Например, когда вы находитесь в состоянии "целое число запущено" и встречаете пробел char, вы получаете целочисленное значение, а FSM переходит в состояние "все, что угодно".
Если вы находитесь в состоянии "integer start" и встретите ".", FSM переходит в состояние "плавающий или целочисленный список запущен" и т.д.
Ответ 2
Ответ довольно близок, но есть несколько основных ошибок. Чтобы дать вам несколько советов (без написания кода для вас): внутри цикла while вы ДОЛЖНЫ ВСЕГДА увеличивать (приращение не должно быть там, где в противном случае вы получаете бесконечный цикл), и вы ДОЛЖНЫ проверить, что вы не достигли конца строка (в противном случае вы получите исключение), и, наконец, ваш цикл while не должен зависеть от CH1, потому что это никогда не изменяется (снова приводя к бесконечному циклу). Но мой лучший совет здесь - проследить через код с помощью отладчика - для этого он и есть. Тогда ваши ошибки станут очевидными.
Ответ 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;
Ответ 4
У вас есть ответы и комментарии, которые предлагают использовать конечный автомат, и я полностью поддерживаю это. Из кода, который вы показываете в 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
Затем определите скелет вашего statemachine. Чтобы это было как можно проще, я решил использовать простой процедурный подход, с одной основной процедурой и четырьмя подпроцедурами, по одному для каждого состояния.
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
, которая заканчивается только пробелом или периодом.
Ответ 5
Здесь используется решение с использованием регулярного выражения. Я реализовал его в Delphi (тестировался в 10.1, но должен также работать с XE8), я уверен, что вы можете принять его для lazarus, просто не знаете, какие библиотеки регулярных выражений работают там.
Шаблон регулярного выражения использует чередование чисел совпадений как целых чисел или поплавков, следующих вашим правилам:
Integer:
(\b\d+(?![.\d]))
- начинающийся с границы слова (так что ни буквы, ни числа, ни подчеркивания до - если подчеркивание является проблемой, вы можете использовать
(?<![[:alnum:]])
) вместо этого
- затем сопоставить одну или несколько цифр
- за которыми не следует цифра или точка
Float:
(\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.