Skip to content

Commit

Permalink
core: support custom set of references for dates
Browse files Browse the repository at this point in the history
Allows references like `now` to be set to an explicit value
for the purposes of tests. Can also be used to have custom
reference points.
  • Loading branch information
brharrington committed Nov 9, 2023
1 parent 4a91e91 commit bdf3fe1
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 82 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -223,19 +223,6 @@ object MathExpr {

def finalGrouping: List[String] = Nil

private def parseDate(
gs: ZonedDateTime,
ge: ZonedDateTime,
ref: Option[String],
d: String
): ZonedDateTime = {
ref match {
case Some("gs") => Strings.parseDate(gs, d, zone)
case Some("ge") => Strings.parseDate(ge, d, zone)
case _ => Strings.parseDate(d, zone)
}
}

private def parseDates(context: EvalContext): (ZonedDateTime, ZonedDateTime) = {
val gs = Instant.ofEpochMilli(context.start).atZone(zone)
val ge = Instant.ofEpochMilli(context.end).atZone(zone)
Expand All @@ -254,20 +241,22 @@ object MathExpr {
throw new IllegalArgumentException("end time is relative to itself")
}

val refs = Map("gs" -> gs, "ge" -> ge)

// If one is relative to the other, the absolute date must be computed first
if (sref.contains("e")) {
// start time is relative to end time
val end = parseDate(gs, ge, eref, e)
val start = Strings.parseDate(end, s, zone)
val end = Strings.parseDate(e, zone, refs)
val start = Strings.parseDate(s, zone, refs + ("e" -> end))
start -> end
} else if (eref.contains("s")) {
// end time is relative to start time
val start = parseDate(gs, ge, sref, s)
val end = Strings.parseDate(start, e, zone)
val start = Strings.parseDate(s, zone, refs)
val end = Strings.parseDate(e, zone, refs + ("s" -> start))
start -> end
} else {
val start = parseDate(gs, ge, sref, s)
val end = parseDate(gs, ge, eref, e)
val start = Strings.parseDate(s, zone, refs)
val end = Strings.parseDate(e, zone, refs)
start -> end
}
}
Expand Down
108 changes: 51 additions & 57 deletions atlas-core/src/main/scala/com/netflix/atlas/core/util/Strings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ object Strings {
}
pos += 1
}
buf.toString()
buf.toString
}

/** Converts a hex character into an integer value. Returns -1 if the input is not a
Expand Down Expand Up @@ -359,62 +359,51 @@ object Strings {
}

/**
* Return the time associated with a given string. The time will be relative
* to `now`.
*/
def parseDate(str: String, tz: ZoneId = ZoneOffset.UTC): ZonedDateTime = {
parseDate(ZonedDateTime.now(tz), str, tz)
}

/**
* Return the time associated with a given string.
*
* - now, n:
* - start, s:
* - end, e:
* - epoch:
*
* - seconds, s:
* - minutes, m:
* - hours, h:
* - days, d:
* - weeks, w:
* - months
* - years, y:
*/
def parseDate(ref: ZonedDateTime, str: String, tz: ZoneId): ZonedDateTime = str match {
case RelativeDate(r, op, p) =>
op match {
case "-" => parseRefVar(ref, r).minus(parseDuration(p))
case "+" => parseRefVar(ref, r).plus(parseDuration(p))
case _ => throw new IllegalArgumentException("invalid operation " + op)
}
case NamedDate(r) =>
parseRefVar(ref, r)
case UnixDate(d) =>
// If the value is too big assume it is a milliseconds unit like java uses. The overlap is
// fairly small and not in the range we typically use:
// scala> Instant.ofEpochMilli(Integer.MAX_VALUE)
// res1: java.time.Instant = 1970-01-25T20:31:23.647Z
val v = d.toLong
val t = if (v > Integer.MAX_VALUE) v else v * 1000L
ZonedDateTime.ofInstant(Instant.ofEpochMilli(t), tz)
case str =>
try IsoDateTimeParser.parse(str, tz)
catch {
case e: Exception => throw new IllegalArgumentException(s"invalid date $str", e)
}
* Return the time associated with a given string. Times can be relative to a reference
* point using syntax `<ref><+/-><duration>`. Supported references points are `s`, `e`, `now`,
* and `epoch`. See `parseDuration` for more details about durations.
*/
def parseDate(
str: String,
tz: ZoneId = ZoneOffset.UTC,
refs: Map[String, ZonedDateTime] = Map.empty
): ZonedDateTime = {
str match {
case RelativeDate(r, op, p) =>
op match {
case "-" => parseRefVar(refs, r).minus(parseDuration(p))
case "+" => parseRefVar(refs, r).plus(parseDuration(p))
case _ => throw new IllegalArgumentException("invalid operation " + op)
}
case NamedDate(r) =>
parseRefVar(refs, r)
case UnixDate(d) =>
// If the value is too big assume it is a milliseconds unit like java uses. The overlap is
// fairly small and not in the range we typically use:
// scala> Instant.ofEpochMilli(Integer.MAX_VALUE)
// res1: java.time.Instant = 1970-01-25T20:31:23.647Z
val v = d.toLong
val t = if (v > Integer.MAX_VALUE) v else v * 1000L
ZonedDateTime.ofInstant(Instant.ofEpochMilli(t), tz)
case str =>
try IsoDateTimeParser.parse(str, tz)
catch {
case e: Exception => throw new IllegalArgumentException(s"invalid date $str", e)
}
}
}

/**
* Returns the datetime object associated with a given reference point.
*/
private def parseRefVar(ref: ZonedDateTime, v: String): ZonedDateTime = {
v match {
case "now" => ZonedDateTime.now(ZoneOffset.UTC)
case "epoch" => ZonedDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC)
case _ => ref
}
private def parseRefVar(refs: Map[String, ZonedDateTime], v: String): ZonedDateTime = {
refs.getOrElse(
v,
v match {
case "epoch" => ZonedDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC)
case _ => ZonedDateTime.now(ZoneOffset.UTC)
}
)
}

