CC-Tweaked/src/main/java/dan200/computercraft/core/apis/LuaDateTime.java

281 lines
11 KiB
Java

/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.apis;
import dan200.computercraft.api.lua.LuaException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.TextStyle;
import java.time.temporal.*;
import java.util.HashMap;
import java.util.Map;
import java.util.function.LongUnaryOperator;
final class LuaDateTime
{
private LuaDateTime()
{
}
static void format( DateTimeFormatterBuilder formatter, String format, ZoneOffset offset ) throws LuaException
{
for( int i = 0; i < format.length(); )
{
char c;
switch( c = format.charAt( i++ ) )
{
case '\n':
formatter.appendLiteral( '\n' );
break;
default:
formatter.appendLiteral( c );
break;
case '%':
if( i >= format.length() ) break;
switch( c = format.charAt( i++ ) )
{
default:
throw new LuaException( "bad argument #1: invalid conversion specifier '%" + c + "'" );
case '%':
formatter.appendLiteral( '%' );
break;
case 'a':
formatter.appendText( ChronoField.DAY_OF_WEEK, TextStyle.SHORT );
break;
case 'A':
formatter.appendText( ChronoField.DAY_OF_WEEK, TextStyle.FULL );
break;
case 'b':
case 'h':
formatter.appendText( ChronoField.MONTH_OF_YEAR, TextStyle.SHORT );
break;
case 'B':
formatter.appendText( ChronoField.MONTH_OF_YEAR, TextStyle.FULL );
break;
case 'c':
format( formatter, "%a %b %e %H:%M:%S %Y", offset );
break;
case 'C':
formatter.appendValueReduced( CENTURY, 2, 2, 0 );
break;
case 'd':
formatter.appendValue( ChronoField.DAY_OF_MONTH, 2 );
break;
case 'D':
case 'x':
format( formatter, "%m/%d/%y", offset );
break;
case 'e':
formatter.padNext( 2 ).appendValue( ChronoField.DAY_OF_MONTH );
break;
case 'F':
format( formatter, "%Y-%m-%d", offset );
break;
case 'g':
formatter.appendValueReduced( IsoFields.WEEK_BASED_YEAR, 2, 2, 0 );
break;
case 'G':
formatter.appendValue( IsoFields.WEEK_BASED_YEAR );
break;
case 'H':
formatter.appendValue( ChronoField.HOUR_OF_DAY, 2 );
break;
case 'I':
formatter.appendValue( ChronoField.HOUR_OF_AMPM );
break;
case 'j':
formatter.appendValue( ChronoField.DAY_OF_YEAR, 3 );
break;
case 'm':
formatter.appendValue( ChronoField.MONTH_OF_YEAR, 2 );
break;
case 'M':
formatter.appendValue( ChronoField.MINUTE_OF_HOUR, 2 );
break;
case 'n':
formatter.appendLiteral( '\n' );
break;
case 'p':
formatter.appendText( ChronoField.AMPM_OF_DAY );
break;
case 'r':
format( formatter, "%I:%M:%S %p", offset );
break;
case 'R':
format( formatter, "%H:%M", offset );
break;
case 'S':
formatter.appendValue( ChronoField.SECOND_OF_MINUTE, 2 );
break;
case 't':
formatter.appendLiteral( '\t' );
break;
case 'T':
case 'X':
format( formatter, "%H:%M:%S", offset );
break;
case 'u':
formatter.appendValue( ChronoField.DAY_OF_WEEK );
break;
case 'U':
formatter.appendValue( ChronoField.ALIGNED_WEEK_OF_YEAR, 2 );
break;
case 'V':
formatter.appendValue( IsoFields.WEEK_OF_WEEK_BASED_YEAR, 2 );
break;
case 'w':
formatter.appendValue( ZERO_WEEK );
break;
case 'W':
formatter.appendValue( WeekFields.ISO.weekOfYear(), 2 );
break;
case 'y':
formatter.appendValueReduced( ChronoField.YEAR, 2, 2, 0 );
break;
case 'Y':
formatter.appendValue( ChronoField.YEAR );
break;
case 'z':
formatter.appendOffset( "+HHMM", "+0000" );
break;
case 'Z':
formatter.appendChronologyId();
break;
}
}
}
}
static long fromTable( Map<?, ?> table ) throws LuaException
{
int year = getField( table, "year", -1 );
int month = getField( table, "month", -1 );
int day = getField( table, "day", -1 );
int hour = getField( table, "hour", 12 );
int minute = getField( table, "min", 12 );
int second = getField( table, "sec", 12 );
LocalDateTime time = LocalDateTime.of( year, month, day, hour, minute, second );
Boolean isDst = getBoolField( table, "isdst" );
if( isDst != null )
{
boolean requireDst = isDst;
for( ZoneOffset possibleOffset : ZoneOffset.systemDefault().getRules().getValidOffsets( time ) )
{
Instant instant = time.toInstant( possibleOffset );
if( possibleOffset.getRules().getDaylightSavings( instant ).isZero() == requireDst )
{
return instant.getEpochSecond();
}
}
}
ZoneOffset offset = ZoneOffset.systemDefault().getRules().getOffset( time );
return time.toInstant( offset ).getEpochSecond();
}
static Map<String, ?> toTable( TemporalAccessor date, ZoneId offset, Instant instant )
{
HashMap<String, Object> table = new HashMap<>( 9 );
table.put( "year", date.getLong( ChronoField.YEAR ) );
table.put( "month", date.getLong( ChronoField.MONTH_OF_YEAR ) );
table.put( "day", date.getLong( ChronoField.DAY_OF_MONTH ) );
table.put( "hour", date.getLong( ChronoField.HOUR_OF_DAY ) );
table.put( "min", date.getLong( ChronoField.MINUTE_OF_HOUR ) );
table.put( "sec", date.getLong( ChronoField.SECOND_OF_MINUTE ) );
table.put( "wday", date.getLong( WeekFields.SUNDAY_START.dayOfWeek() ) );
table.put( "yday", date.getLong( ChronoField.DAY_OF_YEAR ) );
table.put( "isdst", offset.getRules().isDaylightSavings( instant ) );
return table;
}
private static int getField( Map<?, ?> table, String field, int def ) throws LuaException
{
Object value = table.get( field );
if( value instanceof Number ) return ((Number) value).intValue();
if( def < 0 ) throw new LuaException( "field \"" + field + "\" missing in date table" );
return def;
}
private static Boolean getBoolField( Map<?, ?> table, String field ) throws LuaException
{
Object value = table.get( field );
if( value instanceof Boolean || value == null ) return (Boolean) value;
throw new LuaException( "field \"" + field + "\" missing in date table" );
}
private static final TemporalField CENTURY = map( ChronoField.YEAR, ValueRange.of( 0, 6 ), x -> (x / 100) % 100 );
private static final TemporalField ZERO_WEEK = map( WeekFields.SUNDAY_START.dayOfWeek(), ValueRange.of( 0, 6 ), x -> x - 1 );
private static TemporalField map( TemporalField field, ValueRange range, LongUnaryOperator convert )
{
return new TemporalField()
{
private final ValueRange range = ValueRange.of( 0, 99 );
@Override
public TemporalUnit getBaseUnit()
{
return field.getBaseUnit();
}
@Override
public TemporalUnit getRangeUnit()
{
return field.getRangeUnit();
}
@Override
public ValueRange range()
{
return range;
}
@Override
public boolean isDateBased()
{
return field.isDateBased();
}
@Override
public boolean isTimeBased()
{
return field.isTimeBased();
}
@Override
public boolean isSupportedBy( TemporalAccessor temporal )
{
return field.isSupportedBy( temporal );
}
@Override
public ValueRange rangeRefinedBy( TemporalAccessor temporal )
{
return range;
}
@Override
public long getFrom( TemporalAccessor temporal )
{
return convert.applyAsLong( temporal.getLong( field ) );
}
@Override
@SuppressWarnings( "unchecked" )
public <R extends Temporal> R adjustInto( R temporal, long newValue )
{
return (R) temporal.with( field, newValue );
}
};
}
}