/**
* Copyright (C) 2009-2013 FoundationDB, LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.foundationdb.server.types.mcompat.mfuncs;
import com.foundationdb.server.error.AkibanInternalException;
import com.foundationdb.server.error.InvalidDateFormatException;
import com.foundationdb.server.types.LazyList;
import com.foundationdb.server.types.TClass;
import com.foundationdb.server.types.TCustomOverloadResult;
import com.foundationdb.server.types.TExecutionContext;
import com.foundationdb.server.types.TInstance;
import com.foundationdb.server.types.TInstanceGenerator;
import com.foundationdb.server.types.TScalar;
import com.foundationdb.server.types.TOverloadResult;
import com.foundationdb.server.types.TPreptimeContext;
import com.foundationdb.server.types.TPreptimeValue;
import com.foundationdb.server.types.aksql.aktypes.AkInterval;
import com.foundationdb.server.types.mcompat.mtypes.MApproximateNumber;
import com.foundationdb.server.types.mcompat.mtypes.MDateAndTime;
import com.foundationdb.server.types.mcompat.mtypes.MDateAndTime.StringType;
import com.foundationdb.server.types.mcompat.mtypes.MDateAndTime.ZeroFlag;
import com.foundationdb.server.types.mcompat.mtypes.MString;
import com.foundationdb.server.types.value.ValueSource;
import com.foundationdb.server.types.value.ValueTarget;
import com.foundationdb.server.types.texpressions.TInputSetBuilder;
import com.foundationdb.server.types.texpressions.TScalarBase;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.joda.time.MutableDateTime;
public class MDateAddSub extends TScalarBase
{
public static final TScalar[] COMMUTATIVE = new TScalar[]
{
//ADDDATE
new MDateAddSub(Helper.DO_ADD, FirstType.DATE, SecondType.DAY, "ADDDATE"),
new MDateAddSub(Helper.DO_ADD, FirstType.DATETIME, SecondType.DAY, "ADDDATE"),
new MDateAddSub(Helper.DO_ADD, FirstType.TIMESTAMP, SecondType.DAY, "ADDDATE"),
new AddSubWithVarchar(Helper.DO_ADD, SecondType.DAY, "ADDDATE"),
new MDateAddSub(Helper.DO_ADD, FirstType.DATE, SecondType.INTERVAL_MILLIS, "DATE_ADD", "ADDDATE", "plus"),
new MDateAddSub(Helper.DO_ADD_MONTH, FirstType.DATE, SecondType.INTERVAL_MONTH, "DATE_ADD", "ADDDATE", "plus"),
new MDateAddSub(Helper.DO_ADD, FirstType.DATETIME, SecondType.INTERVAL_MILLIS, "DATE_ADD", "ADDDATE", "plus"),
new MDateAddSub(Helper.DO_ADD_MONTH, FirstType.DATETIME, SecondType.INTERVAL_MONTH, "DATE_ADD", "ADDDATE", "plus"),
new MDateAddSub(Helper.DO_ADD, FirstType.TIMESTAMP, SecondType.INTERVAL_MILLIS, "DATE_ADD", "ADDDATE", "plus"),
new MDateAddSub(Helper.DO_ADD_MONTH, FirstType.TIMESTAMP, SecondType.INTERVAL_MONTH, "DATE_ADD", "ADDDATE", "plus"),
new AddSubWithVarchar(Helper.DO_ADD, SecondType.INTERVAL_MILLIS, "DATE_ADD", "ADDDATE", "plus"),
new AddSubWithVarchar(Helper.DO_ADD_MONTH, SecondType.INTERVAL_MONTH, "DATE_ADD", "ADDDATE", "plus"),
// SUBDATE
new MDateAddSub(Helper.DO_SUB, FirstType.DATE, SecondType.DAY, "SUBDATE"),
new MDateAddSub(Helper.DO_SUB, FirstType.DATETIME, SecondType.DAY, "SUBDATE"),
new MDateAddSub(Helper.DO_SUB, FirstType.TIMESTAMP, SecondType.DAY, "SUBDATE"),
new AddSubWithVarchar(Helper.DO_SUB, SecondType.DAY, "SUBDATE"),
new MDateAddSub(Helper.DO_SUB, FirstType.DATE, SecondType.INTERVAL_MILLIS, "DATE_SUB", "SUBDATE", "minus"),
new MDateAddSub(Helper.DO_SUB_MONTH, FirstType.DATE, SecondType.INTERVAL_MONTH, "DATE_SUB", "SUBDATE", "minus"),
new MDateAddSub(Helper.DO_SUB, FirstType.DATETIME, SecondType.INTERVAL_MILLIS, "DATE_SUB", "SUBDATE", "minus"),
new MDateAddSub(Helper.DO_SUB_MONTH, FirstType.DATETIME, SecondType.INTERVAL_MONTH, "DATE_SUB", "SUBDATE", "minus"),
new MDateAddSub(Helper.DO_SUB, FirstType.TIMESTAMP, SecondType.INTERVAL_MILLIS, "DATE_SUB", "SUBDATE", "minus"),
new MDateAddSub(Helper.DO_SUB_MONTH, FirstType.TIMESTAMP, SecondType.INTERVAL_MONTH, "DATE_SUB", "SUBDATE", "minus"),
new AddSubWithVarchar(Helper.DO_SUB, SecondType.INTERVAL_MILLIS, "DATE_SUB", "SUBDATE", "minus"),
new AddSubWithVarchar(Helper.DO_SUB_MONTH, SecondType.INTERVAL_MONTH, "DATE_SUB", "SUBDATE", "minus"),
// ADDTIME
new MDateAddSub(Helper.DO_ADD, FirstType.TIME, SecondType.SECOND, "TIME_ADD", "ADDTIME"),
new AddSubWithVarchar(Helper.DO_ADD, SecondType.SECOND, "TIME_ADD", "ADDTIME"),
new MDateAddSub(Helper.DO_ADD, FirstType.TIME, SecondType.INTERVAL_MILLIS, "addtime"),
new MDateAddSub(Helper.DO_ADD, FirstType.TIME, SecondType.TIME, "TIME_ADD", "ADDTIME"),
new AddSubWithVarchar(Helper.DO_ADD, SecondType.TIME, "TIME_ADD", "ADDTIME"),
new AddSubWithVarchar(Helper.DO_ADD, SecondType.TIME_STRING, "ADDTIME"),
// SUBTIME
new MDateAddSub(Helper.DO_SUB, FirstType.TIME, SecondType.SECOND, "SUBTIME"),
new AddSubWithVarchar(Helper.DO_SUB, SecondType.SECOND, "SUBTIME"),
new MDateAddSub(Helper.DO_SUB, FirstType.TIME, SecondType.TIME, "SUBTIME"),
new AddSubWithVarchar(Helper.DO_SUB, SecondType.TIME, "SUBTIME"),
new AddSubWithVarchar(Helper.DO_ADD, SecondType.TIME_STRING, "SUBTIME"),
// additional date/time ariths
new AddSubWithVarchar(Helper.DO_ADD, SecondType.INTERVAL_MILLIS, 1, 0, "plus"),
new AddSubWithVarchar(Helper.DO_ADD_MONTH, SecondType.INTERVAL_MONTH, 1, 0, "plus"),
new MDateAddSub(Helper.DO_ADD, 1, 0, FirstType.DATE, SecondType.INTERVAL_MILLIS, "plus"),
new MDateAddSub(Helper.DO_ADD_MONTH, 1, 0, FirstType.DATE, SecondType.INTERVAL_MONTH, "plus"),
new MDateAddSub(Helper.DO_ADD, 1, 0, FirstType.DATETIME, SecondType.INTERVAL_MILLIS, "plus"),
new MDateAddSub(Helper.DO_ADD_MONTH, 1, 0, FirstType.DATETIME, SecondType.INTERVAL_MONTH, "plus"),
new MDateAddSub(Helper.DO_ADD, 1, 0, FirstType.TIMESTAMP, SecondType.INTERVAL_MILLIS, "plus"),
new MDateAddSub(Helper.DO_ADD_MONTH, 1, 0, FirstType.TIMESTAMP, SecondType.INTERVAL_MONTH, "plus"),
// Special case, <TIME> plus/minus <INTERVAL> ===> <DATE/DATETIME> plus <INTERVAL>
new MDateAddSub(Helper.DO_ADD, 1, 0, FirstType.TIME_TO_DATE, SecondType.INTERVAL_MILLIS, "plus"),
new MDateAddSub(Helper.DO_ADD, FirstType.TIME_TO_DATE, SecondType.INTERVAL_MILLIS, "plus"),
new MDateAddSub(Helper.DO_ADD, 1, 0, FirstType.TIME_TO_DATE, SecondType.INTERVAL_MONTH, "plus"),
new MDateAddSub(Helper.DO_ADD, 0, 1, FirstType.TIME_TO_DATE, SecondType.INTERVAL_MONTH, "plus"),
new MDateAddSub(Helper.DO_SUB, FirstType.TIME_TO_DATE, SecondType.INTERVAL_MILLIS, "minus"),
new MDateAddSub(Helper.DO_SUB, FirstType.TIME_TO_DATE, SecondType.INTERVAL_MONTH, "minus"),
};
private static class AddSubWithVarchar extends MDateAddSub
{
AddSubWithVarchar (Helper h, SecondType sec, String...ns)
{
super(h, 0, 1, FirstType.VARCHAR, sec, ns);
}
AddSubWithVarchar (Helper h, SecondType sec, int pos0, int pos1, String...ns)
{
super(h, pos0, pos1, FirstType.VARCHAR, sec, ns);
}
@Override
public TOverloadResult resultType()
{
return TOverloadResult.fixed(MString.VARCHAR, 29);
}
@Override
protected void doEvaluate(TExecutionContext context, LazyList<? extends ValueSource> inputs, ValueTarget output)
{
long ymd[] = new long[6];
StringType stType;
long millis;
String arg0 = inputs.get(pos0).getString();
try
{
stType = MDateAndTime.parseDateOrTime(arg0, ymd);
if (!MDateAndTime.isValidType(stType))
{
context.warnClient(new InvalidDateFormatException(stType.name(), arg0));
output.putNull();
return;
}
millis = secondArg.toMillis(inputs.get(pos1));
}
catch (InvalidDateFormatException e)
{
context.warnClient(e);
output.putNull();
return;
}
MutableDateTime dt;
switch (stType)
{
case DATE_ST:
dt = MDateAndTime.toJodaDateTime(ymd, "UTC");
helper.compute(dt, millis);
if (FirstType.DATE.adjustFirstArg(inputs.get(1).getType()) == FirstType.DATE)
output.putString(dt.toString("YYYY-MM-dd"), null);
else
output.putString(dt.toString("YYYY-MM-dd HH:mm:ss"), null);
break;
case DATETIME_ST:
dt = MDateAndTime.toJodaDateTime(ymd, context.getCurrentTimezone());
helper.compute(dt, millis);
output.putString(dt.toString("YYYY-MM-dd HH:mm:ss"), null);
break;
case TIME_ST:
long arg0Millis = timeToMillis(ymd);
long ret = helper == Helper.DO_ADD ? arg0Millis + millis: arg0Millis - millis;
int sign = 1;
if (ret < 0)
ret *= (sign = -1);
// turn millis back to hour-min-sec
long seconds = ret / 1000L;
long hours = seconds / 3600;
long minutes = (seconds - hours * 3600) / 60;
seconds -= (minutes * 60 + hours * 3600);
hours *= sign;
output.putString(String.format("%02d:%02d:%02d",
hours, minutes, seconds),
null);
break;
default:
throw new AkibanInternalException("unexpected argument: " + stType);
}
}
}
private static enum Helper
{
DO_ADD_MONTH
{
@Override
protected void compute(MutableDateTime date, long delta)
{
date.addMonths((int)delta);
}
},
DO_SUB_MONTH
{
@Override
protected void compute(MutableDateTime date, long delta)
{
date.addMonths(-(int)delta);
}
},
DO_ADD
{
@Override
protected void compute(MutableDateTime date, long delta)
{
date.add(delta);
}
},
DO_SUB
{
@Override
protected void compute(MutableDateTime date, long delta)
{
date.add(-delta);
}
};
abstract protected void compute(MutableDateTime date, long delta);
}
private static enum FirstType
{
TIME_TO_DATE(MDateAndTime.TIME)
{
@Override
FirstType adjustFirstArg(TInstance ins)
{
if (ins != null
&& ins.typeClass() instanceof AkInterval
&& ((AkInterval)ins.typeClass()).isTime(ins))
return FirstType.DATETIME_STR;
else
return FirstType.DATE_STR;
}
@Override
long[] decode(ValueSource val, TExecutionContext context)
{
long ret[] = MDateAndTime.decodeTime(val.getInt32());
MDateAndTime.timeToDatetime(ret);
return MDateAndTime.isValidDateTime(ret, ZeroFlag.YEAR) ? ret : null;
}
@Override
protected void putResult(ValueTarget out, MutableDateTime par3, TExecutionContext context)
{
out.putInt32(MDateAndTime.encodeDate(MDateAndTime.fromJodaDateTime(par3)));
}
},
DATE_STR(MString.VARCHAR, 29)
{
@Override
long[] decode(ValueSource val, TExecutionContext context)
{
return FirstType.TIME_TO_DATE.decode(val, context);
}
@Override
protected void putResult(ValueTarget out, MutableDateTime par3, TExecutionContext context)
{
out.putString(par3.toString("YYYY-MM-dd"), null);
}
},
DATETIME_STR(MString.VARCHAR, 29)
{
@Override
long[] decode(ValueSource val, TExecutionContext context)
{
return FirstType.TIME_TO_DATE.decode(val, context);
}
@Override
protected void putResult(ValueTarget out, MutableDateTime par3, TExecutionContext context)
{
out.putString(par3.toString("YYYY-MM-dd HH:mm:ss"), null);
}
},
VARCHAR(MString.VARCHAR, 29)
{
@Override
long[] decode(ValueSource val, TExecutionContext context)
{
throw new AkibanInternalException("shouldn't have been used");
}
@Override
protected void putResult(ValueTarget out, MutableDateTime par3, TExecutionContext context)
{
throw new AkibanInternalException("shouldn't have been used");
}
},
DATE(MDateAndTime.DATE)
{
@Override
FirstType adjustFirstArg(TInstance ins)
{
if (ins != null
&& ins.typeClass() instanceof AkInterval
&& ((AkInterval)ins.typeClass()).isTime(ins))
return FirstType.DATETIME;
else
return this;
}
@Override
long[] decode(ValueSource val, TExecutionContext context)
{
long ret[] = MDateAndTime.decodeDate(val.getInt32());
return MDateAndTime.isValidDateTime(ret, ZeroFlag.YEAR) ? ret : null;
}
@Override
protected void putResult(ValueTarget out, MutableDateTime par3, TExecutionContext context)
{
out.putInt32(MDateAndTime.encodeDate(MDateAndTime.fromJodaDateTime(par3)));
}
},
TIME(MDateAndTime.TIME)
{
@Override
long[] decode(ValueSource val, TExecutionContext context)
{
long ret[] = MDateAndTime.decodeTime(val.getInt32());
return MDateAndTime.isValidHrMinSec(ret, false, false) ? ret : null;
}
@Override
protected void putResult(ValueTarget out, MutableDateTime par3, TExecutionContext context)
{
out.putInt32(MDateAndTime.encodeTime(MDateAndTime.fromJodaDateTime(par3), context));
}
},
DATETIME(MDateAndTime.DATETIME)
{
@Override
long[] decode(ValueSource val, TExecutionContext context)
{
long ret[] = MDateAndTime.decodeDateTime(val.getInt64());
return MDateAndTime.isValidDateTime(ret, ZeroFlag.YEAR) ? ret : null;
}
@Override
protected void putResult(ValueTarget out, MutableDateTime par3, TExecutionContext context)
{
out.putInt64(MDateAndTime.encodeDateTime(MDateAndTime.fromJodaDateTime(par3)));
}
},
TIMESTAMP(MDateAndTime.TIMESTAMP)
{
@Override
long[] decode(ValueSource val, TExecutionContext context)
{
return MDateAndTime.decodeTimestamp(val.getInt32(), context.getCurrentTimezone());
}
@Override
protected void putResult(ValueTarget out, MutableDateTime par3, TExecutionContext context)
{
out.putInt32((int)MDateAndTime.encodeTimestamp(par3.getMillis(), context));
}
};
FirstType(TClass t, int... attrs)
{
this.type = t;
this.attrs = attrs;
}
abstract long[] decode (ValueSource val, TExecutionContext context);
protected abstract void putResult(ValueTarget out, MutableDateTime par3, TExecutionContext context);
FirstType adjustFirstArg(TInstance ins) // to be overriden in DATE
{
// only needs adjusting if <first arg> is DATE
return this;
}
private final TClass type;
private final int[] attrs;
}
private static enum SecondType
{
INTERVAL_MILLIS(AkInterval.SECONDS)
{
@Override
protected long toMillis(ValueSource arg)
{
return AkInterval.secondsIntervalAs(arg, TimeUnit.MILLISECONDS);
}
},
INTERVAL_MONTH(AkInterval.MONTHS)
{
@Override
protected long toMillis(ValueSource arg)
{
// this return the number of months, not millis
return arg.getInt64();
}
},
TIME(MDateAndTime.TIME)
{
@Override
protected long toMillis(ValueSource arg)
{
int val = arg.getInt32();
long hms[] = MDateAndTime.decodeTime(val);
return timeToMillis(hms);
}
},
TIME_STRING(MString.VARCHAR)
{
@Override
protected long toMillis(ValueSource arg)
{
String st = arg.getString();
long hms[] = new long[6];
StringType stType = MDateAndTime.parseDateOrTime(st, hms);
switch(stType)
{
case TIME_ST:
return timeToMillis(hms);
default:
throw new InvalidDateFormatException("TIME", st);
}
}
},
SECOND(MApproximateNumber.DOUBLE)
{
@Override
protected long toMillis(ValueSource arg)
{
return Math.round(arg.getDouble()) * 1000L;
}
},
DAY(MApproximateNumber.DOUBLE)
{
@Override
protected long toMillis(ValueSource arg)
{
return Math.round(arg.getDouble()) * MILLS_PER_DAY;
}
};
private SecondType (TClass t)
{
type = t;
}
protected abstract long toMillis(ValueSource arg);
TClass type;
private static final long MILLS_PER_DAY = 24 * 3600 * 1000;
}
protected final Helper helper;
protected final FirstType firstArg;
protected final SecondType secondArg;
protected final String names[];
protected final int pos0;
protected final int pos1;
private MDateAddSub(Helper h, FirstType first, SecondType sec, String...ns)
{
this(h, 0, 1, first, sec, ns);
}
private MDateAddSub(Helper h, int pos0, int pos1, FirstType first, SecondType sec, String...ns)
{
helper = h;
firstArg = first;
secondArg = sec;
names = ns;
this.pos0 = pos0;
this.pos1 = pos1;
}
@Override
protected void doEvaluate(TExecutionContext context, LazyList<? extends ValueSource> inputs, ValueTarget output)
{
ValueSource arg0 = inputs.get(pos0);
long ymd[] = firstArg.decode(arg0, context);
if (ymd == null)
{
output.putNull();
context.warnClient(new InvalidDateFormatException("DATE", arg0.toString()));
}
else
{
MutableDateTime dt = MDateAndTime.toJodaDateTime(ymd, "UTC"); // calculations should be done
helper.compute(dt, secondArg.toMillis(inputs.get(pos1))); // in UTC (to avoid daylight-saving side effects)
firstArg.adjustFirstArg(inputs.get(pos1).getType()).putResult(output, dt, context);
}
}
@Override
protected void buildInputSets(TInputSetBuilder builder)
{
builder.covers(firstArg.type, pos0).covers(secondArg.type, pos1);
}
@Override
public String displayName()
{
return names[0];
}
@Override
public String[] registeredNames()
{
return names;
}
@Override
public TOverloadResult resultType()
{
return TOverloadResult.custom(new TCustomOverloadResult()
{
@Override
public TInstance resultInstance(List<TPreptimeValue> inputs, TPreptimeContext context)
{
FirstType adjusted = firstArg.adjustFirstArg(inputs.get(pos1).type());
return new TInstanceGenerator(adjusted.type, adjusted.attrs).setNullable(anyContaminatingNulls(inputs));
}
});
}
static long timeToMillis(long ymd[])
{
int sign = 1;
if (ymd[MDateAndTime.HOUR_INDEX] < 0)
ymd[MDateAndTime.HOUR_INDEX] *= sign = -1;
return sign * (ymd[MDateAndTime.HOUR_INDEX] * 3600000
+ ymd[MDateAndTime.MIN_INDEX] * 60000
+ ymd[MDateAndTime.SEC_INDEX] * 1000);
}
}