Feat(fortune): ci add special event checker (#50)
* Feat(fortune): ci add special event checker * Chore(Fortune): Move all scripts from `dev` to `scripts` and remove outdated scripts
This commit was merged in pull request #50.
This commit is contained in:
35
.github/workflows/check-events.yml
vendored
Normal file
35
.github/workflows/check-events.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
name: Check special events
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'fortune_generator/json/custom_special.json'
|
||||||
|
- 'fortune_generator/json/static_special.json'
|
||||||
|
- 'fortune_generator/json/cyclical_special.json'
|
||||||
|
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- 'fortune_generator/json/custom_special.json'
|
||||||
|
- 'fortune_generator/json/static_special.json'
|
||||||
|
- 'fortune_generator/json/cyclical_special.json'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
- name: Check Custom Special Events
|
||||||
|
run: |
|
||||||
|
python3 scripts/check-events.py fortune_generator/json/custom_special.json custom
|
||||||
|
|
||||||
|
- name: Check Static Special Events
|
||||||
|
run: |
|
||||||
|
python3 scripts/check-events.py fortune_generator/json/static_special.json static
|
||||||
|
|
||||||
|
- name: Check Cyclical Special Events
|
||||||
|
run: |
|
||||||
|
python3 scripts/check-events.py fortune_generator/json/cyclical_special.json cyclical
|
||||||
2
dev/.gitignore
vendored
2
dev/.gitignore
vendored
@@ -1,2 +0,0 @@
|
|||||||
good_fortune_data.txt
|
|
||||||
bad_fortune_data.txt
|
|
||||||
289
dev/main.js
289
dev/main.js
@@ -1,289 +0,0 @@
|
|||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const goodFortunes = [
|
|
||||||
{
|
|
||||||
"event": "睡覺",
|
|
||||||
"description": "品質良好,精神煥發",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "做家務",
|
|
||||||
"description": "整潔使人心情愉悅",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "冥想",
|
|
||||||
"description": "平靜心靈,緩解焦慮",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "攝影",
|
|
||||||
"description": "捕捉到美好瞬間",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "喝咖啡",
|
|
||||||
"description": "精力充沛燃燒脂肪",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "朋友聚會",
|
|
||||||
"description": "充滿歡笑和美好回憶",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "體育鍛鍊",
|
|
||||||
"description": "能量滿滿,效果顯著",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "出遊",
|
|
||||||
"description": "好天氣,好心情",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "吃大餐",
|
|
||||||
"description": "聯絡感情",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "逛書店",
|
|
||||||
"description": "新書上架,打折推銷",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "學新技能",
|
|
||||||
"description": "快速上手",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "唱歌",
|
|
||||||
"description": "被星探發掘",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "上課",
|
|
||||||
"description": "整天不累,100% 消化",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "洗澡",
|
|
||||||
"description": "重獲能量",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "請教問題",
|
|
||||||
"description": "問題皆獲高人指點",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "網購",
|
|
||||||
"description": "心儀商品皆促銷",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "放假",
|
|
||||||
"description": "休息充電,明日再戰",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "早睡",
|
|
||||||
"description": "好夢連連",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "早起",
|
|
||||||
"description": "朝氣蓬勃,神采飛揚",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "發文章",
|
|
||||||
"description": "瀏覽數暴增",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "點外賣",
|
|
||||||
"description": "準時到達,新鮮好吃",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "做善事",
|
|
||||||
"description": "積善成福",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "散步",
|
|
||||||
"description": "空氣良好,放鬆身心",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const badFortunes = [
|
|
||||||
{
|
|
||||||
"event": "體育鍛鍊",
|
|
||||||
"description": "不慎受傷",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "攝影",
|
|
||||||
"description": "照片全消失",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "出遊",
|
|
||||||
"description": "天氣不晴朗",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "吃大餐",
|
|
||||||
"description": "被要求請客",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "學新技能",
|
|
||||||
"description": "屢試不爽,始終不懂",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "唱歌",
|
|
||||||
"description": "嗓子發炎",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "洗澡",
|
|
||||||
"description": "水溫不穩",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "請教問題",
|
|
||||||
"description": "疑難雜症,均無解答",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "網購",
|
|
||||||
"description": "錯過促銷",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "放假",
|
|
||||||
"description": "隔日工作量倍增",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "晚睡",
|
|
||||||
"description": "失眠,明日精神渙散",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "晚起",
|
|
||||||
"description": "整天都不順",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "發文章",
|
|
||||||
"description": "搜索枯腸,不知所云",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "點外賣",
|
|
||||||
"description": "路況壅塞,餐點冷掉",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "喝咖啡",
|
|
||||||
"description": "晚上失眠",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "散步",
|
|
||||||
"description": "被害蟲咬傷",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const badLen = badFortunes.length;
|
|
||||||
const goodLen = goodFortunes.length;
|
|
||||||
let num = null;
|
|
||||||
const dates = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 30];
|
|
||||||
const statusLen = 8;
|
|
||||||
let buckets = {};
|
|
||||||
let day = 0;
|
|
||||||
let run_cnt = 0;
|
|
||||||
|
|
||||||
while (run_cnt != 2000) {
|
|
||||||
let n1 = parseInt(Math.random() * 255 + 1);
|
|
||||||
let n2 = parseInt(Math.random() * 255 + 1);
|
|
||||||
let n3 = parseInt(Math.random() * 255 + 1);
|
|
||||||
let n4 = parseInt(Math.random() * 255 + 1);
|
|
||||||
|
|
||||||
if (!check_ip_valid(n1, n2, n3, n4)) continue;
|
|
||||||
|
|
||||||
let index = `${n1}.${n2}.${n3}.${n4}`;
|
|
||||||
buckets[index] = [0, 0, 0, 0];
|
|
||||||
for (let i = 1; i <= 12; i++) {
|
|
||||||
for (let j = 1; j <= dates[i - 1]; j++) {
|
|
||||||
day %= 7;
|
|
||||||
run(2023, i, j, day, [n1, n2, n3, n4]);
|
|
||||||
day++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
run_cnt++;
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFile("./res.txt", JSON.stringify(buckets), (err) => {
|
|
||||||
console.log(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
function check_ip_valid(n1, n2, n3, n4) {
|
|
||||||
if (n1 > 255 || n2 > 255 || n3 > 255 || n4 > 255) return false;
|
|
||||||
|
|
||||||
// private network
|
|
||||||
if (n1 === 10) return false;
|
|
||||||
|
|
||||||
// Carrier-grade NAT
|
|
||||||
if (n1 == 100 && n2 == 64) return false;
|
|
||||||
|
|
||||||
// localhost
|
|
||||||
if (n1 === 127 && n2 === 0 && n3 === 0) return false;
|
|
||||||
|
|
||||||
// link-local address
|
|
||||||
if (n1 == 169 && n2 == 254) return false;
|
|
||||||
|
|
||||||
// private network
|
|
||||||
if (n1 === 172) { if (n2 >= 16 && n2 <= 31) return false; }
|
|
||||||
|
|
||||||
if (n1 === 192) {
|
|
||||||
if (n2 === 168) return false; // private network
|
|
||||||
if (n2 === 0 && n3 === 0) return false; // IANA RFC 5735
|
|
||||||
if (n2 === 0 && n3 === 2) return false; // TEST-NET-1 RFC 5735
|
|
||||||
if (n2 === 88 && n3 === 99) return false; // 6to4
|
|
||||||
}
|
|
||||||
|
|
||||||
if (n1 == 198) {
|
|
||||||
if (n2 == 18) return false; // RFC 2544
|
|
||||||
if (n2 == 51 && n3 == 100) return false; // TEST-NET-2 RFC 5735
|
|
||||||
}
|
|
||||||
|
|
||||||
if (n1 == 203 && n3 == 113) return false; // TEST-NET-3 RFC 5735
|
|
||||||
|
|
||||||
// class D network
|
|
||||||
if (n1 == 224) return false;
|
|
||||||
|
|
||||||
// class E network
|
|
||||||
if (n1 == 255) return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// calculate hash and write result
|
|
||||||
function run(year, month, date, day, ip) {
|
|
||||||
let num = ip;
|
|
||||||
let index = `${ip[0]}.${ip[1]}.${ip[2]}.${ip[3]}`;
|
|
||||||
|
|
||||||
// original hash function
|
|
||||||
let hashDate = Math.round(
|
|
||||||
Math.log10(
|
|
||||||
year *
|
|
||||||
((month << (Math.log10(num[3]) + day - 1)) *
|
|
||||||
(date << Math.log10(num[2] << day))),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
let seed1 = (num[0] >> hashDate) * (num[1] >> Math.min(hashDate, 2)) +
|
|
||||||
(num[2] << 1) * (num[3] >> 3) + (date << 3) * (month << hashDate) +
|
|
||||||
((year * day) >> 2);
|
|
||||||
let seed2 = (num[0] << (hashDate + 2)) * (num[1] << hashDate) +
|
|
||||||
(num[2] << 1) * (num[3] << 3) + (date << (hashDate - 1)) * (month << 4) +
|
|
||||||
(year >> hashDate) + ((date * day) >> 1);
|
|
||||||
|
|
||||||
// make sure the events won't collide
|
|
||||||
let set = new Set();
|
|
||||||
let l1 = (seed1 % goodLen + goodLen) % goodLen;
|
|
||||||
let l2 = (((seed1 << 1) + date) % goodLen + goodLen) % goodLen;
|
|
||||||
|
|
||||||
while (l1 == l2) {
|
|
||||||
l2 = (l2 + 1) % goodLen;
|
|
||||||
}
|
|
||||||
|
|
||||||
set.add(goodFortunes[l1].event);
|
|
||||||
set.add(goodFortunes[l2].event);
|
|
||||||
|
|
||||||
let r1 = (((seed1 >> 1) + (month << 3)) % badLen + badLen) % badLen;
|
|
||||||
while (set.has(badFortunes[r1].event)) {
|
|
||||||
r1 = (r1 + 2) % badLen;
|
|
||||||
}
|
|
||||||
set.add(badFortunes[r1].event);
|
|
||||||
let r2 =
|
|
||||||
((((((seed1 << 3) + (year >> 5) * (date << 2)) % badLen) * seed2) >> 6) %
|
|
||||||
badLen + badLen) % badLen;
|
|
||||||
while (set.has(badFortunes[r2].event)) {
|
|
||||||
r2 = (r2 + 1) % badLen;
|
|
||||||
}
|
|
||||||
|
|
||||||
// write l1, l2, r1, r2
|
|
||||||
buckets[index][0] = l1;
|
|
||||||
buckets[index][1] = l2;
|
|
||||||
buckets[index][2] = r1;
|
|
||||||
buckets[index][3] = r2;
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import matplotlib.pyplot as plt
|
|
||||||
|
|
||||||
# Data Processing
|
|
||||||
groups = 2
|
|
||||||
|
|
||||||
with open('./good_fortune_data.txt', 'r') as f:
|
|
||||||
good_fortune_data = [int(line.split(' ')[1].strip()) for line in f.readlines()]
|
|
||||||
good_fortune_data_len = len(good_fortune_data) // groups
|
|
||||||
|
|
||||||
fig, axs = plt.subplots(groups, 1, figsize=(8, 6))
|
|
||||||
|
|
||||||
axs[0].bar(range(good_fortune_data_len), good_fortune_data[:good_fortune_data_len], color='skyblue', edgecolor='black')
|
|
||||||
axs[0].set_xlabel("Good Fortune Event Index")
|
|
||||||
axs[0].set_ylabel("Occurrences")
|
|
||||||
|
|
||||||
axs[1].bar(range(good_fortune_data_len), good_fortune_data[good_fortune_data_len:], color='skyblue', edgecolor='black')
|
|
||||||
axs[1].set_xlabel("Good Fortune Event Index")
|
|
||||||
axs[1].set_ylabel("Occurrences")
|
|
||||||
|
|
||||||
plt.tight_layout()
|
|
||||||
|
|
||||||
plt.savefig("../docs/good_fortune_statistics.png")
|
|
||||||
|
|
||||||
with open('./bad_fortune_data.txt', 'r') as f:
|
|
||||||
bad_fortune_data = [int(line.split(' ')[1].strip()) for line in f.readlines()]
|
|
||||||
bad_fortune_data_len = len(bad_fortune_data) // groups
|
|
||||||
|
|
||||||
fig, axs = plt.subplots(groups, 1, figsize=(8, 6))
|
|
||||||
|
|
||||||
axs[0].bar(range(bad_fortune_data_len), bad_fortune_data[:bad_fortune_data_len], color='skyblue', edgecolor='black')
|
|
||||||
axs[0].set_xlabel("Bad Fortune Event Index")
|
|
||||||
axs[0].set_ylabel("Occurrences")
|
|
||||||
|
|
||||||
axs[1].bar(range(bad_fortune_data_len), bad_fortune_data[bad_fortune_data_len:], color='skyblue', edgecolor='black')
|
|
||||||
axs[1].set_xlabel("Bad Fortune Event Index")
|
|
||||||
axs[1].set_ylabel("Occurrences")
|
|
||||||
|
|
||||||
plt.tight_layout()
|
|
||||||
|
|
||||||
plt.savefig("../docs/bad_fortune_statistics.png")
|
|
||||||
380
scripts/check-events.py
Normal file
380
scripts/check-events.py
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
#!/bin/python3
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import collections
|
||||||
|
import datetime
|
||||||
|
import argparse
|
||||||
|
import enum
|
||||||
|
import json
|
||||||
|
|
||||||
|
class DateType(enum.Enum):
|
||||||
|
CUSTOM = "custom"
|
||||||
|
STATIC = "static"
|
||||||
|
CYCLICAL = "cyclical"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name.lower()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return str(self)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def argparse(s):
|
||||||
|
try:
|
||||||
|
return DateType[s.upper()]
|
||||||
|
except KeyError:
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
args_parser = argparse.ArgumentParser(description="special events checker")
|
||||||
|
args_parser.add_argument("path", type=str, help="event json file path")
|
||||||
|
args_parser.add_argument(
|
||||||
|
"type",
|
||||||
|
type=DateType.argparse,
|
||||||
|
choices=[t for t in DateType],
|
||||||
|
help="event date type",
|
||||||
|
)
|
||||||
|
|
||||||
|
args = args_parser.parse_args()
|
||||||
|
|
||||||
|
special_events: dict[str, list[dict]] = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(args.path) as f:
|
||||||
|
special_events = json.loads(f.read())
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print(f"`{args.path}` json syntax error.")
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"`{args.path}` not found.")
|
||||||
|
print("Please contact developer to solve this problem.")
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
|
if not isinstance(special_events, dict):
|
||||||
|
print("`special_events` should be a dict")
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
|
if "special_events" not in special_events:
|
||||||
|
print(f"`special_events` not found in `{args.path}`.")
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
|
if not isinstance(special_events["special_events"], list):
|
||||||
|
print(f"`special_events` in `{args.path}` should be a list.")
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
|
MIN_STATUS_INDEX = 0
|
||||||
|
MAX_STATUS_INDEX = 7
|
||||||
|
DAYSPERMONTH = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
||||||
|
|
||||||
|
errors: dict[int, list[str]] = collections.defaultdict(list)
|
||||||
|
|
||||||
|
|
||||||
|
def is_leap_year(year: int) -> bool:
|
||||||
|
"""Determines whether a given year is a leap year.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
year (int): The year to check.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the year is a leap year, False otherwise.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if year % 400 == 0:
|
||||||
|
return True
|
||||||
|
if year % 100 == 0:
|
||||||
|
return False
|
||||||
|
if year % 4 == 0:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def validate_number(event_idx: int, value, min: int, max: int, field_name: str) -> int | None:
|
||||||
|
"""Validates whether a given value is an integer within a specified range.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event_idx (int): The index of the event for associating validation errors.
|
||||||
|
value (Any): The value to validate.
|
||||||
|
min (int): The minimum acceptable value (inclusive).
|
||||||
|
max (int): The maximum acceptable value (inclusive).
|
||||||
|
field_name (str): The name of the field being validated, used in error messages.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int | None: The validated integer value if it is within the range, otherwise None.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If `value` cannot be converted to an integer.
|
||||||
|
|
||||||
|
Validation Rules:
|
||||||
|
- If `value` cannot be converted to an integer, an error is recorded and None is returned.
|
||||||
|
- If `value` is outside the range defined by `min` and `max`, an error is recorded and None is returned.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
value = int(value)
|
||||||
|
except ValueError:
|
||||||
|
errors[event_idx].append(f"`{field_name}` should be between {min} and {max}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if value < min or value > max:
|
||||||
|
errors[event_idx].append(f"`{field_name}` should be between {min} and {max}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def require_field_check(
|
||||||
|
obj: dict, event_idx: int, fields: list[tuple[str, type]], required_field: str = ""
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Validates the presence and types of required fields in a given object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
obj (dict): The object (dictionary) to validate.
|
||||||
|
event_idx (int): The index of the event for associating validation errors.
|
||||||
|
fields (list[tuple[str, type]]): A list of tuples where each tuple contains a field name and its expected type.
|
||||||
|
required_field (str, optional): An optional prefix for error messages to indicate a higher-level required field. Defaults to "".
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if all required fields are present and have the correct types, otherwise False.
|
||||||
|
|
||||||
|
Validation Rules:
|
||||||
|
- If a required field is missing, an error message is recorded.
|
||||||
|
- If a field is present but its type does not match the expected type, an error message is recorded.
|
||||||
|
- The `required_field` parameter, if provided, is prepended to error messages for context.
|
||||||
|
"""
|
||||||
|
|
||||||
|
error_found = False
|
||||||
|
for field_name, field_type in fields:
|
||||||
|
if field_name not in obj:
|
||||||
|
error_found = True
|
||||||
|
msg = ""
|
||||||
|
if required_field != "":
|
||||||
|
msg = f"`{required_field}` "
|
||||||
|
|
||||||
|
msg += f"missing `{field_name}`."
|
||||||
|
errors[event_idx].append(msg)
|
||||||
|
|
||||||
|
elif not isinstance(obj[field_name], field_type):
|
||||||
|
error_found = True
|
||||||
|
errors[event_idx].append(f"`{field_name}` should be a `{field_type}` type.")
|
||||||
|
|
||||||
|
if error_found:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
event_names = set()
|
||||||
|
event_dates = set()
|
||||||
|
|
||||||
|
|
||||||
|
def check_structure(event: dict, idx: int):
|
||||||
|
if not require_field_check(
|
||||||
|
event,
|
||||||
|
idx,
|
||||||
|
[
|
||||||
|
("event", str),
|
||||||
|
("triggerDate", dict),
|
||||||
|
("status_index", str),
|
||||||
|
("goodFortunes", dict),
|
||||||
|
("badFortunes", dict),
|
||||||
|
],
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
|
||||||
|
event_name: str = event["event"]
|
||||||
|
if event_name.strip() == "":
|
||||||
|
errors[idx].append("event name should not empty.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if event_name in event_names:
|
||||||
|
errors[idx].append(f"event `{event_name}` already exists.")
|
||||||
|
|
||||||
|
validate_number(
|
||||||
|
idx, event["status_index"], MIN_STATUS_INDEX, MAX_STATUS_INDEX, "status_index"
|
||||||
|
)
|
||||||
|
|
||||||
|
if require_field_check(
|
||||||
|
event["goodFortunes"],
|
||||||
|
idx,
|
||||||
|
[
|
||||||
|
("l_1_event", str),
|
||||||
|
("l_1_desc", str),
|
||||||
|
("l_2_event", str),
|
||||||
|
("l_2_desc", str),
|
||||||
|
],
|
||||||
|
"goodFortunes"
|
||||||
|
):
|
||||||
|
if bool(event["goodFortunes"]["l_1_event"]) ^ bool(event["goodFortunes"]["l_1_desc"]):
|
||||||
|
# Check for inconsistency: XOR is used to ensure both l_1_event and l_1_desc
|
||||||
|
# are either both provided or both missing. If only one is provided, log an error.
|
||||||
|
errors[idx].append("First good fortune is incomplete.")
|
||||||
|
|
||||||
|
if bool(event["goodFortunes"]["l_2_event"]) ^ bool(event["goodFortunes"]["l_2_desc"]):
|
||||||
|
# Check for inconsistency: XOR is used to ensure both l_2_event and l_2_desc
|
||||||
|
# are either both provided or both missing. If only one is provided, log an error.
|
||||||
|
errors[idx].append("Second good fortune is incomplete.")
|
||||||
|
|
||||||
|
if require_field_check(
|
||||||
|
event["badFortunes"],
|
||||||
|
idx,
|
||||||
|
[
|
||||||
|
("r_1_event", str),
|
||||||
|
("r_1_desc", str),
|
||||||
|
("r_2_event", str),
|
||||||
|
("r_2_desc", str),
|
||||||
|
],
|
||||||
|
"badFortunes"
|
||||||
|
):
|
||||||
|
if bool(event["badFortunes"]["r_1_event"]) ^ bool(event["badFortunes"]["r_1_desc"]):
|
||||||
|
# Check for inconsistency: XOR is used to ensure both r_1_event and r_1_desc
|
||||||
|
# are either both provided or both missing. If only one is provided, log an error.
|
||||||
|
errors[idx].append("First bad fortune is incomplete.")
|
||||||
|
|
||||||
|
if bool(event["badFortunes"]["r_2_event"]) ^ bool(event["badFortunes"]["r_2_desc"]):
|
||||||
|
# Check for inconsistency: XOR is used to ensure both r_2_event and r_2_desc
|
||||||
|
# are either both provided or both missing. If only one is provided, log an error.
|
||||||
|
errors[idx].append("Second bad fortune is incomplete.")
|
||||||
|
|
||||||
|
event_names.add(event_name)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def check_static_date(event: dict, idx: int):
|
||||||
|
trigger_date: dict = event["triggerDate"]
|
||||||
|
corrected = require_field_check(
|
||||||
|
trigger_date,
|
||||||
|
idx,
|
||||||
|
[
|
||||||
|
("month", str),
|
||||||
|
("date", str),
|
||||||
|
],
|
||||||
|
"triggerDate",
|
||||||
|
)
|
||||||
|
|
||||||
|
event_name: str = event["event"]
|
||||||
|
if "year" in trigger_date:
|
||||||
|
errors[idx].append(
|
||||||
|
f"this event `{event_name}` should be placed in `custom_special.json`."
|
||||||
|
)
|
||||||
|
|
||||||
|
if "week" in trigger_date or "weekday" in trigger_date:
|
||||||
|
errors[idx].append(
|
||||||
|
f"this event `{event_name}` should be placed in `cyclical_special.json`."
|
||||||
|
)
|
||||||
|
|
||||||
|
if not corrected:
|
||||||
|
return
|
||||||
|
|
||||||
|
month = validate_number(idx, trigger_date["month"], 1, 12, "triggerDate.month")
|
||||||
|
if month is not None:
|
||||||
|
validate_number(
|
||||||
|
idx, trigger_date["date"], 1, DAYSPERMONTH[month], "triggerDate.date"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def check_cyclical_date(event: dict, idx: int):
|
||||||
|
trigger_date: dict = event["triggerDate"]
|
||||||
|
corrected = require_field_check(
|
||||||
|
trigger_date,
|
||||||
|
idx,
|
||||||
|
[
|
||||||
|
("month", str),
|
||||||
|
("week", str),
|
||||||
|
("weekday", str),
|
||||||
|
],
|
||||||
|
"triggerDate",
|
||||||
|
)
|
||||||
|
|
||||||
|
event_name: str = event["event"]
|
||||||
|
if "year" in trigger_date:
|
||||||
|
errors[idx].append(
|
||||||
|
f"this event `{event_name}` should be placed in `custom_special.json`."
|
||||||
|
)
|
||||||
|
|
||||||
|
elif "date" in trigger_date:
|
||||||
|
errors[idx].append(
|
||||||
|
f"this event `{event_name}` should be placed in `static_special.json`."
|
||||||
|
)
|
||||||
|
|
||||||
|
if not corrected:
|
||||||
|
return
|
||||||
|
|
||||||
|
validate_number(idx, trigger_date["month"], 1, 12, "triggerDate.month")
|
||||||
|
validate_number(idx, trigger_date["week"], 1, 5, "triggerDate.week")
|
||||||
|
validate_number(idx, trigger_date["weekday"], 1, 7, "triggerDate.weekday")
|
||||||
|
|
||||||
|
|
||||||
|
def check_custom_date(event: dict, idx: int):
|
||||||
|
trigger_date: dict = event["triggerDate"]
|
||||||
|
corrected = require_field_check(
|
||||||
|
trigger_date,
|
||||||
|
idx,
|
||||||
|
[
|
||||||
|
("year", str),
|
||||||
|
("month", str),
|
||||||
|
("date", str),
|
||||||
|
],
|
||||||
|
"triggerDate",
|
||||||
|
)
|
||||||
|
|
||||||
|
event_name: str = event["event"]
|
||||||
|
if "week" in trigger_date or "weekday" in trigger_date:
|
||||||
|
errors[idx].append(
|
||||||
|
f"this event `{event_name}` should be placed in `cyclical_special.json`.",
|
||||||
|
)
|
||||||
|
|
||||||
|
elif "year" not in trigger_date:
|
||||||
|
errors[idx].append(
|
||||||
|
f"this event `{event_name}` should be placed in `static_special.json`."
|
||||||
|
)
|
||||||
|
|
||||||
|
if not corrected:
|
||||||
|
return
|
||||||
|
|
||||||
|
year = validate_number(
|
||||||
|
idx,
|
||||||
|
trigger_date["year"],
|
||||||
|
datetime.datetime.min.year,
|
||||||
|
datetime.datetime.max.year,
|
||||||
|
"triggerDate.year",
|
||||||
|
)
|
||||||
|
month = validate_number(idx, trigger_date["month"], 1, 12, "triggerDate.month")
|
||||||
|
|
||||||
|
if year is None or month is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
days = DAYSPERMONTH[month]
|
||||||
|
if month == 2 and is_leap_year(year):
|
||||||
|
days += 1 # 29
|
||||||
|
|
||||||
|
date = validate_number(idx, trigger_date["date"], 1, days, "triggerDate.date")
|
||||||
|
if date is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
date_str = f"{year}/{month}/{date}"
|
||||||
|
if date_str in event_dates:
|
||||||
|
errors[idx].append(f"The date `{date_str}` of `{event_name}` is repeated.")
|
||||||
|
|
||||||
|
event_dates.add(date_str)
|
||||||
|
|
||||||
|
|
||||||
|
date_checker = {
|
||||||
|
DateType.CUSTOM: check_custom_date,
|
||||||
|
DateType.STATIC: check_static_date,
|
||||||
|
DateType.CYCLICAL: check_cyclical_date,
|
||||||
|
}
|
||||||
|
check_triggerdate = date_checker[args.type]
|
||||||
|
|
||||||
|
for idx, event in enumerate(special_events["special_events"]):
|
||||||
|
if check_structure(event, idx):
|
||||||
|
check_triggerdate(event, idx)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
logging.error(args.path)
|
||||||
|
for idx, error_msgs in errors.items():
|
||||||
|
logging.error(json.dumps(special_events["special_events"][idx], indent=4))
|
||||||
|
for msg in error_msgs:
|
||||||
|
logging.error(msg)
|
||||||
|
exit(-1)
|
||||||
Reference in New Issue
Block a user