1pub mod download;
42mod draw;
43mod errors;
44mod ffi;
45mod numbers;
46mod query;
47mod rule;
48
49use query::common::game_query_month_bounds;
50use query::common::{
51 game_query_date_bounds, game_query_date_bounds_for_local, game_query_date_bounds_for_remote,
52};
53use query::remote_query_param_support;
54use rule::metadata_for_game;
55
56pub use download::{
57 build_csv_url, download_all, download_api_doc, download_dataset, parse_codes_from_api_docs,
58};
59#[deprecated(note = "use taiwan_lottery::download::gaze::download_history_draw")]
61pub fn download_history_draw(
62 output_dir: impl AsRef<std::path::Path>,
63) -> Result<Vec<std::path::PathBuf>, DownloadError> {
64 download::gaze::download_history_draw(output_dir)
65}
66#[deprecated(note = "use taiwan_lottery::download::tlc::download_history_draw")]
68pub fn download_history_draw_from_taiwan_lottery(
69 output_dir: impl AsRef<std::path::Path>,
70) -> Result<Vec<std::path::PathBuf>, DownloadError> {
71 download::tlc::download_history_draw(output_dir)
72}
73pub use draw::{draw_by_game, DrawResult};
74pub use errors::DownloadError;
75pub use numbers::{
76 BingoBingoNumbers, BonusDrawNumbers, Daily539Numbers, DrawNumbers, Lotto1224Numbers,
77 Lotto38M6Numbers, Lotto39M5Numbers, Lotto3DNumbers, Lotto49M6Numbers, Lotto4DNumbers,
78 Lotto638Numbers, Lotto649Numbers, Lotto740Numbers, SortedDrawNumbers, SuperLotto638Numbers,
79 TicTacToeNumbers,
80};
81pub use query::{query_history_draw, query_history_draw_from_taiwan_lottery};
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum LotteryGame {
89 SuperLotto638,
90 Lotto649,
91 Daily539,
92 Lotto3D,
93 Lotto4D,
94 Lotto49M6,
95 Lotto39M5,
96 Lotto38M6,
97 Lotto1224,
98 Lotto740,
99 TicTacToe,
100 Lotto638,
101 BingoBingo,
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub struct LotteryGameNumberRule {
107 pub name: &'static str,
109 pub picks: usize,
111 pub min: i32,
113 pub max: i32,
115 pub allow_repeat: bool,
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub struct LotteryGameMetadata {
122 pub display_name: &'static str,
124 pub display_name_english: &'static str,
126 pub display_name_chinese: &'static str,
128 pub number_rule: &'static str,
130 pub number_ranges: &'static [LotteryGameNumberRule],
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub struct RemoteQueryParamSupport {
137 pub month: bool,
138 pub end_month: bool,
139 pub open_date: bool,
140 pub period: bool,
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
145pub enum LotteryDisplayLanguage {
146 #[default]
147 English,
148 Chinese,
149}
150
151impl LotteryGameMetadata {
152 pub const fn display_name_for_language(self, language: LotteryDisplayLanguage) -> &'static str {
154 match language {
155 LotteryDisplayLanguage::English => self.display_name_english,
156 LotteryDisplayLanguage::Chinese => self.display_name_chinese,
157 }
158 }
159
160 pub fn with_display_language(mut self, language: LotteryDisplayLanguage) -> Self {
162 self.display_name = self.display_name_for_language(language);
163 self
164 }
165}
166
167#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct LotteryGameQueryRange {
170 pub min_month: String,
172 pub max_month: String,
174}
175
176#[derive(Debug, Clone, PartialEq, Eq)]
178pub struct LotteryGameDateQueryRange {
179 pub min_date: String,
181 pub max_date: String,
183}
184
185#[deprecated(note = "use LotteryGame instead")]
186pub type HistoryGame = LotteryGame;
187
188#[deprecated(note = "use LotteryGameNumberRule instead")]
189pub type HistoryGameNumberRule = LotteryGameNumberRule;
190
191#[deprecated(note = "use LotteryGameMetadata instead")]
192pub type HistoryGameMetadata = LotteryGameMetadata;
193
194impl LotteryGame {
195 pub const ALL: [Self; 13] = [
197 Self::SuperLotto638,
198 Self::Lotto649,
199 Self::Daily539,
200 Self::Lotto3D,
201 Self::Lotto4D,
202 Self::Lotto49M6,
203 Self::Lotto39M5,
204 Self::Lotto38M6,
205 Self::Lotto1224,
206 Self::Lotto740,
207 Self::TicTacToe,
208 Self::Lotto638,
209 Self::BingoBingo,
210 ];
211
212 pub const fn metadata(self) -> LotteryGameMetadata {
214 metadata_for_game(self)
215 }
216
217 pub fn metadata_with_language(self, language: LotteryDisplayLanguage) -> LotteryGameMetadata {
219 self.metadata().with_display_language(language)
220 }
221
222 pub fn query_month_range(self) -> LotteryGameQueryRange {
226 let (start, end) = game_query_month_bounds(self);
227 LotteryGameQueryRange {
228 min_month: start.to_yyyy_mm(),
229 max_month: end.to_yyyy_mm(),
230 }
231 }
232
233 pub fn query_date_range(self) -> LotteryGameDateQueryRange {
237 let (start, end) = game_query_date_bounds(self);
238 LotteryGameDateQueryRange {
239 min_date: start.to_yyyy_mm_dd(),
240 max_date: end.to_yyyy_mm_dd(),
241 }
242 }
243
244 pub fn query_date_range_for_local(self) -> LotteryGameDateQueryRange {
248 let (start, end) = game_query_date_bounds_for_local(self);
249 LotteryGameDateQueryRange {
250 min_date: start.to_yyyy_mm_dd(),
251 max_date: end.to_yyyy_mm_dd(),
252 }
253 }
254
255 pub fn query_date_range_for_remote(self) -> LotteryGameDateQueryRange {
259 let (start, end) = game_query_date_bounds_for_remote(self);
260 LotteryGameDateQueryRange {
261 min_date: start.to_yyyy_mm_dd(),
262 max_date: end.to_yyyy_mm_dd(),
263 }
264 }
265
266 pub const fn remote_query_param_support(self) -> RemoteQueryParamSupport {
268 remote_query_param_support(self)
269 }
270
271 pub fn parse(value: &str) -> Option<Self> {
276 match value.trim().to_ascii_lowercase().as_str() {
277 "super-lotto638" | "superlotto638" | "5134" => Some(Self::SuperLotto638),
278 "lotto649" | "5118" => Some(Self::Lotto649),
279 "daily539" | "5120" => Some(Self::Daily539),
280 "3d" | "2108" => Some(Self::Lotto3D),
281 "4d" | "2109" => Some(Self::Lotto4D),
282 "49m6" | "1121" => Some(Self::Lotto49M6),
283 "39m5" | "1197" => Some(Self::Lotto39M5),
284 "38m6" | "5122" => Some(Self::Lotto38M6),
285 "1224" | "5290" => Some(Self::Lotto1224),
286 "740" | "2300" => Some(Self::Lotto740),
287 "tic-tac-toe" | "tictactoe" | "2400" => Some(Self::TicTacToe),
288 "638" | "2500" => Some(Self::Lotto638),
289 "bingo-bingo" | "bingobingo" | "bingo_bingo" | "1102" => Some(Self::BingoBingo),
290 _ => None,
291 }
292 }
293
294 pub const fn from_code(code: i32) -> Option<Self> {
296 match code {
297 0 => Some(Self::SuperLotto638),
298 1 => Some(Self::Lotto649),
299 2 => Some(Self::Daily539),
300 3 => Some(Self::Lotto3D),
301 4 => Some(Self::Lotto4D),
302 5 => Some(Self::Lotto49M6),
303 6 => Some(Self::Lotto39M5),
304 7 => Some(Self::Lotto38M6),
305 8 => Some(Self::Lotto1224),
306 9 => Some(Self::Lotto740),
307 10 => Some(Self::TicTacToe),
308 11 => Some(Self::Lotto638),
309 12 => Some(Self::BingoBingo),
310 _ => None,
311 }
312 }
313
314 pub(crate) fn path(self) -> &'static str {
315 match self {
316 Self::SuperLotto638 => "/Lottery/SuperLotto638Result",
317 Self::Lotto649 => "/Lottery/Lotto649Result",
318 Self::Daily539 => "/Lottery/Daily539Result",
319 Self::Lotto3D => "/Lottery/3DResult",
320 Self::Lotto4D => "/Lottery/4DResult",
321 Self::Lotto49M6 => "/Lottery/49M6Result",
322 Self::Lotto39M5 => "/Lottery/39M5Result",
323 Self::Lotto38M6 => "/Lottery/38M6Result",
324 Self::Lotto1224 => "/Lottery/Lotto1224Result",
325 Self::Lotto740 => "/Lottery/Lotto740Result",
326 Self::TicTacToe => "/Lottery/TicTacToeResult",
327 Self::Lotto638 => "/Lottery/Lotto638Result",
328 Self::BingoBingo => "/Lottery/BingoResult",
329 }
330 }
331
332 pub(crate) fn history_session_path(self) -> Option<&'static str> {
333 match self {
334 Self::Lotto3D => Some("/Lottery/3DHistoryResult"),
335 Self::Lotto4D => Some("/Lottery/4DHistoryResult"),
336 _ => None,
337 }
338 }
339}
340
341impl std::str::FromStr for LotteryGame {
342 type Err = ();
343
344 fn from_str(s: &str) -> Result<Self, Self::Err> {
345 Self::parse(s).ok_or(())
346 }
347}
348
349impl TryFrom<i32> for LotteryGame {
350 type Error = ();
351
352 fn try_from(value: i32) -> Result<Self, Self::Error> {
353 Self::from_code(value).ok_or(())
354 }
355}
356
357#[derive(Debug, Clone, PartialEq, Eq, Default)]
365pub struct HistoryDrawQuery {
366 pub period: Option<String>,
367 pub month: Option<String>,
368 pub end_month: Option<String>,
369 pub open_date: Option<String>,
370}
371
372impl HistoryDrawQuery {
373 pub fn by_period(period: impl Into<String>) -> Self {
374 Self {
375 period: Some(period.into()),
376 ..Self::default()
377 }
378 }
379
380 pub fn by_month(month: impl Into<String>) -> Self {
381 let month = month.into();
382 Self {
383 month: Some(month.clone()),
384 end_month: Some(month),
385 ..Self::default()
386 }
387 }
388
389 pub fn by_month_range(month: impl Into<String>, end_month: impl Into<String>) -> Self {
390 Self {
391 month: Some(month.into()),
392 end_month: Some(end_month.into()),
393 ..Self::default()
394 }
395 }
396
397 pub fn by_open_date(open_date: impl Into<String>) -> Self {
398 Self {
399 open_date: Some(open_date.into()),
400 ..Self::default()
401 }
402 }
403
404 fn normalized_params(&self) -> Result<(&str, &str, &str), DownloadError> {
405 let period = self.period.as_deref().unwrap_or("").trim();
406 if !period.is_empty() {
407 return Ok((period, "", ""));
408 }
409
410 let month = self
411 .month
412 .as_deref()
413 .map(str::trim)
414 .filter(|value| !value.is_empty())
415 .ok_or_else(|| std::io::Error::other("month is required when period is empty"))?;
416 let end_month = self
417 .end_month
418 .as_deref()
419 .map(str::trim)
420 .filter(|value| !value.is_empty())
421 .unwrap_or(month);
422
423 Ok(("", month, end_month))
424 }
425}
426
427#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
432pub struct HistoryDrawItem {
433 pub period: String,
434 pub date: Option<String>,
435 pub redeemable_date: Option<String>,
436 pub numbers: SortedDrawNumbers,
437}
438
439#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
443pub struct HistoryDrawPage {
444 pub total_size: usize,
445 pub items: Vec<HistoryDrawItem>,
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451
452 mod history_draw_query_tests {
453 use super::*;
454
455 #[test]
456 fn history_draw_query_requires_month_when_period_is_empty() {
457 let query = HistoryDrawQuery::default();
458 let err = query
459 .normalized_params()
460 .expect_err("must fail without period or month");
461 assert!(matches!(err, DownloadError::Io(_)));
462 }
463 }
464
465 mod lottery_game_tests {
466 use super::*;
467
468 #[test]
469 fn lottery_game_query_month_range_is_exposed_for_ui() {
470 let range = LotteryGame::Lotto1224.query_month_range();
471 assert_eq!(range.min_month, "2018-04");
472 assert_eq!(range.max_month, "2023-12");
473 }
474
475 #[test]
476 fn lottery_game_parse_supports_aliases() {
477 assert_eq!(LotteryGame::parse("lotto649"), Some(LotteryGame::Lotto649));
478 assert_eq!(LotteryGame::parse("5118"), Some(LotteryGame::Lotto649));
479 assert_eq!(
480 LotteryGame::parse("tic-tac-toe"),
481 Some(LotteryGame::TicTacToe)
482 );
483 assert_eq!(
484 LotteryGame::parse("bingo-bingo"),
485 Some(LotteryGame::BingoBingo)
486 );
487 assert_eq!(LotteryGame::parse("1102"), Some(LotteryGame::BingoBingo));
488 assert_eq!(LotteryGame::parse("unknown"), None);
489 }
490
491 #[test]
492 fn lottery_game_from_code_matches_ffi_codes() {
493 assert_eq!(LotteryGame::from_code(0), Some(LotteryGame::SuperLotto638));
494 assert_eq!(LotteryGame::from_code(11), Some(LotteryGame::Lotto638));
495 assert_eq!(LotteryGame::from_code(12), Some(LotteryGame::BingoBingo));
496 assert_eq!(LotteryGame::from_code(99), None);
497 }
498
499 #[test]
500 fn lottery_game_from_str_matches_parse() {
501 use std::str::FromStr as _;
502
503 assert_eq!(LotteryGame::from_str("lotto649"), Ok(LotteryGame::Lotto649));
504 assert_eq!(LotteryGame::from_str("5118"), Ok(LotteryGame::Lotto649));
505 assert_eq!(LotteryGame::from_str("invalid"), Err(()));
506 }
507
508 #[test]
509 fn lottery_game_try_from_i32_matches_from_code() {
510 assert_eq!(LotteryGame::try_from(0), Ok(LotteryGame::SuperLotto638));
511 assert_eq!(LotteryGame::try_from(11), Ok(LotteryGame::Lotto638));
512 assert_eq!(LotteryGame::try_from(12), Ok(LotteryGame::BingoBingo));
513 assert_eq!(LotteryGame::try_from(-1), Err(()));
514 }
515
516 #[test]
517 fn history_game_all_lists_every_supported_game() {
518 assert_eq!(LotteryGame::ALL.len(), 13);
519 assert!(LotteryGame::ALL.contains(&LotteryGame::TicTacToe));
520 assert!(LotteryGame::ALL.contains(&LotteryGame::SuperLotto638));
521 assert!(LotteryGame::ALL.contains(&LotteryGame::BingoBingo));
522 }
523 }
524}