Search code examples
haskellhaskell-lens

Increment suffix with Lens


How to write a lens-based function incrementing numerical suffix (if it exists) and keeping the string as is if no such one?

"aaa1" -> "aaa2"
"aaa_123" -> "aaa_124"
"" -> ""
"aaa" -> "aaa"
"123a" -> "123a"
"3a5" -> "3a6"

I have feeling that I need some prism maybe, first, but no idea which one (suffixed does not get a function as an argument but a scalar).


Solution

  • One quick way to figure out which kind of optic you need is considering the properties it should have. In your case, we can ask the questions suggested by this answer of mine, and note that:

    • Like a prism, your optic can fail, as there might not be a numeric suffix.

    • Unlike a prism, however, your optic is not reversible. suffixed is reversible because its target is the prefix, and the suffix being matched is fixed. In your case, the target is the suffix, and the prefix can't be known in advance.

    The lack of reversibility suggests you want a traversal, rather than a prism. With the lens library, implementing a traversal is a matter of writing a function which fits the Traversal type:

    type Traversal s t a b = forall f. Applicative f => (a -> f b) -> s -> f t
    

    Here is one way of doing it for your purposes:

    import Control.Lens
    import Text.Read (readMaybe)
    import Data.Char (isDigit)
    import Data.List.Extra (spanEnd)
    
    -- numericSuffix :: Applicative f => (Integer -> f Integer) -> String -> f String
    numericSuffix :: Traversal' String Integer
    numericSuffix f str =
        (prefix ++) <$> maybe (pure "") (fmap show . f) (readMaybe suffix)
        where
        (prefix, suffix) = spanEnd isDigit str
    

    Notes on the implementation:

    • spanEnd comes from the extra package.

    • Round-tripping through Read and Show as done here will strip leading zeroes from the prefix. If that's undesirable (which seems rather likely), you'll need something more refined than show to format the modified suffix.

    • If the aforementioned round-tripping is not a problem, though, the implementation can be made very compact by using the _Show prism as a traversal:

      numericSuffix f str = (prefix ++) <$> _Show f suffix
      

    Incrementing the suffix can then be straightforwardly done through over:

    incrementSuffix :: String -> String
    incrementSuffix = over numericSuffix succ
    
    ghci> incrementSuffix "aaa1"
    "aaa2"
    ghci> incrementSuffix "aaa_123"
    "aaa_124"
    ghci> incrementSuffix ""
    ""
    ghci> incrementSuffix "aaa"
    "aaa"
    ghci> incrementSuffix "123a"
    "123a"
    ghci> incrementSuffix "3a5"
    "3a6"