mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2024-12-13 03:30:29 +00:00
Add mostly standard compliant os.time/os.date
- os.time, when given a table, will act the same as PUC Lua - returning the seconds since the epoch. We preserve the previous string/nil behaviour though - os.epoch("local") is equivalent to PUC's os.time(). - os.date will now act accept a string and (optional) time, returning an appropriate table. Somewhat resolves the madness which was dan200/ComputerCraft#183, and hopefully (though probably not) makes @Vexatos happy.
This commit is contained in:
parent
d661cfa88b
commit
210f3fa9e2
281
src/main/java/dan200/computercraft/core/apis/LuaDateTime.java
Normal file
281
src/main/java/dan200/computercraft/core/apis/LuaDateTime.java
Normal file
@ -0,0 +1,281 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2019. 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 '%" + (char) 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 );
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -12,6 +12,11 @@ import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.shared.util.StringUtil;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatterBuilder;
|
||||
import java.util.*;
|
||||
|
||||
import static dan200.computercraft.core.apis.ArgumentHelper.*;
|
||||
@ -184,11 +189,12 @@ public class OSAPI implements ILuaAPI
|
||||
"day",
|
||||
"cancelTimer",
|
||||
"cancelAlarm",
|
||||
"epoch"
|
||||
"epoch",
|
||||
"date",
|
||||
};
|
||||
}
|
||||
|
||||
private float getTimeForCalendar( Calendar c )
|
||||
private static float getTimeForCalendar( Calendar c )
|
||||
{
|
||||
float time = c.get( Calendar.HOUR_OF_DAY );
|
||||
time += c.get( Calendar.MINUTE ) / 60.0f;
|
||||
@ -196,7 +202,7 @@ public class OSAPI implements ILuaAPI
|
||||
return time;
|
||||
}
|
||||
|
||||
private int getDayForCalendar( Calendar c )
|
||||
private static int getDayForCalendar( Calendar c )
|
||||
{
|
||||
GregorianCalendar g = c instanceof GregorianCalendar ? (GregorianCalendar) c : new GregorianCalendar();
|
||||
int year = c.get( Calendar.YEAR );
|
||||
@ -209,7 +215,7 @@ public class OSAPI implements ILuaAPI
|
||||
return day;
|
||||
}
|
||||
|
||||
private long getEpochForCalendar( Calendar c )
|
||||
private static long getEpochForCalendar( Calendar c )
|
||||
{
|
||||
return c.getTime().getTime();
|
||||
}
|
||||
@ -282,6 +288,9 @@ public class OSAPI implements ILuaAPI
|
||||
case 11:
|
||||
{
|
||||
// time
|
||||
Object value = args.length > 0 ? args[0] : null;
|
||||
if( value instanceof Map ) return new Object[] { LuaDateTime.fromTable( (Map<?, ?>) value ) };
|
||||
|
||||
String param = optString( args, 0, "ingame" );
|
||||
switch( param.toLowerCase( Locale.ROOT ) )
|
||||
{
|
||||
@ -355,9 +364,8 @@ public class OSAPI implements ILuaAPI
|
||||
}
|
||||
return null;
|
||||
}
|
||||
case 15:
|
||||
case 15: // epoch
|
||||
{
|
||||
// epoch
|
||||
String param = optString( args, 0, "ingame" );
|
||||
switch( param.toLowerCase( Locale.ROOT ) )
|
||||
{
|
||||
@ -385,6 +393,34 @@ public class OSAPI implements ILuaAPI
|
||||
throw new LuaException( "Unsupported operation" );
|
||||
}
|
||||
}
|
||||
case 16: // date
|
||||
{
|
||||
String format = optString( args, 0, "%c" );
|
||||
long time = optLong( args, 1, Instant.now().getEpochSecond() );
|
||||
|
||||
Instant instant = Instant.ofEpochSecond( time );
|
||||
ZonedDateTime date;
|
||||
ZoneOffset offset;
|
||||
boolean isDst;
|
||||
if( format.startsWith( "!" ) )
|
||||
{
|
||||
offset = ZoneOffset.UTC;
|
||||
date = ZonedDateTime.ofInstant( instant, offset );
|
||||
format = format.substring( 1 );
|
||||
}
|
||||
else
|
||||
{
|
||||
ZoneId id = ZoneId.systemDefault();
|
||||
offset = id.getRules().getOffset( instant );
|
||||
date = ZonedDateTime.ofInstant( instant, id );
|
||||
}
|
||||
|
||||
if( format.equals( "*t" ) ) return new Object[] { LuaDateTime.toTable( date, offset, instant ) };
|
||||
|
||||
DateTimeFormatterBuilder formatter = new DateTimeFormatterBuilder();
|
||||
LuaDateTime.format( formatter, format, offset );
|
||||
return new Object[] { formatter.toFormatter( Locale.ROOT ).format( date ) };
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
121
src/test/resources/test-rom/spec/apis/os_spec.lua
Normal file
121
src/test/resources/test-rom/spec/apis/os_spec.lua
Normal file
@ -0,0 +1,121 @@
|
||||
describe("The os api", function()
|
||||
describe("os.date and os.time", function()
|
||||
it("round trips correctly", function()
|
||||
local t = math.floor(os.epoch("local") / 1000)
|
||||
local T = os.date("*t", t)
|
||||
|
||||
expect(os.time(T)):eq(t)
|
||||
end)
|
||||
|
||||
it("dst field is guessed", function()
|
||||
local T = os.date("*t")
|
||||
local t = os.time(T)
|
||||
expect(T.isdst):type("boolean")
|
||||
T.isdst = nil
|
||||
expect(os.time(T)):eq(t) -- if isdst is absent uses correct default
|
||||
end)
|
||||
|
||||
it("has 365 days in a year", function()
|
||||
local T = os.date("*t")
|
||||
local t = os.time(T)
|
||||
T.year = T.year - 1
|
||||
local t1 = os.time(T)
|
||||
local delta = (t - t1) / (24 * 3600) - 365
|
||||
-- allow for leap years
|
||||
assert(math.abs(delta) < 2, ("expected abs(%d )< 2"):format(delta))
|
||||
end)
|
||||
|
||||
it("os.date uses local timezone", function()
|
||||
local epoch = os.epoch("local") / 1000
|
||||
local date = os.time(os.date("*t"))
|
||||
assert(date - epoch <= 2, ("expected %d - %d <= 2, but not the case"):format(date, epoch))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("os.date", function()
|
||||
it("formats as expected", function()
|
||||
-- From the PUC Lua tests, hence the weird style
|
||||
local t = os.epoch("local")
|
||||
local T = os.date("*t", t)
|
||||
|
||||
_G.T = T
|
||||
loadstring(os.date([[assert(T.year==%Y and T.month==%m and T.day==%d and
|
||||
T.hour==%H and T.min==%M and T.sec==%S and
|
||||
T.wday==%w+1 and T.yday==%j and type(T.isdst) == 'boolean')]], t))()
|
||||
|
||||
T = os.date("!*t", t)
|
||||
_G.T = T
|
||||
loadstring(os.date([[!assert(T.year==%Y and T.month==%m and T.day==%d and
|
||||
T.hour==%H and T.min==%M and T.sec==%S and
|
||||
T.wday==%w+1 and T.yday==%j and type(T.isdst) == 'boolean')]], t))()
|
||||
end)
|
||||
|
||||
describe("produces output consistent with PUC Lua", function()
|
||||
-- Create a separate test for each code, just so it's easier to see what's broken
|
||||
local t1 = os.time { year = 2000, month = 10, day = 1, hour = 23, min = 12, sec = 17 }
|
||||
local function exp_code(code, value)
|
||||
it(("for code '%s'"):format(code), function()
|
||||
expect(os.date(code, t1)):eq(value)
|
||||
end)
|
||||
end
|
||||
|
||||
exp_code("%a", "Sun")
|
||||
exp_code("%A", "Sunday")
|
||||
exp_code("%b", "Oct")
|
||||
exp_code("%B", "October")
|
||||
exp_code("%c", "Sun Oct 1 23:12:17 2000")
|
||||
exp_code("%C", "20")
|
||||
exp_code("%d", "01")
|
||||
exp_code("%D", "10/01/00")
|
||||
exp_code("%e", " 1")
|
||||
exp_code("%F", "2000-10-01")
|
||||
exp_code("%g", "00")
|
||||
exp_code("%G", "2000")
|
||||
exp_code("%h", "Oct")
|
||||
exp_code("%H", "23")
|
||||
exp_code("%I", "11")
|
||||
exp_code("%j", "275")
|
||||
exp_code("%m", "10")
|
||||
exp_code("%M", "12")
|
||||
exp_code("%n", "\n")
|
||||
exp_code("%p", "PM")
|
||||
exp_code("%r", "11:12:17 PM")
|
||||
exp_code("%R", "23:12")
|
||||
exp_code("%S", "17")
|
||||
exp_code("%t", "\t")
|
||||
exp_code("%T", "23:12:17")
|
||||
exp_code("%u", "7")
|
||||
exp_code("%U", "40")
|
||||
exp_code("%V", "39")
|
||||
exp_code("%w", "0")
|
||||
exp_code("%W", "39")
|
||||
exp_code("%x", "10/01/00")
|
||||
exp_code("%X", "23:12:17")
|
||||
exp_code("%y", "00")
|
||||
exp_code("%Y", "2000")
|
||||
exp_code("%%", "%")
|
||||
|
||||
it("zones are numbers", function()
|
||||
local zone = os.date("%z", t1)
|
||||
if not zone:match("^[+-]%d%d%d%d$") then
|
||||
error("Invalid zone: " .. zone)
|
||||
end
|
||||
end)
|
||||
|
||||
it("zones id is made of letters", function()
|
||||
local zone = os.date("%Z", t1)
|
||||
if not zone:match("^%a%a+$") then
|
||||
error("Non letter character in zone: " .. zone)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("os.time", function()
|
||||
it("maps directly to seconds", function()
|
||||
local t1 = os.time { year = 2000, month = 10, day = 1, hour = 23, min = 12, sec = 17 }
|
||||
local t2 = os.time { year = 2000, month = 10, day = 1, hour = 23, min = 10, sec = 19 }
|
||||
expect(t1 - t2):eq(60 * 2 - 2)
|
||||
end)
|
||||
end)
|
||||
end)
|
Loading…
Reference in New Issue
Block a user