/* * 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.LocalDateTime; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoField; import java.time.temporal.TemporalAccessor; import java.util.List; @Data @JsonTypeName("date-time") public class DateTimeExpression implements Expression { public static final DateTimeFormatter STRING_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); // @formatter:off public static final List<DateTimeFormatter> FORMATTERS = ImmutableList.of( DateTimeFormatter.ofPattern("yyyy-MM-dd"), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS") ); // @formatter:On private final Context context; private final LocalDateTime dateTime; @Override public <R> R visit(final Visitor<R> visitor) { return visitor.visitDateTime(this); } // TODO: support other time-zones fetched from the scope. @Override public Expression eval(final Scope scope) { return new InstantExpression(context, dateTime.toInstant(ZoneOffset.UTC)); } @Override public String toRepr() { return String.format("{%s}", STRING_FORMAT.format(dateTime)); } public static DateTimeExpression parse(final Context c, final String input) { final LocalDateTime dateTime = parseLocalDateTime(input); return new DateTimeExpression(c, dateTime); } public static LocalDateTime parseLocalDateTime(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 year = accessor.get(ChronoField.YEAR); final int month = accessor.get(ChronoField.MONTH_OF_YEAR); final int dayOfMonth = accessor.get(ChronoField.DAY_OF_MONTH); final int hour = getOrDefault(accessor, ChronoField.HOUR_OF_DAY, 0); final int minute = getOrDefault(accessor, ChronoField.MINUTE_OF_HOUR, 0); final int second = getOrDefault(accessor, ChronoField.SECOND_OF_MINUTE, 0); final int nanoOfSecond = getOrDefault(accessor, ChronoField.MILLI_OF_SECOND, 0) * 1000000; return LocalDateTime.of(year, month, dayOfMonth, hour, minute, second, nanoOfSecond); } final IllegalArgumentException e = new IllegalArgumentException("Invalid dateTime: " + 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; } }