Using custom date argument parser in scallop
Overview
I have recently discovered scallop, a simple Scala CLI parsing library.
And in this post I will describe how I have added a custom argument parser that would take a string that represents
a date and transform it into a LocalDate
object, or fail if the date is invalid.
In my recent projects I mostly write Spark Jobs that process several previous days worth of data. So one of the most used arguments for my Spark jobs is a date, so I was looking for an easy way to pass that argument and also validate that it is a valid date.
My Scallop basics
The basic argument parser class for all my projects looks something like this:
class ArgumentsParser(arguments: Seq[String]) extends ScallopConf(arguments) {
val appName = opt[String](name = "app-name", descr = "Application name to use", required = true)
val date = opt[String](name = "date", descr = "Date to process", required = true)
verify()
}
As you can see, I basically need two arguments:
appName
- The name I want to give to the spark application.date
- The date that I want spark to process.
After running the app with the --help
option, I will get the following output:
// -a, --app-name <arg> Application name to use
// -d, --date <arg> Date to process
// -h, --help Show help message
But in this case, the date object is just a String
. And I prefer to work with a LocalDate
.
Especially if I want to process several past days. For example the last 7 days.
So the first change that I would do, is to allow the date
argument to have a default date, if not entered.
And also that this default would be of type LocalDate
.
class ArgumentsParser(arguments: Seq[String]) extends ScallopConf(arguments) {
val appName = opt[String](name = "app-name", descr = "Application name to use", required = true)
val date = opt[LocalDate](name = "date", descr = "Date to process", default = Some(LocalDate.now().minusDays(1)))
verify()
}
In the above example, I have created a default for the date
argument, which provides a LocalDate
object
with yesterdays date.
But if I would like to provide any date, I should create some custom converter from a String to a LocalDate
object.
Fortunately it is very easy to do in scallop
. All we need to do, is to define an implicit converter.
class ArgumentsParser(arguments: Seq[String]) extends ScallopConf(arguments) {
implicit val dateConverter = singleArgConverter[LocalDate](LocalDate.parse)
val appName = opt[String]("app-name", descr = "Application name to use", required = true)
val date = opt[LocalDate](name = "date", descr = "Date to process", default = Some(LocalDate.now().minusDays(1)))
verify()
}
Now it is possible to pass a string argument that represents a date. And it would be converted to a LocalDate
.
For example, the following arguments will fill the variables as follows:
// --app-name sample-app --date 2020-12-23
val arguments = ArgumentsParser(args)
println(s"App name: ${arguments.appName()}, Date: ${arguments.date()}")
// App Name: sample-app, Date: 2020-12-23
And that is great, but what happens if an invalid date is provided? The error message is not that great.
// --app-name sample-app --date 2020-12-40
// [scallop] Error: Bad arguments for option 'date': '2020-12-40' - wrong arguments format
It tells us that there is some issue, but not explaining exactly what.
Luckily for us, scallop
provides a way to handle that with an error handler function that we can pass to
the converter.
class ArgumentsParser(arguments: Seq[String]) extends ScallopConf(arguments) {
implicit val dateConverter = singleArgConverter[LocalDate](LocalDate.parse, error => Left(error.getMessage))
val appName = opt[String]("app-name", descr = "Application name to use", required = true)
val date = opt[LocalDate](name = "date", descr = "Date to process", default = Some(LocalDate.now().minusDays(1)))
verify()
}
And now, when we provide an invalid date, it shows us a more meaningful message.
// --app-name sample-app --date 2020-12-40
// [scallop] Error: Bad arguments for option 'date': '2020-12-40' - Text '2020-12-40' could not be parsed: Invalid value for DayOfMonth (valid values 1 - 28/31): 40
And… That is all for this time. Thanks for reading.