use std::fmt;
use std::fmt::Formatter;
use crate::event::{TickTimeEvent, TicketTimeEventValue};
pub mod event;
mod lib_tests;
const LUNAR_MONTH_DURATION: usize = 30;
const LUNAR_YEAR_DURATION: usize = LUNAR_MONTH_DURATION * 12;
#[derive(Clone, Debug)]
pub enum TickTimeType {
EarthLike {
seconds_per_tick: usize,
month_type: EarthLikeMonthType,
},
Custom {
seconds_per_tick: usize,
hours_in_a_day: usize,
months_durations: Vec<usize>,
seasons_durations: Vec<usize>,
week_duration: usize,
},
}
#[derive(Clone, Debug)]
pub enum EarthLikeMonthType {
Lunar,
Real,
}
#[derive(Clone, Debug)]
pub struct TickTimeOptions {
pub tick_time_type: TickTimeType,
pub compute_events: bool,
}
#[derive(Clone, Debug, Default)]
struct TickTimeValue {
year: usize,
season: usize,
month: usize,
week: usize,
day: usize,
hour: usize,
minute: usize,
second: usize,
}
#[derive(Clone, Debug)]
pub struct TickTime {
options: TickTimeOptions,
current_tick: usize,
values: TickTimeValue,
old_values: TickTimeValue,
}
impl TickTime {
pub fn init(current_tick: usize, options: TickTimeOptions) -> Result<Self, &'static str> {
if let Err(e) = verify_tick_time_type_values(&options.tick_time_type) {
return Err(e);
}
let mut tick_time = TickTime {
current_tick,
options,
values: Default::default(),
old_values: Default::default()
};
tick_time.apply_current_tick();
Ok(tick_time)
}
pub fn tick(&mut self) -> Option<TickTimeEvent> {
self.current_tick += 1;
self.apply_current_tick();
if self.options.compute_events {
Some(self.compute_event())
}else{
None
}
}
pub fn values(&self) -> (usize, usize, usize, usize, usize, usize, usize, usize) {
(
self.values.year,
self.values.season,
self.values.week,
self.values.month,
self.values.day,
self.values.hour,
self.values.minute,
self.values.second,
)
}
fn compute_event(&self) -> TickTimeEvent {
let mut event = TickTimeEvent::default();
let mut update_level = 0;
if self.old_values.year != self.values.year {
update_level += 1;
event.year_update = Some(TicketTimeEventValue{ old_value: self.old_values.year, new_value: self.values.year });
}
if update_level > 0 || self.old_values.season != self.values.season {
event.season_update = Some(TicketTimeEventValue{ old_value: self.old_values.season, new_value: self.values.season });
}
if update_level > 0 || self.old_values.week != self.values.week {
event.week_update = Some(TicketTimeEventValue{ old_value: self.old_values.week, new_value: self.values.week });
}
if update_level > 0 || self.old_values.month != self.values.month {
update_level += 1;
event.month_update = Some(TicketTimeEventValue{ old_value: self.old_values.month, new_value: self.values.month });
}
if update_level > 0 || self.old_values.day != self.values.day {
update_level += 1;
event.day_update = Some(TicketTimeEventValue{ old_value: self.old_values.day, new_value: self.values.day });
}
if update_level > 0 || self.old_values.hour != self.values.hour {
update_level += 1;
event.hour_update = Some(TicketTimeEventValue{ old_value: self.old_values.hour, new_value: self.values.hour });
}
if update_level > 0 || self.old_values.minute != self.values.minute {
update_level += 1;
event.minute_update = Some(TicketTimeEventValue{ old_value: self.old_values.minute, new_value: self.values.minute });
}
if update_level > 0 || self.old_values.second != self.values.second {
event.second_update = Some(TicketTimeEventValue{ old_value: self.old_values.second, new_value: self.values.second });
}
event
}
pub fn current_tick(&self) -> usize {
self.current_tick
}
pub fn year(&self) -> usize {
self.values.year
}
pub fn month(&self) -> usize {
self.values.month
}
pub fn season(&self) -> usize {
self.values.season
}
pub fn week(&self) -> usize {
self.values.week
}
pub fn day(&self) -> usize {
self.values.day
}
pub fn hour(&self) -> usize {
self.values.hour
}
pub fn minute(&self) -> usize {
self.values.minute
}
pub fn second(&self) -> usize {
self.values.second
}
fn apply_current_tick(&mut self) {
if self.options.compute_events {
self.old_values = self.values.clone();
}
match self.options.tick_time_type {
TickTimeType::EarthLike { .. } => { self.compute_earthlike_time(); }
TickTimeType::Custom { .. } => { self.compute_custom_date_time_values() }
}
}
fn compute_earthlike_time(&mut self) {
if let TickTimeType::EarthLike {
seconds_per_tick,
month_type,
} = &self.options.tick_time_type
{
let total_seconds = self.current_tick * seconds_per_tick;
self.values.second = total_seconds % 60;
self.values.minute = (total_seconds / 60) % 60;
self.values.hour = (total_seconds / 3600) % 24;
let total_days = total_seconds / 86400;
let (day, week, month, season, year) = match month_type {
EarthLikeMonthType::Lunar => compute_lunar_calendar_value(total_days),
EarthLikeMonthType::Real => compute_real_calendar_value(total_days)
};
self.values.day = day;
self.values.month = month;
self.values.week = week;
self.values.season = season;
self.values.year = year;
}
}
fn compute_custom_date_time_values(&mut self) {
if let TickTimeType::Custom {
seconds_per_tick, hours_in_a_day, months_durations, seasons_durations, week_duration
} = &self.options.tick_time_type
{
let total_seconds = self.current_tick * seconds_per_tick;
self.values.second = total_seconds % 60;
self.values.minute = (total_seconds / 60) % 60;
self.values.hour = (total_seconds / 3600) % hours_in_a_day;
let total_days = total_seconds / 3600 / hours_in_a_day;
let year_duration: usize = months_durations.iter().sum();
let (day, week, month, season, year) = {
let (day, current_year) = (total_days % year_duration, total_days / year_duration);
let (month, day_of_month) = find_correct_index_and_day_in_section(
day,
months_durations.len(),
months_durations,
);
let (season, _) = find_correct_index_and_day_in_section(
day,
seasons_durations.len(),
seasons_durations,
);
(day_of_month, day / week_duration, month, season % 4, current_year)
};
self.values.day = day;
self.values.week = week;
self.values.month = month;
self.values.season = season;
self.values.year = year;
}
}
}
impl fmt::Display for TickTime {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "Tick time: [ Current tick: {}, Year: {}, Season: {}, Week: {} Month: {}, Day: {}, Hour: {}, Minute: {}, Second: {}]",
self.current_tick, self.year(), self.season(), self.week(), self.month(), self.day(), self.hour(), self.minute(), self.second())
}
}
fn compute_real_calendar_value(total_days: usize) -> (usize, usize, usize, usize, usize) {
let (day, current_year, is_leap_year) =
normalize_total_day_to_year_information(total_days);
let (month, day_of_month) = find_correct_index_and_day_in_section(
day,
12,
&get_month_duration(is_leap_year),
);
let (season, _) = find_correct_index_and_day_in_section(
day,
4,
&get_season_duration(is_leap_year),
);
(day_of_month, day / 7, month, season % 4, current_year)
}
fn compute_lunar_calendar_value(total_days: usize) -> (usize, usize, usize, usize, usize) {
(
total_days % LUNAR_YEAR_DURATION % LUNAR_MONTH_DURATION,
total_days % LUNAR_YEAR_DURATION / 7,
total_days % LUNAR_YEAR_DURATION / LUNAR_MONTH_DURATION,
(total_days % LUNAR_YEAR_DURATION) / (LUNAR_YEAR_DURATION / 4),
total_days / LUNAR_YEAR_DURATION,
)
}
fn get_month_duration(is_leap_year: bool) -> Vec<usize> {
vec![31, if is_leap_year { 29 } else { 28 }, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
}
fn get_season_duration(is_leap_year: bool) -> Vec<usize> {
vec![if is_leap_year { 81 } else { 80 }, 92, 92, 91]
}
fn verify_tick_time_type_values(tick_time_type: &TickTimeType) -> Result<(), &'static str> {
match tick_time_type {
TickTimeType::EarthLike {
seconds_per_tick, ..
} => {
if *seconds_per_tick == 0 {
return Err("The minimum value for EarthLike::seconds_per_tick is 1");
}
}
TickTimeType::Custom {
seconds_per_tick, hours_in_a_day: _, months_durations, seasons_durations, ..
} => {
if *seconds_per_tick == 0 {
return Err("The minimum value for Custom::seconds_per_tick is 1");
}
if months_durations.iter().sum::<usize>() != seasons_durations.iter().sum::<usize>() {
return Err("The sum of values of Custom::months_durations and Custom::season_duration should be the same to keep consistent");
}
}
}
Ok(())
}
fn normalize_total_day_to_year_information(total_days: usize) -> (usize, usize, bool) {
let base_4_year_days = total_days % 1461;
let base_4_year_start = (total_days / 1461) * 4;
match base_4_year_days {
0..=365 => (base_4_year_days, base_4_year_start, true),
366..=730 => (base_4_year_days - 366, base_4_year_start + 1, false),
731..=1095 => (base_4_year_days - 731, base_4_year_start + 2, false),
_ => (base_4_year_days - 1095, base_4_year_start + 3, false),
}
}
fn find_correct_index_and_day_in_section(
day: usize,
max: usize,
array: &Vec<usize>,
) -> (usize, usize) {
let (mut day_counter, mut stop, mut index) = (day, false, 0);
while !stop && index < max {
let next_month_duration = array[index];
if day_counter < next_month_duration {
stop = true;
} else {
day_counter -= next_month_duration;
index += 1;
}
}
(index, day_counter)
}