diff --git a/spec/myst/time_spec.mt b/spec/myst/time_spec.mt index 768d0c0..d088599 100644 --- a/spec/myst/time_spec.mt +++ b/spec/myst/time_spec.mt @@ -1,7 +1,51 @@ require "stdlib/spec.mt" describe("Time#initialize") do - it("instantiates a new time type") do + it("instantiates second, nanosecond") do + t = %Time{63618825600, 10} + assert(t.year == 2017) + assert(t.month == 1) + assert(t.day == 1) + assert(t.hour == 0) + assert(t.minute == 0) + assert(t.second == 0) + assert(t.nanosecond == 10) + end + + it("instantiates year, month, day") do + t = %Time{2017, 1, 2} + assert(t.year == 2017) + assert(t.month == 1) + assert(t.day == 2) + assert(t.hour == 0) + assert(t.minute == 0) + assert(t.second == 0) + assert(t.nanosecond == 0) + end + + it("instantiates year, month, day, hour") do + t = %Time{2017, 1, 2, 3} + assert(t.year == 2017) + assert(t.month == 1) + assert(t.day == 2) + assert(t.hour == 3) + assert(t.minute == 0) + assert(t.second == 0) + assert(t.nanosecond == 0) + end + + it("instantiates year, month, day, hour, minute") do + t = %Time{2017, 1, 2, 3, 4} + assert(t.year == 2017) + assert(t.month == 1) + assert(t.day == 2) + assert(t.hour == 3) + assert(t.minute == 4) + assert(t.second == 0) + assert(t.nanosecond == 0) + end + + it("instantiates year, month, day, hour, minute, second") do t = %Time{2017, 1, 2, 3, 4, 5} assert(t.year == 2017) assert(t.month == 1) @@ -9,6 +53,29 @@ describe("Time#initialize") do assert(t.hour == 3) assert(t.minute == 4) assert(t.second == 5) + assert(t.nanosecond == 0) + end + + it("instantiates year, month, day, hour, minute, second, nanosecond") do + t = %Time{2017, 1, 2, 3, 4, 5, 6} + assert(t.year == 2017) + assert(t.month == 1) + assert(t.day == 2) + assert(t.hour == 3) + assert(t.minute == 4) + assert(t.second == 5) + assert(t.nanosecond == 6) + end + + it("raises invalid time") do + expect_raises { %Time{5, 10000000000} } + expect_raises { %Time{100000, 10, 1} } + expect_raises { %Time{2017, 15, 1} } + expect_raises { %Time{2017, 10, 40} } + expect_raises { %Time{2017, 10, 15, 600} } + expect_raises { %Time{2017, 10, 15, 12, 65} } + expect_raises { %Time{2017, 10, 15, 12, 59, 61} } + expect_raises { %Time{2017, 10, 15, 12, 59, 59, -1} } end end @@ -74,8 +141,75 @@ describe("Time#-") do assert(t1 - t2 == -1.0) end + it("gives fractional seconds") do + t1 = %Time{2017, 1, 2, 3, 4, 5, 7} + t2 = %Time{2017, 1, 2, 3, 4, 6, 8} + assert(t2 - t1 == 1.000000001) + end + it("raises if a non Time type is passed") do t1 = %Time{2017, 1, 2, 3, 4, 5} expect_raises { t1 - nil } end end + +describe("Time#year") do + it("returns the year") do + t = %Time{2017, 1, 2, 3, 4, 5} + assert(t.year == 2017) + end +end + +describe("Time#month") do + it("returns the month") do + t = %Time{2017, 1, 2, 3, 4, 5} + assert(t.month == 1) + end +end + +describe("Time#day") do + it("returns the day") do + t = %Time{2017, 1, 2, 3, 4, 5} + assert(t.day == 2) + end +end + +describe("Time#hour") do + it("returns the hour") do + t = %Time{2017, 1, 2, 3, 4, 5} + assert(t.hour == 3) + end +end + +describe("Time#minute") do + it("returns the minute") do + t = %Time{2017, 1, 2, 3, 4, 5} + assert(t.minute == 4) + end +end + +describe("Time#second") do + it("returns the second") do + t = %Time{2017, 1, 2, 3, 4, 5} + assert(t.second == 5) + end +end + +describe("Time#millisecond") do + it("returns 0 when no nanoseconds") do + t = %Time{2017, 1, 2, 3, 4, 5} + assert(t.millisecond == 0) + end + + it("returns the millisecond") do + t = %Time{2017, 1, 2, 3, 4, 5, 6000000} + assert(t.millisecond == 6) + end +end + +describe("Time#nanosecond") do + it("returns the nanosecond") do + t = %Time{2017, 1, 2, 3, 4, 5, 6} + assert(t.nanosecond == 6) + end +end diff --git a/src/myst/interpreter/native_lib/time.cr b/src/myst/interpreter/native_lib/time.cr index f7594df..c2663c1 100644 --- a/src/myst/interpreter/native_lib/time.cr +++ b/src/myst/interpreter/native_lib/time.cr @@ -1,31 +1,17 @@ module Myst class Interpreter NativeLib.method :static_time_now, Value do - t = Time.now + seconds, nanoseconds = Crystal::System::Time.compute_utc_seconds_and_nanoseconds + offset = Crystal::System::Time.compute_utc_offset(seconds) instance = NativeLib.instantiate(self, this.as(TType), [ - TInteger.new(t.year.to_i64), - TInteger.new(t.month.to_i64), - TInteger.new(t.day.to_i64), - TInteger.new(t.hour.to_i64), - TInteger.new(t.minute.to_i64), - TInteger.new(t.second.to_i64) + TInteger.new(seconds + offset), + TInteger.new(nanoseconds.to_i64) ] of Value) instance end - NativeLib.method :time_subtract, Value, other : Value do - case __typeof(other).name - when "Time" - this_time = to_crystal_time(this.as(TInstance)) - other_time = to_crystal_time(other.as(TInstance)) - TFloat.new((this_time - other_time).total_seconds) - else - raise NativeLib.error("invalid argument for Time#-: #{__typeof(other).name}", callstack) - end - end - NativeLib.method :time_to_s, TInstance, format : TString? do crystal_time = to_crystal_time(this) @@ -42,19 +28,15 @@ module Myst NativeLib.def_method(time_type, :now, :static_time_now) NativeLib.def_instance_method(time_type, :to_s, :time_to_s) - NativeLib.def_instance_method(time_type, :-, :time_subtract) time_type end private def to_crystal_time(myst_time : TInstance) Time.new( - myst_time.ivars["@year"].as(TInteger).value, - myst_time.ivars["@month"].as(TInteger).value, - myst_time.ivars["@day"].as(TInteger).value, - myst_time.ivars["@hour"].as(TInteger).value, - myst_time.ivars["@minute"].as(TInteger).value, - myst_time.ivars["@second"].as(TInteger).value + seconds: myst_time.ivars["@seconds"].as(TInteger).value, + nanoseconds: myst_time.ivars["@nanoseconds"].as(TInteger).value.to_i32, + kind: Time::Kind::Unspecified ) end end diff --git a/stdlib/time.mt b/stdlib/time.mt index b3014bb..8791427 100644 --- a/stdlib/time.mt +++ b/stdlib/time.mt @@ -1,18 +1,207 @@ deftype Time + # Logic taken from Crystal::Time + defmodule Util + DAYS_MONTH = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + + DAYS_MONTH_LEAP = [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + + SECONDS_PER_MINUTE = 60 + + SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE + + SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR + + NANOSECONDS_PER_MILLISECOND = 1_000_000.0 + + NANOSECONDS_PER_SECOND = 1_000_000_000.0 + + NANOSECONDS_PER_MINUTE = NANOSECONDS_PER_SECOND * 60 + + DAYS_PER_400_YEARS = 365 * 400 + 97 + + DAYS_PER_100_YEARS = 365 * 100 + 24 + + DAYS_PER_4_YEARS = 365 * 4 + 1 + + def leap_year?(year) + (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) + end + + def absolute_days(year, month, day) + days = when leap_year?(year) + DAYS_MONTH_LEAP + else + DAYS_MONTH + end + + temp = 0 + m = 1 + while m < month + temp += days[m] + m += 1 + end + + (day - 1) + temp + (365*(year - 1)) + ((year - 1)/4) - ((year - 1)/100) + ((year - 1)/400) + end + + def year_month_day_day_year(seconds) + m = 1 + + days = DAYS_MONTH + total_days = seconds / SECONDS_PER_DAY + + num400 = total_days / DAYS_PER_400_YEARS + total_days -= num400 * DAYS_PER_400_YEARS + + num100 = total_days / DAYS_PER_100_YEARS + when num100 == 4 # leap + num100 = 3 + end + total_days -= num100 * DAYS_PER_100_YEARS + + num4 = total_days / DAYS_PER_4_YEARS + total_days -= num4 * DAYS_PER_4_YEARS + + num_years = total_days / 365 + + when num_years == 4 # leap + num_years = 3 + end + + year = num400 * 400 + num100 * 100 + num4 * 4 + num_years + 1 + + total_days -= num_years * 365 + day_year = total_days + 1 + + dec_31_leap = num100 == 3 || num4 != 24 + when num_years == 3 && dec_31_leap # 31 dec leapyear + days = DAYS_MONTH_LEAP + end + + while total_days >= days[m] + total_days -= days[m] + m += 1 + end + + month = m + day = total_days + 1 + + [year, month, day, day_year] + end + + def convert_to_seconds(year, month, day, hour, minute, second) + days = absolute_days(year, month, day) + seconds = 1 * + SECONDS_PER_DAY * days + + SECONDS_PER_HOUR * hour + + SECONDS_PER_MINUTE * minute + + second + end + + def days_in_month(year, month) + when leap_year?(year) + days = DAYS_MONTH_LEAP + else + days = DAYS_MONTH + end + + days[month] + end + end + + def initialize(seconds, nanoseconds) + unless 0 <= nanoseconds && nanoseconds <= Util.NANOSECONDS_PER_SECOND + raise "Invalid time: invalid nanoseconds" + end + @seconds = seconds + @nanoseconds = nanoseconds + end + + def initialize(year, month, day) + validate(year, month, day, 0, 0, 0, 0) + @seconds = Util.convert_to_seconds(year, month, day, 0, 0, 0) + @nanoseconds = 0 + end + + def initialize(year, month, day, hour) + validate(year, month, day, hour, 0, 0, 0) + @seconds = Util.convert_to_seconds(year, month, day, hour, 0, 0) + @nanoseconds = 0 + end + + def initialize(year, month, day, hour, minute) + validate(year, month, day, hour, minute, 0, 0) + @seconds = Util.convert_to_seconds(year, month, day, hour, minute, 0) + @nanoseconds = 0 + end + def initialize(year, month, day, hour, minute, second) - @year = year - @month = month - @day = day - @hour = hour - @minute = minute - @second = second + validate(year, month, day, hour, minute, second, 0) + @seconds = Util.convert_to_seconds(year, month, day, hour, minute, second) + @nanoseconds = 0 + end + + def initialize(year, month, day, hour, minute, second, nanosecond) + validate(year, month, day, hour, minute, second, nanosecond) + @seconds = Util.convert_to_seconds(year, month, day, hour, minute, second) + @nanoseconds = nanosecond + end + + def validate(year, month, day, hour, minute, second, nanosecond) + unless 1 <= year && year <= 9999 && + 1 <= month && month <= 12 && + 1 <= day && day <= Util.days_in_month(year, month) && + 0 <= hour && hour <= 23 && + 0 <= minute && minute <= 59 && + 0 <= second && second <= 59 && + 0 <= nanosecond && nanosecond <= Util.NANOSECONDS_PER_SECOND + raise "Invalid time" + end end # Only getters since a given Time is immutable - def year; @year; end - def month; @month; end - def day; @day; end - def hour; @hour; end - def minute; @minute; end - def second; @second; end + def seconds; @seconds; end + def nanoseconds; @nanoseconds; end + + def year + @year ||= Util.year_month_day_day_year(@seconds)[0] + end + + def month + @month ||= Util.year_month_day_day_year(@seconds)[1] + end + + def day + @day ||= Util.year_month_day_day_year(@seconds)[2] + end + + def hour + @hour ||= (@seconds % Util.SECONDS_PER_DAY) / Util.SECONDS_PER_HOUR + end + + def minute + @minute ||= (@seconds % Util.SECONDS_PER_HOUR) / Util.SECONDS_PER_MINUTE + end + + def second + @second ||= @seconds % Util.SECONDS_PER_MINUTE + end + + def millisecond + @millisecond ||= @nanoseconds / Util.NANOSECONDS_PER_MILLISECOND + end + + def nanosecond + @nanoseconds + end + + def -(other : Time) + new_seconds = seconds - other.seconds + new_nanoseconds = nanoseconds - other.nanoseconds + new_seconds + (new_nanoseconds / Util.NANOSECONDS_PER_SECOND) + end + + def -(other) + raise "Invalid Argument for Time#-: <(other.type)>" + end end