/**
Expand Down Expand Up @@ -461,15 +450,20 @@ object Strings {
* @return
* Tuple `start -> end`.
*/
def timeRange(s: String, e: String, tz: ZoneId = ZoneOffset.UTC): (Instant, Instant) = {
def timeRange(
s: String,
e: String,
tz: ZoneId = ZoneOffset.UTC,
refs: Map[String, ZonedDateTime] = Map.empty
): (Instant, Instant) = {
val range = if (Strings.isRelativeDate(s, true) || s == "e") {
require(!Strings.isRelativeDate(e, true), "start and end are both relative")
val end = Strings.parseDate(e, tz)
val start = Strings.parseDate(end, s, tz)
val end = Strings.parseDate(e, tz, refs)
val start = Strings.parseDate(s, tz, refs + ("e" -> end))
start.toInstant -> end.toInstant
} else {
val start = Strings.parseDate(s, tz)
val end = Strings.parseDate(start, e, tz)
val start = Strings.parseDate(s, tz, refs)
val end = Strings.parseDate(e, tz, refs + ("s" -> start))
start.toInstant -> end.toInstant
}
require(isBeforeOrEqual(range._1, range._2), "end time is before start time")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -400,25 +400,25 @@ class StringsSuite extends FunSuite {
test("parseDate, relative minus") {
val ref = ZonedDateTime.of(2012, 2, 1, 3, 0, 0, 0, ZoneOffset.UTC)
val expected = ZonedDateTime.of(2012, 2, 1, 0, 0, 0, 0, ZoneOffset.UTC)
assertEquals(parseDate(ref, "e-3h", ZoneOffset.UTC), expected)
assertEquals(parseDate("e-3h", ZoneOffset.UTC, Map("e" -> ref)), expected)
}

test("parseDate, relative plus") {
val ref = ZonedDateTime.of(2012, 2, 1, 3, 0, 0, 0, ZoneOffset.UTC)
val expected = ZonedDateTime.of(2012, 2, 1, 3, 0, 42, 0, ZoneOffset.UTC)
assertEquals(parseDate(ref, "start+42s", ZoneOffset.UTC), expected)
assertEquals(parseDate("start+42s", ZoneOffset.UTC, Map("start" -> ref)), expected)
}

test("parseDate, relative iso") {
val ref = ZonedDateTime.of(2012, 2, 2, 3, 0, 0, 0, ZoneOffset.UTC)
val expected = ZonedDateTime.of(2012, 2, 1, 2, 54, 18, 0, ZoneOffset.UTC)
assertEquals(parseDate(ref, "start-P1DT5M42S", ZoneOffset.UTC), expected)
assertEquals(parseDate("start-P1DT5M42S", ZoneOffset.UTC, Map("start" -> ref)), expected)
}

test("parseDate, epoch + 4h") {
val ref = ZonedDateTime.of(2012, 2, 2, 3, 0, 0, 0, ZoneOffset.UTC)
val expected = ZonedDateTime.of(1970, 1, 1, 4, 0, 0, 0, ZoneOffset.UTC)
assertEquals(parseDate(ref, "epoch+4h", ZoneOffset.UTC), expected)
assertEquals(parseDate("epoch+4h", ZoneOffset.UTC), expected)
}

test("parseDate, relative invalid op") {
Expand Down Expand Up @@ -456,13 +456,13 @@ class StringsSuite extends FunSuite {
test("parseDate, s=e-0h") {
val ref = ZonedDateTime.of(2012, 2, 1, 3, 0, 0, 0, ZoneOffset.UTC)
val expected = ZonedDateTime.of(2012, 2, 1, 3, 0, 0, 0, ZoneOffset.UTC)
assertEquals(parseDate(ref, "e-0h", ZoneOffset.UTC), expected)
assertEquals(parseDate("e-0h", ZoneOffset.UTC, Map("e" -> ref)), expected)
}

test("parseDate, s=e") {
val ref = ZonedDateTime.of(2012, 2, 1, 3, 0, 0, 0, ZoneOffset.UTC)
val expected = ZonedDateTime.of(2012, 2, 1, 3, 0, 0, 0, ZoneOffset.UTC)
assertEquals(parseDate(ref, "e", ZoneOffset.UTC), expected)
assertEquals(parseDate("e", ZoneOffset.UTC, Map("e" -> ref)), expected)
}

test("parseColor") {
Expand Down Expand Up @@ -636,4 +636,11 @@ class StringsSuite extends FunSuite {
assertEquals(s, parseDate("2018-07-24").toInstant)
assertEquals(e, parseDate("2018-07-24T00:05").toInstant)
}

test("range: explicit now") {
val now = ZonedDateTime.parse("2018-07-24T12:00:00.000Z")
val (s, e) = timeRange("2018-07-24", "now-3h", refs = Map("now" -> now))
assertEquals(s, parseDate("2018-07-24T00:00:00.000Z").toInstant)
assertEquals(e, parseDate("2018-07-24T09:00:00.000Z").toInstant)
}
}

0 comments on commit bdf3fe1

Please sign in to comment.