Implement os.date() and os.time()

I wanted to make an application that features a calendar but quickly noticed that playdate has no os.time() or os.date() Lua functions, making it impossible to calculate dates. I have attached my pure Lua code to showcase which methods are required to make that app.

Here's my code in pure Lua.
local Calendar = {}

local function clamp(value, min, max)
    return math.max(math.min(tonumber(value), tonumber(max) or 1), tonumber(min) or 0)
end

function Calendar.isLeapYear(year)
    return year % 4 == 0 and (year % 100 ~= 0 or year % 400 == 0)
end

function Calendar.isLightsaveDay(year, month, day)
    return os.date("*t", os.time{year = year, month = month, day = day})["isdst"]
end

function Calendar.getWeekdayIndex(year, month, day)
    local day_position = os.date("*t", os.time{year = year, month = month, day = day})["wday"] - 1 -- shift weekday indices to 0-6, starting with sunday at 0
    if(day_position == 0) then day_position = 7 end -- move sunday from index 0 to 7, thus starting week at 1 with monday and ending it with 7 at sunday
    return day_position
end

function Calendar.getWeeknames(locale)
    return ({
        en = {"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"},
        de = {"Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"},
        ru = {"Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота", "Воскресенье"}
    })[locale or "en"]
end

function Calendar.getMonthnames(locale)
    return ({
        en = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"},
        de = {"Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"},
        ru = {"Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"}
    })[locale or "en"]
end

function Calendar.getMonthDaycount(year, month) -- based on code from http://lua-users.org/wiki/DayOfWeekAndDaysInMonthExample
    return month == 2 and Calendar.isLeapYear(year) and 29 or ("\31\28\31\30\31\30\31\31\30\31\30\31"):byte(month)
end

function Calendar.getYearDaycount(year, month, day)
    return tonumber(os.date("%j", os.time{year = year, month = month, day = day})) -- date format documentation at https://www.gammon.com.au/scripts/doc.php?lua=os.date
end

function Calendar.getWeekIndex(year, month, day) -- based on code from http://lua-users.org/wiki/WeekNumberInYear
    local weekDayOffset = function(year)
        local beginning_week = Calendar.getWeekdayIndex(year, 1, 1)
        return beginning_week < 5
            and beginning_week - 2 -- first day is week 1
            or beginning_week - 9 -- first day is week 52 or 53
    end

    local elapsed_days = Calendar.getYearDaycount(year, month, day)
    local day_of_year = elapsed_days + weekDayOffset(year)

    if day_of_year < 0 then -- week of last year: decide if 52 or 53
        day_of_year = elapsed_days + Calendar.getYearDaycount(year - 1, 12, 31) + weekDayOffset(year - 1)
    end
    
    local week_number = math.floor(day_of_year / 7) + 1

    if(week_number == 53 and day_of_year > 0 and Calendar.getWeekdayIndex(year + 1, 1, 1) < 5) then -- day in week 1 is part of the next year
        return 1
    end

    return week_number
end

function Calendar.getDate(year, month, day, locale)
    day = clamp(day, 1, Calendar.getMonthDaycount(year, month)) -- consider leap year and cut overflowing days
    local weekday = Calendar.getWeekdayIndex(year, month, day)
    local weeknames = Calendar.getWeeknames(locale)
    return {
        locale = locale,
        year = year,
        year_daycount = Calendar.getYearDaycount(year, 12, 31),
        year_weekcount = Calendar.getWeekIndex(year, 12, 31),
        year_leaping = Calendar.isLeapYear(year),
        month = month,
        month_daycount = Calendar.getMonthDaycount(year, month),
        month_name = Calendar.getMonthnames(locale)[month],
        weekday = weekday,
        weekday_name = weeknames[weekday],
        week_daycount = #weeknames,
        week_firstday = weeknames[1],
        calendar_week = Calendar.getWeekIndex(year, month, day),
        day = day,
        day_lightsaving = Calendar.isLightsaveDay(year, month, day)
    }
end

return Calendar

I could pre-render that data for the next couple of years, but that seems to be a rather stupid workaround as the app would need to be re-compiled every now and then.

Hi, did you have a look at the Date & Time section in the docs? There are quite a few functions available even if they’re non standard.

1 Like

My Playtime clocks don't have a calendar, but they do sometimes show the date, and the SDK functions worked well for me.

