Rutas
/
Elm
Elm
/
Ejercicios
/
La prestigiosa pizza de Paola
La prestigiosa pizza de Paola

La prestigiosa pizza de Paola

Ejercicio de aprendizaje

Introducción

Parseo

El parseo es el proceso de transformar texto en datos con significado. Las funciones del paquete String resultan limitadas para eso y las expresiones regulares son difíciles de usar, así que la forma idiomática de parsear en Elm es con el paquete elm/parser.

Veamos un ejemplo en el que escribimos un parser para (un subconjunto de) el lenguaje de programación LOLCODE. Vamos a ver el siguiente programa comentado:

HAI 1                 # program version 1
CAN HAS STDIO?        # import stdio
VISIBLE "HAI WORLD!"  # print "HAI WORLD!"
KTHXBYE               # end of program

Primero, escribamos un tipo de Elm que represente los posibles comandos:

type Command
    = Version Int
    | Import String
    | Print String

Nuestro programa LOLCODE se puede modelar entonces como un List Command. Empecemos con un parser para la línea HAI 1:

versionP : Parser Command
versionP =
    Parser.succeed Version
        |. Parser.keyword "HAI"
        |. Parser.spaces
        |= Parser.int

versionP se construye usando un pipeline de parsers. El operador (|.) significa «parsear el string pero descartar el resultado» y (|=) significa «parsear el string y quedarse con el resultado». En este ejemplo, Parser.keyword "HAI" consumirá un «HAI», pero ese resultado se ignora. Parser.spaces consumirá cualquier número de caracteres ' ', '\n' y '\r', pero eso también se ignorará. Parser.int parseará un número entero, y el valor de ese entero (por ejemplo, 1) se pasará al Parser.succeed Version de más arriba y el parser conseguirá devolver el valor Version 1.

Si uno cualquiera de los parsers del pipeline falla, devolverá un Parser.DeadEnd y todo el parser fallará. Un parser se puede ejecutar con Parser.run:

Parser.run versionP "HAI 1"
    --> Ok (Version 1)
Parser.run versionP "HAI 2"
    --> Ok (Version 2)
Parser.run versionP "BYE 2"
    --> Err [{ problem = ExpectingKeyword "HAI", col = 1, row = 1 }]
Parser.run versionP "HAI MOM"
    --> Err [{ problem = ExpectingInt, col = 5, row = 1 }]

Abordemos la línea «CAN HAS STDIO?» con

importP : Parser Command
importP =
    Parser.succeed Import
        |. Parser.keyword "CAN HAS"
        |. Parser.spaces
        |= packageP
        |. Parser.symbol "?"

importP también es un pipeline de parsers, pero esta vez termina con Parser.symbol, que es adecuado para parsear símbolos como «?». El valor que queremos conservar (el nombre del paquete que hay que importar) lo parsea

packageP : Parser String
packageP =
    Parser.chompWhile Char.isAlphaNum
        |> Parser.getChompedString
        |> Parser.map String.toLower

Como no sabemos cuál será el nombre del paquete, no podemos usar Parser.keyword, así que en su lugar usamos Parser.chompWhile, que consumirá caracteres uno a uno siempre que cumplan una condición (en este caso, ser un carácter alfanumérico). Esos caracteres consumidos los recoge después Parser.getChompedString, que devuelve un Parser String. En este caso, queremos normalizar los nombres de los paquetes, así que usamos Parser.map para modificar el string que contiene el parser y ponerlo en minúsculas.

Parser.run importP "CAN HAS STDIO?"
    --> Ok (Import "stdio")
Parser.run importP "CAN HAS StDiO?"
    --> Ok (Import "stdio")
Parser.run importP "CAN HAS STDIO"
    -->  Err [{ problem = ExpectingSymbol "?", col = 14, row = 1 }]

A continuación, VISIBLE "HAI WORLD!" se parsea con

printP : Parser Command
printP =
    Parser.succeed Print
        |. Parser.keyword "VISIBLE"
        |. Parser.spaces
        |. Parser.symbol "\""
        |= stringP
        |. Parser.symbol "\""

stringP : Parser String
stringP =
    Parser.chompWhile (\c -> c /= '"')
        |> Parser.getChompedString

Parser.run printP "VISIBLE \"HAI WORLD!\""
    --> Ok (Print "HAI WORLD!")

Ya tenemos un parser para cada comando. Por supuesto, en un programa general no se puede predecir el orden de estos comandos, así que construimos un parser de comandos genérico

commandP : Parser Command
commandP =
    Parser.oneOf
        [ versionP
        , importP
        , printP
        ]

Parser.oneOf probará un parser, en el orden dado. Si un parser falla, se prueba el siguiente, pero si tiene éxito, se devuelve su resultado sin probar los parsers restantes. Si todos los parsers fallan, Parser.oneOf falla.

Parser.run commandP "HAI 2"
    --> Ok (Version 2)
Parser.run commandP "CAN HAS STDIO?"
    --> Ok (Import "stdio")
