Files
life_generators/fortune_generator/js/fortune.js
tobiichi3227 e80f077ae3 Fix(Fortune): Calculate the time difference for events that have already occurred based on the next year
For example, if today is January 2nd, then the New Year's Day on January 1st should be calculated using January 1st of the next year.
2024-12-25 11:35:26 +08:00

480 lines
14 KiB
JavaScript

let ip = null;
fetch("https://api.ipify.org?format=json").then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
}).then((res) => {
ip = res.ip;
}).catch((_error) => {
if ("caches" in window) {
caches.match("https://api.ipify.org?format=json").then((response) => {
if (response) {
return response.json();
}
}).then((data) => {
if (ip === null && data !== undefined) {
ip = JSON.parse(data).ip;
}
});
}
});
let goodFortunes = [];
let badFortunes = [];
let special_events = [];
let fortune_generated = false;
// using async and await to prevent fetching the data too late...
async function fetch_data() {
await fetch("./json/fortune.json")
.then((response) => response.json())
.then((data) => {
goodFortunes = data.goodFortunes;
badFortunes = data.badFortunes;
});
async function fetch_events(path) {
await fetch(path)
.then((response) => response.json())
.then((data) => {
special_events.push(...data.special_events);
});
}
await fetch_events("./json/custom_special.json");
await fetch_events("./json/static_special.json");
await fetch_events("./json/cyclical_special.json");
}
const textColorClass = [
"good-fortune",
"good-fortune",
"good-fortune",
"good-fortune",
"good-fortune",
"middle-fortune",
"bad-fortune",
"bad-fortune",
];
const fortuneStatus = [
"大吉",
"中吉",
"小吉",
"吉",
"末吉",
"中平",
"凶",
"大凶",
];
const chineseMonth = [
"一",
"二",
"三",
"四",
"五",
"六",
"七",
"八",
"九",
"十",
"十一",
"十二",
];
const week = ["日", "一", "二", "三", "四", "五", "六"];
const title =
`<span class="title" style="font-size:8vmin;"><b>今日運勢</b></span>`;
const allGood =
`<span class="bad-fortune" style="font-size:6vmin;"><b>萬事皆宜</b></span>`;
const allBad =
`<span class="good-fortune" style="font-size:6vmin;"><b>諸事不宜</b></span>`;
// date
const d = new Date();
const date = d.getDate();
const day = d.getDay();
const month = d.getMonth() + 1;
const year = d.getFullYear();
function validateNumber(value, min, max, fieldName, event) {
value = parseInt(value);
if (isNaN(value) || value < min || value > max) {
console.warn(`illegal event: ${fieldName} should be between ${min} and ${max}`, event);
return null;
}
return value;
}
function isLeapYear(year) {
if (year % 400 === 0) return true;
if (year % 100 === 0) return false;
if (year % 4 === 0) return true;
return false;
}
const daysPerMonth = [
0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
];
const maxDate = new Date(8640000000000000);
function daysDiff(eventIndex) {
// define the date right now and the special event date
const event = special_events[eventIndex];
const startDate = new Date(year, month - 1, date);
let eventYear = -1, eventMonth = -1, eventDate = -1;
if (!('triggerDate' in event)) {
console.warn('illegal event: missing `triggerDate` field', event);
return -1;
} else if (Object.prototype.toString.call(event.triggerDate) !== "[object Object]") {
console.warn('illegal event: `triggerDate` field should be a json object', event);
return -1;
}
const triggerDate = event.triggerDate;
let isCustomEvent = false;
eventYear = year;
if ('year' in triggerDate) {
eventYear = validateNumber(triggerDate.year, 1, maxDate.getFullYear(), 'triggerDate.year', event);
if (eventYear === null) {
return -1;
}
isCustomEvent = true;
}
if (!('month' in triggerDate)) {
console.warn('illegal event: `triggerDate` missing `month` field', event);
return -1;
}
eventMonth = validateNumber(triggerDate.month, 1, 12, 'triggerDate.Month', event);
if (eventMonth === null) {
return -1;
}
if (!('date' in triggerDate) && (!('week' in triggerDate) || !('weekday' in triggerDate))) {
console.warn('illegal event: `triggerDate` require (`week` and `weekday`) or `date` field', event);
return -1;
}
if ('date' in triggerDate) {
let days = daysPerMonth[eventMonth];
if (isLeapYear(eventYear) && eventMonth == 2) days += 1;
eventDate = validateNumber(triggerDate.date, 1, days, 'triggerDate.date', event);
if (eventDate === null) {
return -1;
}
} else {
triggerDate.week = validateNumber(triggerDate.week, 1, 5, 'triggerDate.week', event);
triggerDate.weekday = validateNumber(triggerDate.weekday, 1, 7, 'triggerDate.weekday', event);
if (triggerDate.week === null || triggerDate.weekday === null) {
return -1;
}
const firstDayOfMonth = new Date(eventYear, eventMonth - 1, 1);
const firstDayWeekday = firstDayOfMonth.getDay();
// Sunday -> 7
const adjustedFirstDayWeekday = firstDayWeekday === 0 ? 7 : firstDayWeekday;
const firstTargetDay = 1 + (triggerDate.weekday - adjustedFirstDayWeekday + 7) % 7;
eventDate = firstTargetDay + (triggerDate.week - 1) * 7;
}
if (!isCustomEvent && (month > eventMonth || (month == eventMonth && date > eventDate))) {
eventYear += 1;
}
const endDate = new Date(
eventYear,
eventMonth - 1,
eventDate,
);
// calculate the difference in milliseconds and convert it to days
const timeDiff = Math.ceil((endDate - startDate) / (1000 * 60 * 60 * 24));
return timeDiff;
}
// pre-search jquery - save to a variable to improve performance
const J_l_1_event = $("#l-1-event");
const J_l_1_desc = $("#l-1-desc");
const J_l_2_event = $("#l-2-event");
const J_l_2_desc = $("#l-2-desc");
const J_r_1_event = $("#r-1-event");
const J_r_1_desc = $("#r-1-desc");
const J_r_2_event = $("#r-2-event");
const J_r_2_desc = $("#r-2-desc");
const J_ip_to_fortune = $("#ip-to-fortune");
let special = false;
let special_events_index = 0;
// init page
async function init_page() {
// fetch fortune.json and special.json
await fetch_data();
// hide the elements of show fortune page
$("#result-page").hide();
// show date before button pressed
const showMonth =
`<span class="date-color" style="font-size:10vmin; -webkit-writing-mode:vertical-lr;"><b>${
chineseMonth[month - 1] + "月"
}</b></span>`;
const showDate = `<span class="date-color" style="font-size:25vmin;"><b>${
("0" + date).slice(-2)
}</b></span>`;
const showDay =
`<span class="date-color" style="font-size:10vmin; -webkit-writing-mode:vertical-lr; margin-right:10%;"><b>${
"星期" + week[day]
}</b></span>`;
$("#month").html(showMonth);
$("#date").html(showDate);
$("#weekday").html(showDay);
const showSpecialEventCount = 2;
let eventIndexList = Array(showSpecialEventCount).fill(-1);
let eventDiffDaysIndexList = Array(showSpecialEventCount).fill(Number.MAX_SAFE_INTEGER);
// check if there is special event today
for (let i = 0; i < special_events.length; i++) {
let diffCount = daysDiff(i);
if (diffCount > 0) {
let j = 0;
for (; j < showSpecialEventCount; j++) {
if (diffCount < eventDiffDaysIndexList[j]) {
break;
}
}
eventDiffDaysIndexList[j] = diffCount;
eventIndexList[j] = i;
} else if (diffCount === 0) {
special = true;
special_events_index = i;
}
}
// if there is upcoming event then show
for (let eventIndex = 0; eventIndex < showSpecialEventCount; eventIndex++) {
if (eventIndexList[eventIndex] != -1) {
const days = daysDiff(eventIndexList[eventIndex]);
const upcoming_event =
`<span class="desc" style="font-size:5vmin;">距離<b class="special-event">${
special_events[eventIndexList[eventIndex]].event
}</b>還剩<b class="special-event">${days}</b>天</span>`;
$(`#upcoming-event-${eventIndex + 1}`).html(upcoming_event);
}
}
// show special event if today is a special day
if (special) {
const special_event_today =
`<span class="desc" style="font-size:9vmin;">今日是<b class="good-fortune">${
special_events[special_events_index].event
}</b></span>`;
$("#special-day").html(special_event_today);
}
const last_date_str = localStorage.getItem("last_date");
if (last_date_str !== null && last_date_str !== undefined) {
const now_date = new Date();
const last_date = new Date(last_date_str);
if (
now_date.getFullYear() === last_date.getFullYear() &&
now_date.getMonth() === last_date.getMonth() &&
now_date.getDate() === last_date.getDate()
) {
fortune_generated = true;
Appear();
}
}
}
// event bar
const good_span = (event) =>
`<span class="good-fortune" style="font-size:5.6vmin;"><b>宜: </b>${event}</span>`;
const bad_span = (event) =>
`<span class="bad-fortune" style="font-size:5.6vmin;"><b>忌: </b>${event}</span>`;
const desc_span = (desc) =>
`<span class="desc" style="font-size:3.5vmin;">${desc}</span>`;
function Appear() {
$("#title").html(title);
$("#btn").html("打卡成功");
// disable the btn
$("#btn").attr("disabled", "disabled");
//change page
$("#init-page").hide();
$("#result-page").show();
// some lengths
const goodLen = goodFortunes.length;
const badLen = badFortunes.length;
const statusLen = fortuneStatus.length;
let status_index = -1;
let seed1 = -1;
let seed2 = -1;
if (!fortune_generated) {
// transform ip to four numbers
const num = ip.split(".").map((num) => parseInt(num));
// TODO: improve the hash process
const hashDate = Math.round(
Math.log10(
year *
((month << (Math.log10(num[3]) + day - 1)) *
(date << Math.log10(num[2] << day))),
),
);
seed1 = (num[0] >> hashDate) * (num[1] >> Math.min(hashDate, 2)) +
(num[2] << 1) * (num[3] >> 3) + (date << 3) * (month << hashDate) +
(year * day) >> 2;
seed2 = (num[0] << (hashDate + 2)) * (num[1] << hashDate) +
(num[2] << 1) * (num[3] << 2) +
(date << (hashDate - 1)) * (month << 4) + year >>
hashDate + (date * day) >> 1;
// decide the status
status_index = ((seed1 + seed2) % statusLen + statusLen) % statusLen;
// update last record
localStorage.setItem("last_date", d.toISOString());
localStorage.setItem("last_status_index", status_index.toString());
localStorage.setItem("last_seed1", seed1.toString());
localStorage.setItem("last_seed2", seed2.toString());
} else {
status_index = parseInt(localStorage.getItem("last_status_index"));
seed1 = parseInt(localStorage.getItem("last_seed1"));
seed2 = parseInt(localStorage.getItem("last_seed2"));
}
const status = `<span class=${
textColorClass[status_index]
} style="font-size:12vmin;"><b>§ ${fortuneStatus[status_index]} §</b></span>`;
if (special) {
status_index = special_events[special_events_index].status_index;
const special_status = `<span class=${
textColorClass[status_index]
} style="font-size:12vmin;"><b>§ ${
fortuneStatus[status_index]
} §</b></span>`;
J_ip_to_fortune.html(special_status);
} else {
J_ip_to_fortune.html(status);
}
// make sure the events won't collide
const set = new Set();
const l1 = (seed1 % goodLen + goodLen) % goodLen;
set.add(goodFortunes[l1].event);
let l2 = (((seed1 << 1) + date) % goodLen + goodLen) % goodLen;
while (set.has(goodFortunes[l2].event)) {
l2 = (l2 + 1) % goodLen;
}
set.add(goodFortunes[l2].event);
let r1 = (((seed1 >> 1) + (d.getMonth() << 3)) % badLen + badLen) % badLen;
while (set.has(badFortunes[r1].event)) {
r1 = (r1 + 2) % badLen;
}
set.add(badFortunes[r1].event);
let r2 = ((((((seed1 << 3) + (d.getFullYear() >> 5) * (date << 2)) % badLen) *
seed2) >> 6) % badLen + badLen) % badLen;
while (set.has(badFortunes[r2].event)) {
r2 = (r2 + 1) % badLen;
}
// organize the stuffs below this line...
const l1_desc_list = goodFortunes[l1].description;
const l2_desc_list = goodFortunes[l2].description;
const r1_desc_list = badFortunes[r1].description;
const r2_desc_list = badFortunes[r2].description;
const l_1_event = good_span(goodFortunes[l1].event);
const l_1_desc = desc_span(
l1_desc_list[Math.abs(seed1) % l1_desc_list.length],
);
const l_2_event = good_span(goodFortunes[l2].event);
const l_2_desc = desc_span(
l2_desc_list[Math.abs(seed2) % l2_desc_list.length],
);
const r_1_event = bad_span(badFortunes[r1].event);
const r_1_desc = desc_span(
r1_desc_list[Math.abs(seed1) % r1_desc_list.length],
);
const r_2_event = bad_span(badFortunes[r2].event);
const r_2_desc = desc_span(
r2_desc_list[Math.abs(seed2) % r2_desc_list.length],
);
if (special) {
// instead clear variable name, use short variable name for here... cuz it's too repetitive
const Data = special_events[special_events_index];
if (status_index == 0) {
J_r_1_event.html(allGood);
} else {
J_r_1_event.html(bad_span(Data.badFortunes.r_1_event));
J_r_1_desc.html(desc_span(Data.badFortunes.r_1_desc));
J_r_2_event.html(bad_span(Data.badFortunes.r_2_event));
J_r_2_desc.html(desc_span(Data.badFortunes.r_2_desc));
if (Data.badFortunes.r_1_event.length == 0) {
J_r_1_event.html(r_1_event);
J_r_1_desc.html(r_1_desc);
}
if (Data.badFortunes.r_2_event.length == 0) {
J_r_2_event.html(r_2_event);
J_r_2_desc.html(r_2_desc);
}
}
if (status_index == statusLen - 1) {
J_l_1_event.html(allBad);
} else {
J_l_1_event.html(good_span(Data.goodFortunes.l_1_event));
J_l_1_desc.html(desc_span(Data.goodFortunes.l_1_desc));
J_l_2_event.html(good_span(Data.goodFortunes.l_2_event));
J_l_2_desc.html(desc_span(Data.goodFortunes.l_2_desc));
if (Data.goodFortunes.l_1_event.length == 0) {
J_l_1_event.html(l_1_event);
J_l_1_desc.html(l_1_desc);
}
if (Data.goodFortunes.l_2_event.length == 0) {
J_l_2_event.html(l_2_event);
J_l_2_desc.html(l_2_desc);
}
}
} else {
if (status_index == 0) {
J_r_1_event.html(allGood);
} else {
J_r_1_event.html(r_1_event);
J_r_1_desc.html(r_1_desc);
J_r_2_event.html(r_2_event);
J_r_2_desc.html(r_2_desc);
}
if (status_index == statusLen - 1) {
J_l_1_event.html(allBad);
} else {
J_l_1_event.html(l_1_event);
J_l_1_desc.html(l_1_desc);
J_l_2_event.html(l_2_event);
J_l_2_desc.html(l_2_desc);
}
}
$("#copy-result-button").removeClass("d-none");
}
function getLuck() {
Update();
}
init_page();