delphi - ¿Existe una forma simplista de extraer números de una cadena siguiendo ciertas reglas?
lazarus freepascal (5)
Aquí hay una solución usando regex. Lo implementé en Delphi (probado en 10.1, pero también debería funcionar con XE8), estoy seguro de que puedes adoptarlo para Lázaro, pero no estoy seguro de qué bibliotecas de expresiones regulares funcionan allí. El patrón de expresiones regulares usa la alternancia para hacer coincidir números como enteros o flotantes siguiendo sus reglas:
Entero:
(/b/d+(?![./d]))
- comenzó con un límite de palabra (por lo tanto, no hay letras, números ni guiones bajos antes, si los guiones bajos son un problema que podría usar
(?<![[:alnum:]])
) - luego une uno o más dígitos
- que no están seguidos de dígitos ni puntos
Flotador:
(/b/d+(?:/./d+)?)
- comenzó con un límite de palabra (por lo tanto, no hay letras, números ni guiones bajos antes, si los guiones bajos son un problema que podría usar
(?<![[:alnum:]])
) - luego une uno o más dígitos
- Opcionalmente, haga coincidir el punto seguido de otros dígitos.
Una aplicación de consola simple parece
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.
Necesito extraer números de una cadena y colocarlos en una lista; existen algunas reglas para esto, como identificar si el número extraído es un número entero o flotante.
La tarea parece bastante simple pero me encuentro cada vez más confundido a medida que pasa el tiempo y realmente podría hacerlo con cierta orientación.
Tome la siguiente cadena de prueba como ejemplo:
There are test values: P7 45.826.53.91.7, .5, 66.. 4 and 5.40.3.
Las reglas a seguir al analizar la cadena son las siguientes:
los números no pueden ser precedidos por una letra.
Si encuentra un número y no está seguido por un punto decimal, entonces el número es como un entero.
Si encuentra un número y le sigue un punto decimal, entonces el número es un flotante, por ejemplo, 5.
~ Si más números siguen el punto decimal, entonces el número sigue siendo un flotante, por ejemplo, 5.40
~ Otro punto decimal encontrado debería dividir el número, por ejemplo, 5.40.3 se convierte en (5.40 Flotante) y (3 Flotante)
En el caso de una letra, por ejemplo, después de un punto decimal, por ejemplo,
3.H
continuación, agregue3.
como Flotante a la lista (incluso si técnicamente no es válido)
Ejemplo 1
Para hacer esto un poco más claro, tomar la cadena de prueba citada sobre la salida deseada debe ser como sigue:
En la imagen de arriba, el color azul claro ilustra los números de flotación, el rojo pálido ilustra los enteros únicos (pero también tenga en cuenta cómo los flotadores unidos se dividen en flotadores separados).
- 45.826 (flotador)
- 53.91 (flotador)
- 7 (entero)
- 5 (entero)
- 66. (Flotador)
- 4 (entero)
- 5.40 (Flotador)
- 3. (Flotador)
Tenga en cuenta que hay espacios deliberados entre 66. y 3. arriba debido a la forma en que se formatearon los números.
Ejemplo 2:
Anoth3r Te5.t string .4 abc 8.1Q 123.45.67.8.9
- 4 (entero)
- 8.1 (Flotador)
- 123.45 (flotador)
- 67.8 (Flotador)
- 9 (entero)
Para dar una mejor idea, creé un nuevo proyecto durante la prueba que se parece a esto:
Ahora en la tarea real. Pensé que tal vez podría leer cada carácter de la cadena e identificar cuáles son los números válidos según las reglas anteriores, y luego agruparlos en una lista.
Para mi capacidad, esto era lo mejor que podía manejar:
El código es el siguiente:
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.
Claramente, no da el resultado deseado (el código fallido ha sido comentado) y mi enfoque probablemente sea incorrecto, pero creo que solo necesito hacer algunos cambios aquí y allá para una solución funcional.
En este punto, me he sentido bastante confundido y bastante perdido, a pesar de pensar que la respuesta es bastante cercana, la tarea se está volviendo cada vez más exasperante y realmente agradecería alguna ayuda.
EDITAR 1
Aquí me acerqué un poco más porque ya no hay números duplicados, pero el resultado sigue siendo claramente erróneo.
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.
Mi pregunta es ¿cómo puedo extraer números de una cadena, agregarlos a una lista y determinar si el número es entero o flotante?
El cuadro de lista verde pálido de la izquierda (salida deseada) muestra cuáles deberían ser los resultados, el cuadro de lista azul pálido de la derecha (salida real) muestra lo que realmente obtuvimos.
Por favor asesóreme, gracias.
Tenga en cuenta que volví a agregar la etiqueta Delphi, ya que uso XE7, así que no la elimine, aunque este problema en particular está en Lazarus, mi solución debería funcionar tanto para XE7 como para Lazarus.
Hay tantos errores básicos en su código que decidí corregir su tarea, por así decirlo. Esta no es una buena forma de hacerlo, pero al menos se eliminan los errores básicos. ¡Ten cuidado de leer los comentarios!
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;
La respuesta es bastante cercana, pero hay varios errores básicos. Para darle algunas sugerencias (sin escribir su código): dentro del ciclo while, DEBE SIEMPRE un incremento (el incremento no debería estar donde está, de lo contrario se obtiene un ciclo infinito) y DEBE verificar que no haya llegado al final de la cadena (de lo contrario obtendrías una excepción) y, finalmente, tu bucle while no debería depender de CH1, porque eso nunca cambia (lo que de nuevo da como resultado un bucle infinito). Pero mi mejor consejo aquí es rastrear a través de su código con el depurador, para eso está. Entonces tus errores se volverían obvios.
Sus reglas son bastante complejas, por lo que puede intentar construir una máquina de estados finitos (FSM, DFA - autómata finito determinista ).
Cada char provoca la transición entre estados.
Por ejemplo, cuando se encuentra en el estado "entero iniciado" y se encuentra con el espacio, se obtiene un valor entero y el FSM pasa al estado "todo lo que se desea".
Si se encuentra en el estado "entero iniciado" y cumple ".", FSM pasa al estado "flotante o lista de enteros iniciada" y así sucesivamente.
Usted tiene respuestas y comentarios que sugieren el uso de una máquina de estados, y yo lo apoyo completamente. Desde el código que muestra en Edit1, veo que todavía no implementó una máquina de estado. Por los comentarios, creo que no sabes cómo hacerlo, así que para empujarte en esa dirección, aquí hay un enfoque:
Defina los estados con los que necesita trabajar:
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
Luego define el esqueleto de tu máquina. Para mantenerlo lo más fácil posible, opté por utilizar un enfoque de procedimiento directo, con un procedimiento principal y cuatro subprocedimientos, uno para cada estado.
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;
Ese es el esqueleto. La lógica principal está en los cuatro procedimientos estatales.
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;
Los procedimientos estatales deben ser autoexplicativos. Pero solo pregunta si algo no está claro.
Las dos cadenas de prueba dan como resultado los valores enumerados según lo especificado. Una de sus reglas era un poco ambigua y mi interpretación podría ser incorrecta.
los números no pueden ser precedidos por una letra
El ejemplo que proporcionó es "P7", y en su código solo verificó el carácter anterior inmediato. Pero, ¿y si se lee "P71"? Lo interpreté como que "1" debería omitirse como "7", aunque el carácter anterior de "1" es "7". Esta es la razón principal del estado de ReadingText
de ReadingText
, que termina solo en un espacio o período.