How to use elm/parser to parse Int's followed by a period (like in IP Addresses)?

The parser I’m struggling to make work is this snippet:

parseIP : Parser IPAddress
parseIP =
  succeed IPAddress
    |= int
    |. symbol "."
    |= int
    |. symbol "."
    |= int
    |. symbol "."
    |= int

This parser looks reasonable on first blush, but the parser fails as |= int complains that there’s a period following the number and returns the ExpectingInt error. This is problematic in my use case as each component of an IP Address is an int, but also it’s delimited by a period.

I tried to wrap my head around the elm/parser docs, including using the number API to no avail. I’m pretty much stumped at this point on how to return an int from the parser when that int is followed by a . :frowning:.

Here’s an Ellie-app demonstrating my (not working with period & working with comma) parsers in action:
https://ellie-app.com/4h3ffMB25DHa1

1 Like

In my code I decided to just replace "." with ":" before parsing as a simple work-around.

Look at getChompedString comment:

Sometimes parsers like int or variable cannot do exactly what you need. The “chomping” family of functions is meant for that case!

So I would use getChompedString, chompWhile and andThen to parse and validate each part:

type alias IPv4 =
    { a : Int
    , b : Int
    , c : Int
    , d : Int
    , mask : Int
    }


segment : Parser Int
segment =
    getChompedString (chompWhile Char.isDigit)
        |> andThen checkSegment


checkSegment : String -> Parser Int
checkSegment str =
    case String.toInt str of
        Just n ->
            if n >= 0 && n <= 255 then
                succeed n

            else
                problem "invalid segment value"

        Nothing ->
            problem "segment is not a number"


mask : Parser Int
mask =
    getChompedString (chompWhile Char.isDigit)
        |> andThen checkMask


checkMask : String -> Parser Int
checkMask str =
    case String.toInt str of
        Just n ->
            if n >= 0 && n <= 32 then
                succeed n

            else
                problem "invalid subnet mask"

        Nothing ->
            problem "subnet mask is not a number"


ipv4 : Parser IPv4
ipv4 =
    succeed IPv4
        |= segment
        |. symbol "."
        |= segment
        |. symbol "."
        |= segment
        |. symbol "."
        |= segment
        |. symbol "/"
        |= mask

This way, you are also sure that a parsed IPv4 is valid:

https://ellie-app.com/4h5PrzTLJDBa1


You could also define an intRange parser to factorize some code:

intRange : Int -> Int -> Parser Int
intRange from to =
    getChompedString (chompWhile Char.isDigit)
        |> andThen (checkRange from to)


checkRange : Int -> Int -> String -> Parser Int
checkRange from to str =
    case String.toInt str of
        Just n ->
            if n >= from && n <= to then
                succeed n

            else
                rangeProblem from to

        Nothing ->
            rangeProblem from to


rangeProblem : Int -> Int -> Parser a
rangeProblem from to =
    problem <|
        String.join " "
            [ "expected a number between"
            , String.fromInt from
            , "and"
            , String.fromInt to
            ]


ipv4 : Parser IPv4
ipv4 =
    succeed IPv4
        |= intRange 0 255
        |. symbol "."
        |= intRange 0 255
        |. symbol "."
        |= intRange 0 255
        |. symbol "."
        |= intRange 0 255
        |. symbol "/"
        |= intRange 0 32

https://ellie-app.com/4h5VXKJSyDpa1

14 Likes

I ran into the same exact thing a couple weeks ago, trying to parse IP addresses and getting the unexpected behavior from the int parser. There’s an issue on elm/parser about it too.

I personally think this is a bug, but I haven’t really thought through the implications of trying to fix it.

Thank you @dmy! Breaking it up like that was exactly what I needed to understand how Parsers in Elm work :blush:!

I made a final modification based on your code to allow both IPv4 + Mask and only IPv4 by using oneOf:

ipv4 : Parser IPv4
ipv4 =
    succeed IPv4
        |= intRange 0 255
        |. symbol "."
        |= intRange 0 255
        |. symbol "."
        |= intRange 0 255
        |. symbol "."
        |= intRange 0 255
        |= oneOf
            [ map (\_ -> 32) end
            , succeed identity
                |. symbol "/"
                |= intRange 0 32
            ]

https://ellie-app.com/4jcMBV2f47xa1

3 Likes

Note that the first branch of your oneOf requires the end of the string, preventing other parsers after, but the second does not, which seems inconsistent.

You may want instead something like:

        |= oneOf
            [ succeed identity
                |. symbol "/"
                |= intRange 0 32
            , succeed 32
            ]

https://ellie-app.com/4jpNLQDqJ5Ra1

2 Likes

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.