Есть ли упрощенный способ извлечения чисел из строки, следуя определенным правилам?

Мне нужно вывести числа из строки и поместить их в список, для этого есть некоторые правила, такие как определение того, является ли извлеченное число целым или плавающим.

Задача звучит достаточно просто, но со временем я становлюсь все более и более запутанным, и на самом деле можно сделать некоторые рекомендации.


В качестве примера возьмем следующую тестовую строку:

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.