/*
* Copyright (c) 2015 Spotify AB.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.spotify.heroic.grammar;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.google.common.collect.ImmutableList;
import lombok.Data;
import java.time.DateTimeException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoField;
import java.time.temporal.TemporalAccessor;
import java.util.List;
import java.util.Optional;
/**
* Expression representing a given time of day, like 12:00:00.
* <p>
* This does <em>not</em> contain timezone or day information, and when evaluated using {@link
* #eval(com.spotify.heroic.grammar.Expression.Scope)} is converted to an {@link
* com.spotify.heroic.grammar.InstantExpression} using the information available in the scope.
*/
@Data
@JsonTypeName("time")
public class TimeExpression implements Expression {
// @formatter:off
public static final List<DateTimeFormatter> FORMATTERS = ImmutableList.of(
DateTimeFormatter.ofPattern("HH:mm"),
DateTimeFormatter.ofPattern("HH:mm:ss"),
DateTimeFormatter.ofPattern("HH:mm:ss.SSS")
);
// @formatter:On
private final Context context;
private final int hours;
private final int minutes;
private final int seconds;
private final int milliSeconds;
@Override
public <R> R visit(final Visitor<R> visitor) {
return visitor.visitTime(this);
}
@Override
public Context getContext() {
return context;
}
// TODO: support other time-zones fetched from the scope.
@Override
public Expression eval(final Scope scope) {
final long now =
scope.lookup(context, Expression.NOW).cast(IntegerExpression.class).getValue();
final Instant nowInstant = Instant.ofEpochMilli(now);
final LocalDateTime local = LocalDateTime.ofInstant(nowInstant, ZoneOffset.UTC);
final int year = local.get(ChronoField.YEAR);
final int month = local.get(ChronoField.MONTH_OF_YEAR);
final int dayOfMonth = local.get(ChronoField.DAY_OF_MONTH);
final Instant instant = LocalDateTime
.of(year, month, dayOfMonth, hours, minutes, seconds, milliSeconds * 1000000)
.toInstant(ZoneOffset.UTC);
return new InstantExpression(context, instant);
}
@Override
public String toRepr() {
return String.format("{{%02d:%02d:%02d.%03d}}", hours, minutes, seconds, milliSeconds);
}
public static TimeExpression parse(final Context c, final String input) {
final ImmutableList.Builder<Throwable> errors = ImmutableList.builder();
for (final DateTimeFormatter f : FORMATTERS) {
final TemporalAccessor accessor;
try {
accessor = f.parse(input);
} catch (final DateTimeException e) {
errors.add(e);
continue;
}
final int hours = accessor.get(ChronoField.HOUR_OF_DAY);
final int minutes = accessor.get(ChronoField.MINUTE_OF_HOUR);
final int seconds = getOrDefault(accessor, ChronoField.SECOND_OF_MINUTE, 0);
final int milliSeconds = getOrDefault(accessor, ChronoField.MILLI_OF_SECOND, 0);
return new TimeExpression(c, hours, minutes, seconds, milliSeconds);
}
final IllegalArgumentException e =
new IllegalArgumentException("Invalid instant: " + input);
errors.build().forEach(e::addSuppressed);
throw e;
}
private static int getOrDefault(
final TemporalAccessor accessor, final ChronoField field, final int defaultValue
) {
return accessor.isSupported(field) ? accessor.get(field) : defaultValue;
}
private static Optional<Integer> getOrEmpty(
final TemporalAccessor accessor, final ChronoField field
) {
return accessor.isSupported(field) ? Optional.of(accessor.get(field)) : Optional.empty();
}
}