| /** |
| * 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; |
| |
| 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(TajoDataTypes.Type.INTERVAL); |
| this.months = months; |
| this.milliseconds = milliseconds; |
| } |
| |
| public IntervalDatum(String intervalStr) { |
| super(TajoDataTypes.Type.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.type()) { |
| 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.type() == 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.type()) { |
| 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.type()) { |
| 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.type() == 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.type() == 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()); |
| } |
| } |