Parser.run commandP "VISIBLE \"HAI WORLD!\""
    --> Ok (Print "HAI WORLD!")
Parser.run commandP "O NOES"
    --> Err [ { problem = ExpectingKeyword "HAI", col = 1, row = 1 }
    --      , { problem = ExpectingKeyword "CAN HAS", col = 1, row = 1 }
    --      , { problem = ExpectingKeyword "VISIBLE", col = 1, row = 1 }
    --      ]

Fíjate en que cada intento fallido produce un Parser.DeadEnd que Parser.oneOf recopila.

Para parsear un programa completo, tenemos que parsear varios comandos de forma secuencial con

programP : Parser (List Command)
programP =
    Parser.sequence
        { start = ""
        , separator = "\n"
        , end = "KTHXBYE"
        , spaces = Parser.succeed ()
        , item = commandP
        , trailing = Parser.Optional
        }

Parser.sequence es un parser muy personalizable con varias opciones. start, separator y end toman strings que separan los elementos de la secuencia; para parsear un array de Elm usaríamos «[», «,» y «]». spaces aporta flexibilidad: recibe un parser que se encargará de los posibles espacios entre elementos y separadores; para parsear un array de Elm usaríamos Parser.spaces, pero en este caso no aceptamos ningún espacio, así que usamos Parser.succeed (), que tiene éxito de inmediato sin consumir nada en realidad. item recibe el parser del elemento y, por último, trailing nos permite decidir si los separadores finales son Parser.Forbidden, Parser.Mandatory o Parser.Optional.

Parser.run programP "HAI 2\nCAN HAS STDIO?\nKTHXBYE"
    --> Ok [Version 2, Import "stdio"]
Parser.run programP "KTHXBYE"
    --> Ok []
Parser.run programP "HAI 2\nCAN HAS STDIO?\n"
    --> Err [ { problem = ExpectingKeyword "HAI", col = 1, row = 3 }
    --      , { problem = ExpectingKeyword "CAN HAS", col = 1, row = 3 }
    --      , { problem = ExpectingKeyword "VISIBLE", col = 1, row = 3 }
    --      , { problem = Expecting "KTHXBYE", col = 1, row = 3 }
    --      ]

Cuando los parsers tienen éxito, dejan de consumir el string

Parser.run Parser.int "1"
    --> Ok 1
Parser.run Parser.int "1 BTW VISIBLE \"U SEE NOTHING\""
    --> Ok 1

Así que, si tenemos que asegurarnos de que hemos parseado todo el string, podemos usar Parser.end

fullProgramP : Parser (List Command)
fullProgramP =
    programP
        |. Parser.spaces
        |. Parser.end

Parser.run fullProgramP "HAI 2\nCAN HAS STDIO?\nKTHXBYE"
    --> Ok [Version 2, Import "stdio"]
Parser.run fullProgramP "HAI 2\nCAN HAS STDIO?\nKTHXBYE\nBTW VISIBLE \"U SEE NOTHING\""
    --> Err [{ problem = ExpectingEnd, col = 1, row = 4 }]

Por último, a veces tenemos que decidir si hacer fallar un parser en función de su contenido ya parseado. Por ejemplo, si necesitáramos un parser que hiciera fallar los programas que empiezan con dos comandos «HAI» seguidos, podríamos usar

singleVersionProgramP : Parser (List Command)
singleVersionProgramP =
    fullProgramP
        |> Parser.andThen
            (\commands ->
                case commands of
                    (Version _) :: (Version _) :: _ ->
                        Parser.problem "Multiple versions defined"

                    _ ->
                        Parser.succeed commands
            )

Parser.andThen puede inspeccionar el contenido de un parser y devuelve un parser nuevo en función de su valor. Aquí hacemos fallar el parser con un mensaje personalizado usando Parser.problem en un caso, y devolvemos el valor original en el otro.

Parser.run singleVersionProgramP "HAI 2\nCAN HAS STDIO?\nKTHXBYE"
    --> Ok [Version 2, Import "stdio"]
Parser.run singleVersionProgramP "HAI 1\nHAI 2\nCAN HAS STDIO?\nKTHXBYE"
    --> Err [{ problem = Problem "Multiple versions defined", col = 8, row = 4 }]

Instrucciones

Estás deseando incorporarte a Paola's Prestigious Pizza como nuevo gerente del restaurante. Tienes muchos planes para gestionar el local de forma eficiente y, para ello, necesitas algunos datos. Lo primero es lo primero: necesitas analizar el menú.

En el menú hay muchos tipos de pizza. Paola te ha dado una lista en un archivo de texto, con una pizza por línea. Cada entrada de pizza tiene tres componentes: su nombre, un indicador de vegetariano opcional y un precio.

1. Analizar el precio de la pizza

Echas un vistazo a las dos primeras líneas del menú:

Regina: tomato, ham, mushrooms, cantal - 11€
Formaggio (v): tomato, emmental - 8€
...

El precio parece un buen punto de partida. Todos los precios son números enteros en euros.