If it might be useful, I have Lua code I can share for some related tasks:

  • Getting the length of any month.

  • The complicated rules for whether there's a February 29 (which is not always "every 4 years").

  • Incrementing/decrementing the second, minute, hour, weekday, numeric day, month, year... and having one value cascade through to the others as needed.

  • 24- to 12-hour conversion.

1 Like

@RDK I did have a quick look at it but I had the feeling it was missing functions to lookup future or past dates. Seems like I could use the epoch-functions to do the required conversions as they return back a date/time table. But I have to dig deeper first to answer that question.

@AdamsImmersive Oh cool! Your code would be very helpful indeed! Thank you for sharing!

1 Like

These are Lua snippets out of context... and written when I was a Playdate n00b...but maybe some of this will be applicable.

The functions below use a bunch of global variables I declared. These are for showing text names:

dayNames = {"Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"}
dayNamesLong = {"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"}

monthNamesLong = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}

These variables store components of the current time for quick access whenever I need:

sourceTime = nil  --Raw system time
sourceHrs = 0 --12-hour number
sourceHrs24 = 0
sourceMin = 0

These I used as the WORKING time, for modification and display:

hrs = 0 --12-hour number
hrs24 = 0
min = 0
day = 0
weekday = 1
month = 0
year = 0

display24Hour = false

This function sets those variable when called:

function currentTime()
	sourceTime = pd.getTime() 
	
	sourceHrs24 = sourceTime.hour
	hrs24 = sourceHrs24 --24-hour time
	
	sourceHrs = sourceHrs24
	if sourceHrs > 12 then
		sourceHrs -= 12
	elseif sourceHrs < 1 then
		sourceHrs += 12
	end
	hrs = sourceHrs --12-hour time
	
	sourceMin = sourceTime.minute
	min = sourceMin
	
	day = sourceTime.day
	weekday = sourceTime.weekday
	
	month = sourceTime.month
	year = sourceTime.year
end

Then, whenever I do math to increase/decrease those working variables, they can end up "out of range" and impossible to display. For example:

Month 11 + 3 months later is Month 14??

Weekday 6 + 5 days later is Weekday 11??

Hour 3 - 6 hours earlier is -3 o'clock??

Minute 59 + 1 is 60... but ought to be 0. And when I "roll" one unit over like that (in either direction), the next unit (hours) should also change.

Etc. etc.

So this function normalizes all values after I modify any of them, giving me a more useful representation of the same time. I don't deal with seconds here (could be added at the beginning) but a chanage of minute can cascade all the way down to a change of year. (Happy New Year!) Leap years (variable February length) are accounted for.

function normalizeTime()
	if min > 59 then
		min -= 60
		hrs += 1
		hrs24 += 1
	end
	if min < 0 then
		min += 60
		hrs -= 1
		hrs24 -= 1
	end
	
	if hrs > 12 then
		hrs -= 12
	end
	if hrs < 1 then
		hrs += 12
	end
	
	if hrs24 > 23 then
		hrs24 -= 24
		day += 1
		weekday += 1
	end
	if hrs24 < 0 then
		hrs24 += 24
		day -= 1
		weekday -= 1
	end
	
	local feb = 28
	if year % 400 == 0 or ( (year % 4 == 0) and (year % 100 ~= 0) ) then
		feb = 29 --#$*!@ leap year
	end

	--Month lengths pre-defined EXCEPT "feb" which changes for leap years:
	local monthLengths = {31, feb, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}

	local prevMonth = month - 1
	if prevMonth == 0 then
		prevMonth = 12
	end
	
	if day > monthLengths[month] then
		day -= monthLengths[month]
		month += 1
	end
	if day < 1 then
		day += monthLengths[prevMonth]
		month -= 1
	end
	
	if month > 12 then
		month -= 12
		year += 1
	end
	if month < 1 then
		month += 12
		year -= 1
	end
	
	if weekday > 7 then
		weekday -= 7
	end
	if weekday < 1 then
		weekday += 7
	end
end

And lastly, this function makes a user-facing formatted time string—adding zero padding as needed, and a colon, while respecting the boolean value of display24hours:

function digitalTime()
	local minText = min
	if minText < 10 then --Pad to 2 digits
		minText = "0"..minText
	end
	
	local hrsText = hrs
	
	if display24Hour then
		hrsText = hrs24
		
		if hrs24 < 10 then --Pad to 2 digits
			hrsText = "0"..hrsText
		end
	end
	
	return hrsText..":"..minText
end
2 Likes

For advanced date logic, I can recommend this library:

I haven't tested it with Playdate, but it works well with Defold (another Lua game engine).

2 Likes