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).
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"