Reputation: 134270
I'm trying to achieve a StringContext
extension which will allow me to write this:
val tz = zone"Europe/London" //tz is of type java.util.TimeZone
But with the added caveat that it should fail to compile if the supplied time-zone is invalid (assuming that can be determined at compile-time).
Here's a helper function:
def maybeTZ(s: String): Option[java.util.TimeZone] =
java.util.TimeZone.getAvailableIDs collectFirst { case id if id == s =>
java.util.TimeZone.getTimeZone(id)
}
I can create a non-macro implementation very easily:
scala> implicit class TZContext(val sc: StringContext) extends AnyVal {
| def zone(args: Any *) = {
| val s = sc.raw(args.toSeq : _ *)
| maybeTZ(s) getOrElse sys.error(s"Invalid zone: $s")
| }
| }
Then:
scala> zone"UTC"
res1: java.util.TimeZone = sun.util.calendar.ZoneInfo[id="UTC",offset=0,...
So far, so good. Except that this doesn't fail compilation if the timezone is nonsensical (e.g. zone"foobar"
); the code falls over at runtime. I'd like to extend it to a macro but, despite reading the docs, I'm really struggling with the details (All of the details, to be precise.)
Can anyone help to get me started here? The all-singing, all-dancing solution should look to see if the StringContext
defines any arguments and (if so), defer calculation until runtime, otherwise attempting to parse the zone at compile-time
What have I tried?
Well, macro defs appear to have to be in statically accessible objects. So:
package object oxbow {
implicit class TZContext(val sc: StringContext) extends AnyVal {
def zone(args: Any *) = macro zoneImpl //zoneImpl cannot be in TZContext
}
def zoneImpl(c: reflect.macros.Context)
(args: c.Expr[Any] *): c.Expr[java.util.TimeZone] = {
import c.universe._
//1. How can I access sc from here?
/// ... if I could, would this be right?
if (args.isEmpty) {
val s = sc.raw()
reify(maybeTZ(s) getOrElse sys.error(s"Not valid $s"))
}
else {
//Ok, now I'm stuck. What goes here?
}
}
}
Based on som-snytt's suggestion below, here's the latest attempt:
def zoneImpl(c: reflect.macros.Context)
(args: c.Expr[Any] *): c.Expr[java.util.TimeZone] = {
import c.universe._
val z =
c.prefix.tree match {
case Apply(_, List(Apply(_, List(Literal(Constant(const: String)))))) => gsa.shared.datetime.XTimeZone.getTimeZone(const)
case x => ??? //not sure what to put here
}
c.Expr[java.util.TimeZone](Literal(Constant(z))) //this compiles but doesn't work at the use-site
^^^^^^^^^^^^^^^^^^^
this is wrong. What should it be?
}
At the use-site, a valid zone"UTC"
fails to compile with the error:
java.lang.Error: bad constant value: sun.util.calendar.ZoneInfo[id="UTC",offset=0,dstSavings=0,useDaylight=false,transitions=0,lastRule=null] of class class sun.util.calendar.ZoneInfo
Presumably I should not have used a Literal(Constant( .. ))
to enclose it. What should I have used?
Last example - based on Travis Brown's answer below
def zoneImpl(c: reflect.macros.Context)
(args: c.Expr[Any] *): c.Expr[java.util.TimeZone] = {
import c.universe._
import java.util.TimeZone
val tzExpr: c.Expr[String] = c.prefix.tree match {
case Apply(_, Apply(_, List(tz @ Literal(Constant(s: String)))) :: Nil)
if TimeZone.getAvailableIDs contains s => c.Expr(tz)
case Apply(_, Apply(_, List(tz @ Literal(Constant(s: String)))) :: Nil) =>
c.abort(c.enclosingPosition, s"Invalid time zone! $s")
case _ => ???
// ^^^ What do I do here? I do not want to abort, I merely wish to
// "carry on as you were". I've tried ...
// c.prefix.tree.asInstanceOf[c.Expr[String]]
// ...but that does not work
}
c.universe.reify(TimeZone.getTimeZone(tzExpr.splice))
}
Upvotes: 10
Views: 1971
Reputation: 39577
This is the "song-and-dance" solution that handles interpolation of the timezone:
package object timezone {
import scala.language.implicitConversions
implicit def zoned(sc: StringContext) = new ZoneContext(sc)
}
package timezone {
import scala.language.experimental.macros
import scala.reflect.macros.Context
import java.util.TimeZone
class ZoneContext(sc: StringContext) {
def tz(args: Any*): TimeZone = macro TimeZoned.tzImpl
// invoked if runtime interpolation is required
def tz0(args: Any*): TimeZone = {
val s = sc.s(args: _*)
val z = TimeZoned maybeTZ s getOrElse (throw new RuntimeException(s"Bad timezone $s"))
TimeZone getTimeZone z
}
}
object TimeZoned {
def maybeTZ(s: String): Option[String] =
if (TimeZone.getAvailableIDs contains s) Some(s) else None
def tzImpl(c: Context)(args: c.Expr[Any]*): c.Expr[TimeZone] = {
import c.universe._
c.prefix.tree match {
case Apply(_, List(Apply(_, List(tz @Literal(Constant(const: String)))))) =>
maybeTZ(const) map (
k => reify(TimeZone getTimeZone c.Expr[String](tz).splice)
) getOrElse c.abort(c.enclosingPosition, s"Bad timezone $const")
case x =>
val rts = x.tpe.declaration(newTermName("tz0"))
val rt = treeBuild.mkAttributedSelect(x, rts)
c.Expr[TimeZone](Apply(rt, args.map(_.tree).toList))
}
}
}
}
Usage:
package tztest
import timezone._
object Test extends App {
val delta = 8
//Console println tz"etc/GMT+$delta" //java.lang.RuntimeException: Bad timezone etc/GMT+8
Console println tz"Etc/GMT+$delta"
Console println tz"US/Hawaii"
//Console println tz"US/Nowayi" //error: Bad timezone US/Nowayi
}
Upvotes: 7
Reputation: 139038
The problem is that you can't smuggle a compile-time instance of TimeZone
into the code generated by your macro. You can, however, slip a string literal through, so you can generate code that will construct the TimeZone
you want at run time, while still checking at compile time to make sure the identifier is available.
The following is a complete working example:
object TimeZoneLiterals {
import java.util.TimeZone
import scala.language.experimental.macros
import scala.reflect.macros.Context
implicit class TZContext(val sc: StringContext) extends AnyVal {
def zone() = macro zoneImpl
}
def zoneImpl(c: reflect.macros.Context)() = {
import c.universe._
val tzExpr = c.prefix.tree match {
case Apply(_, Apply(_, List(tz @ Literal(Constant(s: String)))) :: Nil)
if TimeZone.getAvailableIDs contains s => c.Expr(tz)
case _ => c.abort(c.enclosingPosition, "Invalid time zone!")
}
reify(TimeZone.getTimeZone(tzExpr.splice))
}
}
The argument to reify
will be the body of the generated method—literally, not after any kind of evaluation, except that the tzExpr.slice
bit will be replaced by the compile-time string literal (if, of course, you've found it in the list of available identifiers—otherwise you get a compile-time error).
Upvotes: 7