Countdown
ScriptWidget template.
Example
//
// When setting up the widget provide a parameter
// in the setup dialog like Please provide a parameter like
// '2022-11-26 Vacation' if you want to count down
// or up to a date or '2022-11-26T12:35:00 Flight'
// if you want to count up or down to a specific time
// on a date.
//
const SECONDS_PER_MINUTE = 60;
const SECONDS_PER_HOUR = SECONDS_PER_MINUTE * 60;
const SECONDS_PER_DAY = SECONDS_PER_HOUR * 24;
function is_leap(yr) {
return yr % 400 === 0 || (yr % 4 === 0 && yr % 100 !== 0);
}
function days_per_month(month, year) {
if (month === 1) {
if (is_leap(year)) {
return 29;
} else {
return 28;
}
} else {
months = [31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
return months[month];
}
}
function months_to_days(month) {
return Math.floor((month * 3057 - 3007) / 100);
}
function years_to_days(yr) {
return (
yr * 365 + Math.floor(yr / 4) - Math.floor(yr / 100) + Math.floor(yr / 400)
);
}
function ymd_to_days(yr, mo, day) {
scalar = day + months_to_days(mo);
if (mo > 2)
// adjust if past February
scalar -= is_leap(yr) ? 1 : 2;
yr--;
scalar += years_to_days(yr);
return scalar;
}
function determine_sign(remainder, large_unit, small_unit) {
if (remainder > large_unit - Math.floor(small_unit / 2)) {
return { text: "≈", count: 1 };
} else if (remainder > Math.floor(large_unit / 2)) {
return { text: "<", count: 1 };
} else if (remainder > Math.floor(small_unit / 2)) {
return { text: ">", count: 0 };
} else if (remainder > 0) {
return { text: "≈", count: 0 };
} else {
return { text: "", count: 0 };
}
}
function Countdown(target, countdown_to) {
// Convert target and now to tm formats
const now_date = new Date();
now_tm = {
tm_min: countdown_to === "T" ? now_date.getMinutes() : 0,
tm_hour: countdown_to === "T" ? now_date.getHours() : 0,
tm_mday: now_date.getDate(),
tm_mon: now_date.getMonth(),
tm_year: now_date.getFullYear(),
};
const target_date = target;
target_tm = {
tm_min: countdown_to === "T" ? target_date.getMinutes() : 0,
tm_hour: countdown_to === "T" ? target_date.getHours() : 0,
tm_mday: target_date.getDate(),
tm_mon: target_date.getMonth(),
tm_year: target_date.getFullYear(),
};
// Choose post-text, max_tm and min_tm
if (target_date.getTime() > now_date.getTime()) {
// Count down to
post_text = "";
max_tm = target_tm;
min_tm = now_tm;
} else {
// Count down to
post_text = "ago";
max_tm = now_tm;
min_tm = target_tm;
}
// Calculate differences in years, months, days, hours and minutes
received = min_tm.tm_min > max_tm.tm_min ? 60 : 0;
min_diff = max_tm.tm_min + received - min_tm.tm_min;
borrow = received > 0 ? 1 : 0;
received = min_tm.tm_hour + borrow > max_tm.tm_hour ? 24 : 0;
hour_diff = max_tm.tm_hour + received - min_tm.tm_hour - borrow;
borrow = received > 0 ? 1 : 0;
received =
min_tm.tm_mday + borrow > max_tm.tm_mday
? days_per_month(max_tm.tm_mon, max_tm.tm_year)
: 0;
day_diff = max_tm.tm_mday + received - min_tm.tm_mday - borrow;
borrow = received > 0 ? 1 : 0;
received = min_tm.tm_mon + borrow > max_tm.tm_mon ? 12 : 0;
month_diff = max_tm.tm_mon + received - min_tm.tm_mon - borrow;
borrow = received > 0 ? 1 : 0;
year_diff = max_tm.tm_year - min_tm.tm_year - borrow;
// Calculate total difference in seconds
diff =
ymd_to_days(max_tm.tm_year + 1900, max_tm.tm_mon + 1, max_tm.tm_mday) -
ymd_to_days(min_tm.tm_year + 1900, min_tm.tm_mon + 1, min_tm.tm_mday);
if (
min_tm.tm_hour * 100 + min_tm.tm_min >
max_tm.tm_hour * 100 + max_tm.tm_min
)
diff -= 1;
diff = diff * 24 + hour_diff;
diff = diff * 60 + min_diff;
diff = diff * 60;
if (diff == 0 || (countdown_to == 'D' && diff == SECONDS_PER_DAY)) {
// Display one word
if (diff == 0) {
if (countdown_to == 'D') {
return "Today";
} else {
return "Now";
}
} else {
if (target_date.getTime() > now_date.getTime()) {
return "Tomorrow";
} else {
return "Yesterday";
}
}
}
// Display incremental detail
count = 0;
remainder = 0;
if (
year_diff > 3 ||
(year_diff == 3 &&
(month_diff > 0 || day_diff > 0 || hour_diff > 0 || min_diff > 0))
) {
count = year_diff;
remainder =
ymd_to_days(
max_tm.tm_year - year_diff + 1900,
max_tm.tm_mon + 1,
max_tm.tm_mday
) - ymd_to_days(min_tm.tm_year + 1900, min_tm.tm_mon + 1, min_tm.tm_mday);
remainder *= SECONDS_PER_DAY;
remainder += hour_diff * SECONDS_PER_HOUR + min_diff * SECONDS_PER_MINUTE;
sign = determine_sign(
remainder,
(is_leap(max_tm.tm_year + 1900) ? 366 : 365) * SECONDS_PER_DAY,
SECONDS_PER_DAY
);
pre_text = sign.text;
count += sign.count;
unit = " years ";
} else if (
year_diff * 12 + month_diff > 3 ||
(month_diff == 3 && (day_diff > 0 || hour_diff > 0 || min_diff > 0))
) {
count = year_diff * 12 + month_diff;
remainder =
day_diff * SECONDS_PER_DAY +
hour_diff * SECONDS_PER_HOUR +
min_diff * SECONDS_PER_MINUTE;
sign = determine_sign(
remainder,
days_per_month(max_tm.tm_mon, max_tm.tm_year) * SECONDS_PER_DAY,
SECONDS_PER_DAY
);
pre_text = sign.text;
count += sign.count;
unit = " months ";
} else if (diff > 3 * 7 * SECONDS_PER_DAY) {
count = Math.floor(diff / (7 * SECONDS_PER_DAY));
remainder = diff % (7 * SECONDS_PER_DAY);
sign = determine_sign(remainder, 7 * SECONDS_PER_DAY, SECONDS_PER_DAY);
pre_text = sign.text;
count += sign.count;
unit = " weeks ";
} else if (diff > 3 * SECONDS_PER_DAY || countdown_to === "D") {
count = Math.floor(diff / SECONDS_PER_DAY);
remainder = diff % SECONDS_PER_DAY;
sign = determine_sign(remainder, SECONDS_PER_DAY, SECONDS_PER_HOUR);
pre_text = sign.text;
count += sign.count;
unit = " days ";
} else if (diff > 3 * SECONDS_PER_HOUR) {
count = Math.floor(diff / SECONDS_PER_HOUR);
remainder = diff % SECONDS_PER_HOUR;
sign = determine_sign(remainder, SECONDS_PER_HOUR, SECONDS_PER_MINUTE);
pre_text = sign.text;
count += sign.count;
unit = " hours ";
} else if (diff > SECONDS_PER_MINUTE) {
count = Math.floor(diff / SECONDS_PER_MINUTE);
pre_text = "";
unit = " minutes ";
} else {
count = 1;
pre_text = "";
unit = " minute ";
}
return pre_text + count + unit + post_text;
}
// Retrieve target from widget parameter
const param = $getenv("widget-param");
const dtre = /\d\d\d\d\-\d\d\-\d\d(T\d\d\:\d\d\:\d\d)?/;
dt_param = param.match(dtre);
if (!dt_param) {
$render(
<vstack frame="max">
<text>No valid widget parameter specified!</text>
<text></text>
<text font="caption">
Please provide a parameter like '2022-11-26 Vacation' or
'2022-11-26T12:35:00 Flight'
</text>
</vstack>
);
return;
} else {
target = new Date(dt_param[0]);
event = param.replace(dtre, "").trim();
countdown_to = dt_param[1] ? "T" : "D";
}
// Update widget
text = Countdown(target, countdown_to);
let linearGradient = {
type: "linear",
colors: ["#fb5a72", "#f5243b"],
startPoint: "top",
endPoint: "bottom",
};
// Date formatting
if (countdown_to === "T") {
var dateFormat = {
year: "numeric",
month: "short",
day: "numeric",
hour: "numeric",
minute: "numeric",
};
} else {
var dateFormat = {
year: "numeric",
month: "short",
day: "numeric",
};
}
$render(
<vstack
background={$gradient(linearGradient)}
frame="max,leading"
alignment="leading"
>
<hstack padding="10">
<vstack alignment="leading">
<text font="body" color="white">
{event}
</text>
<text font="caption" color="white">
{target.toLocaleDateString(undefined, dateFormat)}
</text>
</vstack>
<spacer />
</hstack>
<spacer />
<text font="title" color="white" padding="10">
{text}
</text>
</vstack>
);Templates live in
Shared/ScriptWidgetRuntime/Resource/Script.bundle/template/ and can be imported directly into the app.