Implementa priceParser para analizar los precios. Como todos los precios están en euros, no necesitas llevar un registro de la moneda, aunque sí que tienes que analizarla.

Parser.run priceParser "8€"
    --> Ok 8
Parser.run priceParser "8"
    --> Err [{ problem = ExpectingSymbol "€", col = 2, row = 1 }]

2. Analizar el indicador de vegetariano

Vuelves a mirar el menú:

Regina: tomato, ham, mushrooms, cantal - 11€
Formaggio (v): tomato, emmental - 8€
...

El indicador de vegetariano "(v)" también es bastante sencillo, así que decides hacerlo a continuación.

Implementa vegetarianParser para analizar el indicador. El analizador debe devolver un Bool, True si el indicador está presente y False en caso contrario.

Parser.run vegetarianParser "(v)"
    --> Ok True
Parser.run vegetarianParser ""
    --> Ok False

3. Analizar los nombres de las pizzas y de los ingredientes

Bien, ¿y ahora qué?

Regina: tomato, ham, mushrooms, cantal - 11€
Formaggio (v): tomato, emmental - 8€
...

Los nombres de los ingredientes y de las pizzas parecen bastante sencillos: todos son palabras sueltas formadas por caracteres ASCII en mayúscula y minúscula.

Implementa wordParser para analizar los nombres. Ya que estás, pon todas las palabras en minúscula para mayor uniformidad.

Parser.run wordParser "REGINA"
    --> Ok "regina"
Parser.run wordParser "tomato"
    --> Ok "tomato"
Parser.run wordParser "(v)"
    --> Ok ""
Parser.run wordParser ""
    --> Ok ""

4. Analizar una lista de ingredientes

Los ingredientes, sin embargo, vienen en grupos:

Regina: tomato, ham, mushrooms, cantal - 11€
Formaggio (v): tomato, emmental - 8€
...

Son palabras sueltas separadas por comas y, posiblemente, espacios.

Implementa ingredientsParser para analizar los ingredientes.

Parser.run ingredientsParser "tomato, emmental"
    --> Ok ["tomato", "emmental"]
Parser.run ingredientsParser "tomato"
    --> Ok ["tomato"]

5. Analizar una pizza completa

Ahora que ya tienes todos los ingredientes, ¡es hora de preparar una Pizza!

Regina: tomato, ham, mushrooms, cantal - 11€
Formaggio (v): tomato, emmental - 8€
...

Primero está el nombre de la pizza, el indicador de vegetariano opcional, dos puntos ':', la lista de ingredientes, un guion '-' y el precio, con posibles espacios intercalados.

Implementa pizzaParser para analizar la pizza completa.

Parser.run pizzaParser "Regina: tomato, ham, mushrooms, cantal - 11€"
    --> Ok (Pizza "regina" False ["tomato", "ham", "mushrooms", "cantal"] 11)

6. Analizar el menú completo

¡Hora de celebrar!

Implementa menuParser para analizar una lista con todas las pizzas separadas por caracteres de nueva línea '\n' en el archivo de texto. Asegúrate de obtener todas las pizzas ejecutando el analizador hasta que llegue al final del archivo.

Parser.run menuParser "Regina: tomato, ham, mushrooms, cantal - 11€\nFormaggio (v): tomato, emmental - 8€"
    --> Ok [Pizza "regina" False ["tomato", "ham", "mushrooms", "cantal"] 11,
    --      Pizza "formaggio" True ["tomato", "emmental"] 8]
Parser.run menuParser "Regina: tomato, ham, mushrooms, cantal - 11€\[END]"
    --> Err [{ problem = ExpectingEnd, col = 1, row = 2 }]

7. Analizar nombres de ingredientes de varias palabras

Oh, no, parece que se te ha escapado algo. En alguna parte más abajo del menú, algunos ingredientes no son palabras sueltas:

...
Tonno: tomato sauce, tuna - 10€
Hawaii: tomato sauce, fresh pineapple, ham - 9€
...

Implementa oneIngredientParser, que aceptará caracteres ASCII en mayúscula y minúscula o espacios ' '. Aprovecha también para corregir un pequeño defecto que tenía wordParser, asegurándote de que los strings vacíos no se reconozcan como ingredientes válidos y, en su lugar, emitir Problem "empty string". Ya que estás, asegúrate de poner los strings en minúscula y de recortar también los caracteres de espacio en blanco de ambos lados.

Parser.run oneIngredientParser "Tomato Sauce"
    --> Ok "tomato sauce"
Parser.run oneIngredientParser "   tomato sauce     "
    --> Ok "tomato sauce"
Parser.run oneIngredientParser ""
    --> Err [{ problem = Problem "empty string", col = 1, row = 1 }]

Una vez definido el analizador, úsalo en ingredientsParser.

Editar en GitHub El enlace se abre en una ventana o pestaña nueva
Elm Exercism

¿Listo para empezar La prestigiosa pizza de Paola?

Regístrate en Exercism para aprender y dominar Elm con 28 conceptos110 ejercicios y mentoría humana real, todo gratis.