/** * 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 org.apache.tajo.datum; import com.google.common.primitives.Longs; import org.apache.tajo.common.TajoDataTypes; import org.apache.tajo.exception.InvalidOperationException; import org.apache.tajo.util.datetime.DateTimeUtil; import org.apache.tajo.util.datetime.TimeMeta; import java.text.DecimalFormat; import java.util.HashMap; import java.util.Map; import static org.apache.tajo.type.Type.Interval; public class IntervalDatum extends Datum { public static final long MINUTE_MILLIS = 60 * 1000; public static final long HOUR_MILLIS = 60 * MINUTE_MILLIS; public static final long DAY_MILLIS = 24 * HOUR_MILLIS; public static final long MONTH_MILLIS = 30 * DAY_MILLIS; static enum DATE_UNIT { CENTURY, DECADE, YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, MICROSEC, MILLISEC, TIMEZONE, } static Map<String, DATE_UNIT> DATE_FORMAT_LITERAL = new HashMap<>(); static { Object[][] dateFormatLiterals = new Object[][]{ {DATE_UNIT.CENTURY, "c,cent,centuries,century"}, {DATE_UNIT.DAY, "d,day,days"}, {DATE_UNIT.DECADE, "dec,decade,decades,decs"}, {DATE_UNIT.HOUR, "h,hour,hours,hr,hrs"}, {DATE_UNIT.MILLISEC, "millisecon,ms,msec,msecond,mseconds,msecs"}, {DATE_UNIT.MINUTE, "m,min,mins,minute,minutes"}, {DATE_UNIT.MONTH, "mon,mons,month,months"}, {DATE_UNIT.SECOND, "s,sec,second,seconds,secs"}, {DATE_UNIT.TIMEZONE, "timezone"}, {DATE_UNIT.MICROSEC, "microsecon,us,usec,microsecond,useconds,usecs"}, {DATE_UNIT.YEAR, "y,year,years,yr,yrs"} }; for (Object[] eachLiteral: dateFormatLiterals) { String[] tokens = ((String)eachLiteral[1]).split(","); DATE_UNIT unit = (DATE_UNIT)eachLiteral[0]; for (String eachToken: tokens) { DATE_FORMAT_LITERAL.put(eachToken, unit); } } } final int months; final long milliseconds; public IntervalDatum(long milliseconds) { this(0, milliseconds); } public IntervalDatum(int months, long milliseconds) { super(Interval); this.months = months; this.milliseconds = milliseconds; } public IntervalDatum(String intervalStr) { super(Interval); intervalStr = intervalStr.trim(); if (intervalStr.isEmpty()) { throw new InvalidOperationException("interval expression is empty"); } try { int year = 0; int month = 0; int day = 0; int hour = 0; int minute = 0; int second = 0; int microsecond = 0; int millisecond = 0; long time = 0; int length = intervalStr.getBytes().length; StringBuilder digitChars = new StringBuilder(); StringBuilder unitChars = new StringBuilder(); for (int i = 0; i < length; i++) { char c = intervalStr.charAt(i); if (Character.isDigit(c) || c == ':' || c == '.' || c == '-') { digitChars.append(c); } else if (c == ' ') { if (digitChars.length() > 0) { if (unitChars.length() > 0) { String unitName = unitChars.toString(); DATE_UNIT foundUnit = DATE_FORMAT_LITERAL.get(unitName); if (foundUnit == null) { throw new InvalidOperationException("invalid input syntax for type interval: " + intervalStr); } int digit = Integer.parseInt(digitChars.toString()); switch(foundUnit) { case YEAR: year = digit; break; case MONTH: month = digit; break; case DAY: day = digit; break; case HOUR: hour = digit; break; case MINUTE: minute = digit; break; case SECOND: second = digit; break; case MICROSEC: microsecond = digit; break; case MILLISEC: millisecond = digit; break; default: throw new InvalidOperationException("Unknown datetime unit: " + foundUnit); } digitChars.setLength(0); unitChars.setLength(0); } else if (digitChars.indexOf(":") >= 0) { time = parseTime(digitChars.toString()); digitChars.setLength(0); } } } else { unitChars.append(c); } } if (digitChars.length() > 0) { if (unitChars.length() > 0) { String unitName = unitChars.toString(); DATE_UNIT foundUnit = DATE_FORMAT_LITERAL.get(unitName); if (foundUnit == null) { throw new InvalidOperationException("invalid input syntax for type interval: " + intervalStr); } int digit = Integer.parseInt(digitChars.toString()); switch(foundUnit) { case YEAR: year = digit; break; case MONTH: month = digit; break; case DAY: day = digit; break; case HOUR: hour = digit; break; case MINUTE: minute = digit; break; case SECOND: second = digit; break; case MICROSEC: microsecond = digit; break; case MILLISEC: millisecond = digit; break; default: throw new InvalidOperationException("Unknown datetime unit: " + foundUnit); } } else if (digitChars.indexOf(":") >= 0) { time = parseTime(digitChars.toString()); digitChars.setLength(0); } } if (time > 0 && (hour != 0 || minute != 0 || second != 0 || microsecond != 0 || millisecond != 0)) { throw new InvalidOperationException("invalid input syntax for type interval: " + intervalStr); } this.milliseconds = time + day * DAY_MILLIS + hour * HOUR_MILLIS + minute * 60 * 1000L + second * 1000L + microsecond * 100L + millisecond; this.months = year * 12 + month; } catch (InvalidOperationException e) { throw e; } catch (Throwable t) { throw new InvalidOperationException(t.getMessage() + ": " + intervalStr); } } public static long parseTime(String timeStr) { //parse HH:mm:ss.SSS int hour = 0; int minute = 0; int second = 0; int millisecond = 0; String[] timeTokens = timeStr.split(":"); if (timeTokens.length == 1) { //sec String[] secondTokens = timeTokens[0].split("\\."); if (secondTokens.length == 1) { second = Integer.parseInt(secondTokens[0]); } else if (secondTokens.length == 2) { millisecond = Integer.parseInt(secondTokens[1]); } else { throw new InvalidOperationException("invalid input syntax for type interval: " + timeStr); } } else { if (timeTokens.length > 3) { throw new InvalidOperationException("invalid input syntax for type interval: " + timeStr); } for(int i = 0; i < timeTokens.length - 1; i++) { if (i == 0) hour = Integer.parseInt(timeTokens[i]); if (i == 1) minute = Integer.parseInt(timeTokens[i]); } if (timeTokens.length == 3) { //sec String[] secondTokens = timeTokens[2].split("\\."); if (secondTokens.length == 1) { second = Integer.parseInt(secondTokens[0]); } else if (secondTokens.length == 2) { second = Integer.parseInt(secondTokens[0]); millisecond = Integer.parseInt(secondTokens[1]); } else { throw new InvalidOperationException("invalid input syntax for type interval: " + timeStr); } } } return hour * HOUR_MILLIS+ minute * MINUTE_MILLIS + second * 1000 + millisecond; } public int getMonths() { return this.months; } public long getMilliSeconds() { return milliseconds; } @Override public Datum plus(Datum datum) { switch(datum.kind()) { case INTERVAL: IntervalDatum other = (IntervalDatum) datum; return new IntervalDatum(months + other.months, milliseconds + other.milliseconds); case DATE: { DateDatum dateDatum = (DateDatum) datum; TimeMeta tm = dateDatum.asTimeMeta(); tm.plusInterval(months, milliseconds); return new TimestampDatum(DateTimeUtil.toJulianTimestamp(tm)); } case TIME: { TimeMeta tm = datum.asTimeMeta(); tm.plusInterval(months, milliseconds); return new TimeDatum(DateTimeUtil.toTime(tm)); } case TIMESTAMP: { TimeMeta tm = new TimeMeta(); DateTimeUtil.toJulianTimeMeta(datum.asInt8(), tm); tm.plusInterval(months, milliseconds); return new TimestampDatum(DateTimeUtil.toJulianTimestamp(tm)); } default: throw new InvalidOperationException("operator does not exist: " + type() + " + " + datum.type()); } } @Override public Datum minus(Datum datum) { if (datum.kind() == TajoDataTypes.Type.INTERVAL) { IntervalDatum other = (IntervalDatum) datum; return new IntervalDatum(months - other.months, milliseconds - other.milliseconds); } else { throw new InvalidOperationException("operator does not exist: " + type() + " - " + datum.type()); } } @Override public Datum multiply(Datum datum) { switch(datum.kind()) { case INT2: case INT4: case INT8: long int8Val = datum.asInt8(); return createIntervalDatum((double)months * int8Val, (double) milliseconds * int8Val); case FLOAT4: case FLOAT8: double float8Val = datum.asFloat8(); return createIntervalDatum((double)months * float8Val, (double) milliseconds * float8Val); default: throw new InvalidOperationException("operator does not exist: " + type() + " * " + datum.type()); } } @Override public Datum divide(Datum datum) { switch (datum.kind()) { case INT2: case INT4: case INT8: long paramValueI8 = datum.asInt8(); if (!validateDivideZero(paramValueI8)) { return NullDatum.get(); } return createIntervalDatum((double) months / paramValueI8, (double) milliseconds / paramValueI8); case FLOAT4: case FLOAT8: double paramValueF8 = datum.asFloat8(); if (!validateDivideZero(paramValueF8)) { return NullDatum.get(); } return createIntervalDatum((double) months / paramValueF8, (double) milliseconds / paramValueF8); default: throw new InvalidOperationException("operator does not exist: " + type() + " / " + datum.type()); } } private IntervalDatum createIntervalDatum(double monthValue, double millisValue) { int month = (int)(monthValue); return new IntervalDatum(month, Math.round((monthValue - (double)month) * (double)MONTH_MILLIS + millisValue)); } @Override public long asInt8() { return (months * 30) * DAY_MILLIS + milliseconds; } @Override public String toString() { return asChars(); } static DecimalFormat df = new DecimalFormat("00"); static DecimalFormat df2 = new DecimalFormat("000"); @Override public String asChars() { try { StringBuilder sb = new StringBuilder(); String prefix = ""; if (months != 0) { int positiveNum = Math.abs(months); int year = positiveNum / 12; int remainMonth = positiveNum - year * 12; if (year > 0) { sb.append(months < 0 ? "-" : ""); sb.append(year).append(year == 1 ? " year" : " years"); prefix = " "; } sb.append(prefix).append(months < 0 ? "-" : "").append(remainMonth).append(months == 1 ? " month" : " months"); prefix = " "; } formatMillis(sb, prefix, milliseconds); return sb.toString(); } catch (Exception e) { return ""; } } public static String formatMillis(long millis) { StringBuilder sb = new StringBuilder(); formatMillis(sb, "", millis); return sb.toString(); } public static void formatMillis(StringBuilder sb, String prefix, long millis) { if (millis != 0) { long positiveNum = Math.abs(millis); int days = (int)(positiveNum / DAY_MILLIS); long remainInterval = positiveNum - days * DAY_MILLIS; if(days != 0) { sb.append(prefix).append(millis < 0 ? "-" : "").append(days).append(days == 1 ? " day" : " days"); prefix = " "; } if (remainInterval != 0) { int hour = (int) (remainInterval / HOUR_MILLIS); int minutes = (int) (remainInterval - hour * HOUR_MILLIS) / (60 * 1000); long sec = (int) (remainInterval - hour * HOUR_MILLIS - minutes * (60 * 1000L)) / 1000L; long millisecond = (int) (remainInterval - hour * HOUR_MILLIS - minutes * (60 * 1000L) - sec * 1000L); sb.append(prefix) .append(millis < 0 ? "-" : "") .append(df.format(hour)) .append(":") .append(df.format(minutes)) .append(":") .append(df.format(sec)); if (millisecond > 0) { sb.append(".").append(df2.format(millisecond)); } } } else { sb.append(prefix) .append(df.format(0)) .append(":") .append(df.format(0)) .append(":") .append(df.format(0)); } } @Override public int size() { return 0; } @Override public int compareTo(Datum datum) { if (datum.kind() == TajoDataTypes.Type.INTERVAL) { return Longs.compare(asInt8(), datum.asInt8()); } else if (datum instanceof NullDatum || datum.isNull()) { return -1; } else { throw new InvalidOperationException(datum.type()); } } @Override public Datum equalsTo(Datum datum) { if (datum.kind() == TajoDataTypes.Type.INTERVAL) { return DatumFactory.createBool(asInt8() == datum.asInt8()); } else if (datum.isNull()) { return datum; } else { throw new InvalidOperationException(datum.type()); } } @Override public boolean equals(Object obj) { if (obj instanceof IntervalDatum) { return asInt8() == ((IntervalDatum)obj).asInt8(); } else { return false; } } @Override public int hashCode(){ return Longs.hashCode(asInt8()); } }