use chrono::{prelude::*, Datelike, Duration, Timelike};
use lazy_static::lazy_static;
use rand::prelude::*;
use regex::Regex;
use std::{
cmp::{Ord, Ordering},
collections::HashSet,
fmt,
};
use tracing::debug;
#[cfg(feature = "ffi")]
use crate::callable::ffi::ExternUnitToUnit;
use crate::{
interval_error, invalid_hour_error, unit_error, weekday_collision_error, weekday_error,
Callable, Error, FiveToUnit, FourToUnit, OneToUnit, Real, Result, Scheduler, SixToUnit,
ThreeToUnit, Timekeeper, Timestamp, TwoToUnit, Unit, UnitToUnit,
};
pub type Tag = String;
pub type Interval = u32;
lazy_static! {
static ref DAILY_RE: Regex = Regex::new(r"^([0-2]\d:)?[0-5]\d:[0-5]\d$").unwrap();
static ref HOURLY_RE: Regex = Regex::new(r"^([0-5]\d)?:[0-5]\d$").unwrap();
static ref MINUTE_RE: Regex = Regex::new(r"^:[0-5]\d$").unwrap();
}
#[inline]
#[must_use]
pub fn every(interval: Interval) -> Job {
Job::new(interval)
}
#[inline]
#[allow(clippy::module_name_repetitions)]
#[must_use]
pub fn every_single() -> Job {
Job::new(1)
}
#[derive(Debug, PartialEq, Eq)]
pub struct Job {
interval: Interval, latest: Option<Interval>,
job: Option<Box<dyn Callable>>,
tags: HashSet<Tag>,
unit: Option<Unit>,
at_time: Option<NaiveTime>,
last_run: Option<Timestamp>,
pub(crate) next_run: Option<Timestamp>,
period: Option<Duration>,
start_day: Option<Weekday>,
pub(crate) cancel_after: Option<Timestamp>,
clock: Option<Box<dyn Timekeeper>>,
#[cfg(test)]
pub(crate) call_count: u64,
}
impl Job {
#[must_use]
pub fn new(interval: Interval) -> Self {
Self {
interval,
latest: None,
job: None,
tags: HashSet::new(),
unit: None,
at_time: None,
last_run: None,
next_run: None,
period: None,
start_day: None,
cancel_after: None,
clock: Some(Box::new(Real::default())),
#[cfg(test)]
call_count: 0,
}
}
#[cfg(test)]
#[must_use]
pub fn with_mock_time(interval: Interval, clock: crate::time::mock::Mock) -> Self {
Self {
interval,
latest: None,
job: None,
tags: HashSet::new(),
unit: None,
at_time: None,
last_run: None,
next_run: None,
period: None,
start_day: None,
cancel_after: None,
clock: Some(Box::new(clock)),
#[cfg(test)]
call_count: 0,
}
}
#[cfg(test)]
pub fn add_duration(&mut self, duration: Duration) {
self.clock.as_mut().unwrap().add_duration(duration);
}
fn now(&self) -> Timestamp {
self.clock.as_ref().unwrap().now()
}
pub fn tag(&mut self, tags: &[&str]) {
for &t in tags {
let new_tag = t.to_string();
if !self.tags.contains(&new_tag) {
self.tags.insert(new_tag);
}
}
}
pub(crate) fn has_tag(&self, tag: &str) -> bool {
self.tags.contains(tag)
}
pub fn at(mut self, time_str: &str) -> Result<Self> {
use Unit::{Day, Hour, Minute, Week, Year};
if ![Week, Day, Hour, Minute].contains(&self.unit.unwrap_or(Year)) {
return Err(Error::InvalidUnit);
}
if (self.unit == Some(Day) || self.start_day.is_some()) && !DAILY_RE.is_match(time_str) {
return Err(Error::InvalidDailyAtStr);
}
if self.unit == Some(Hour) && !HOURLY_RE.is_match(time_str) {
return Err(Error::InvalidHourlyAtStr);
}
if self.unit == Some(Minute) && !MINUTE_RE.is_match(time_str) {
return Err(Error::InvalidMinuteAtStr);
}
let time_vals = time_str.split(':').collect::<Vec<&str>>();
let mut hour = 0;
let mut minute = 0;
let mut second = 0;
let num_vals = time_vals.len();
if num_vals == 3 {
hour = time_vals[0].parse()?;
minute = time_vals[1].parse()?;
second = time_vals[2].parse()?;
} else if num_vals == 2 && self.unit == Some(Minute) {
second = time_vals[1].parse()?;
} else if num_vals == 2 && self.unit == Some(Hour) {
minute = if time_vals[0].is_empty() {
0
} else {
time_vals[0].parse()?
};
second = time_vals[1].parse()?;
} else {
hour = time_vals[0].parse()?;
minute = time_vals[1].parse()?;
}
if self.unit == Some(Day) || self.start_day.is_some() {
if hour > 23 {
return Err(invalid_hour_error(hour));
}
} else if self.unit == Some(Hour) {
hour = 0;
} else if self.unit == Some(Minute) {
hour = 0;
minute = 0;
}
self.at_time = Some(NaiveTime::from_hms(hour, minute, second));
Ok(self)
}
pub fn to(mut self, latest: Interval) -> Result<Self> {
if latest <= self.interval {
Err(Error::InvalidInterval)
} else {
self.latest = Some(latest);
Ok(self)
}
}
pub fn until(mut self, until_time: Timestamp) -> Result<Self> {
if until_time < self.now() {
return Err(Error::InvalidUntilTime);
}
self.cancel_after = Some(until_time);
Ok(self)
}
pub fn run(mut self, scheduler: &mut Scheduler, job: fn() -> ()) -> Result<()> {
self.job = Some(Box::new(UnitToUnit::new("job", job)));
self.schedule_next_run()?;
scheduler.add_job(self);
Ok(())
}
#[cfg(feature = "ffi")]
pub fn run_extern(
mut self,
scheduler: &mut Scheduler,
job: extern "C" fn() -> (),
) -> Result<()> {
self.job = Some(Box::new(ExternUnitToUnit::new("job", job)));
self.schedule_next_run()?;
scheduler.add_job(self);
Ok(())
}
pub fn run_one_arg<T>(
mut self,
scheduler: &mut Scheduler,
job: fn(T) -> (),
arg: T,
) -> Result<()>
where
T: 'static + Clone,
{
self.job = Some(Box::new(OneToUnit::new("job_one_arg", job, arg)));
self.schedule_next_run()?;
scheduler.add_job(self);
Ok(())
}
pub fn run_two_args<T, U>(
mut self,
scheduler: &mut Scheduler,
job: fn(T, U) -> (),
arg_one: T,
arg_two: U,
) -> Result<()>
where
T: 'static + Clone,
U: 'static + Clone,
{
self.job = Some(Box::new(TwoToUnit::new(
"job_two_args",
job,
arg_one,
arg_two,
)));
self.schedule_next_run()?;
scheduler.add_job(self);
Ok(())
}
pub fn run_three_args<T, U, V>(
mut self,
scheduler: &mut Scheduler,
job: fn(T, U, V) -> (),
arg_one: T,
arg_two: U,
arg_three: V,
) -> Result<()>
where
T: 'static + Clone,
U: 'static + Clone,
V: 'static + Clone,
{
self.job = Some(Box::new(ThreeToUnit::new(
"job_three_args",
job,
arg_one,
arg_two,
arg_three,
)));
self.schedule_next_run()?;
scheduler.add_job(self);
Ok(())
}
pub fn run_four_args<T, U, V, W>(
mut self,
scheduler: &mut Scheduler,
job: fn(T, U, V, W) -> (),
arg_one: T,
arg_two: U,
arg_three: V,
arg_four: W,
) -> Result<()>
where
T: 'static + Clone,
U: 'static + Clone,
V: 'static + Clone,
W: 'static + Clone,
{
self.job = Some(Box::new(FourToUnit::new(
"job_four_args",
job,
arg_one,
arg_two,
arg_three,
arg_four,
)));
self.schedule_next_run()?;
scheduler.add_job(self);
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn run_five_args<T, U, V, W, X>(
mut self,
scheduler: &mut Scheduler,
job: fn(T, U, V, W, X) -> (),
arg_one: T,
arg_two: U,
arg_three: V,
arg_four: W,
arg_five: X,
) -> Result<()>
where
T: 'static + Clone,
U: 'static + Clone,
V: 'static + Clone,
W: 'static + Clone,
X: 'static + Clone,
{
self.job = Some(Box::new(FiveToUnit::new(
"job_four_args",
job,
arg_one,
arg_two,
arg_three,
arg_four,
arg_five,
)));
self.schedule_next_run()?;
scheduler.add_job(self);
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn run_six_args<T, U, V, W, X, Y>(
mut self,
scheduler: &mut Scheduler,
job: fn(T, U, V, W, X, Y) -> (),
arg_one: T,
arg_two: U,
arg_three: V,
arg_four: W,
arg_five: X,
arg_six: Y,
) -> Result<()>
where
T: 'static + Clone,
U: 'static + Clone,
V: 'static + Clone,
W: 'static + Clone,
X: 'static + Clone,
Y: 'static + Clone,
{
self.job = Some(Box::new(SixToUnit::new(
"job_four_args",
job,
arg_one,
arg_two,
arg_three,
arg_four,
arg_five,
arg_six,
)));
self.schedule_next_run()?;
scheduler.add_job(self);
Ok(())
}
pub(crate) fn should_run(&self) -> bool {
self.next_run.is_some() && self.now() >= self.next_run.unwrap()
}
pub fn execute(&mut self) -> Result<bool> {
if self.is_overdue(self.now()) {
debug!("Deadline already reached, cancelling job {self}");
return Ok(false);
}
debug!("Running job {self}");
if self.job.is_none() {
debug!("No work scheduled, moving on...");
return Ok(true);
}
let _ = self.job.as_ref().ok_or(Error::CallableUnreachable)?.call();
#[cfg(test)]
{
self.call_count += 1;
}
self.last_run = Some(self.now());
self.schedule_next_run()?;
if self.is_overdue(self.now()) {
debug!("Execution went over deadline, cancelling job {self}",);
return Ok(false);
}
Ok(true)
}
fn set_unit_mode(mut self, unit: Unit) -> Result<Self> {
if let Some(u) = self.unit {
Err(unit_error(unit, u))
} else {
self.unit = Some(unit);
Ok(self)
}
}
fn set_single_unit_mode(self, unit: Unit) -> Result<Self> {
if self.interval == 1 {
self.set_unit_mode(unit)
} else {
Err(interval_error(unit))
}
}
pub fn second(self) -> Result<Self> {
self.set_single_unit_mode(Unit::Second)
}
pub fn seconds(self) -> Result<Self> {
self.set_unit_mode(Unit::Second)
}
pub fn minute(self) -> Result<Self> {
self.set_single_unit_mode(Unit::Minute)
}
pub fn minutes(self) -> Result<Self> {
self.set_unit_mode(Unit::Minute)
}
pub fn hour(self) -> Result<Self> {
self.set_single_unit_mode(Unit::Hour)
}
pub fn hours(self) -> Result<Self> {
self.set_unit_mode(Unit::Hour)
}
pub fn day(self) -> Result<Self> {
self.set_single_unit_mode(Unit::Day)
}
pub fn days(self) -> Result<Self> {
self.set_unit_mode(Unit::Day)
}
pub fn week(self) -> Result<Self> {
self.set_single_unit_mode(Unit::Week)
}
pub fn weeks(self) -> Result<Self> {
self.set_unit_mode(Unit::Week)
}
pub fn month(self) -> Result<Self> {
self.set_single_unit_mode(Unit::Month)
}
pub fn months(self) -> Result<Self> {
self.set_unit_mode(Unit::Month)
}
pub fn year(self) -> Result<Self> {
self.set_single_unit_mode(Unit::Year)
}
pub fn years(self) -> Result<Self> {
self.set_unit_mode(Unit::Year)
}
fn set_weekday_mode(mut self, weekday: Weekday) -> Result<Self> {
if self.interval != 1 {
Err(weekday_error(weekday))
} else if let Some(w) = self.start_day {
Err(weekday_collision_error(weekday, w))
} else {
self.start_day = Some(weekday);
self.weeks()
}
}
pub fn monday(self) -> Result<Self> {
self.set_weekday_mode(Weekday::Mon)
}
pub fn tuesday(self) -> Result<Self> {
self.set_weekday_mode(Weekday::Tue)
}
pub fn wednesday(self) -> Result<Self> {
self.set_weekday_mode(Weekday::Wed)
}
pub fn thursday(self) -> Result<Self> {
self.set_weekday_mode(Weekday::Thu)
}
pub fn friday(self) -> Result<Self> {
self.set_weekday_mode(Weekday::Fri)
}
pub fn saturday(self) -> Result<Self> {
self.set_weekday_mode(Weekday::Sat)
}
pub fn sunday(self) -> Result<Self> {
self.set_weekday_mode(Weekday::Sun)
}
fn schedule_next_run(&mut self) -> Result<()> {
let interval = match self.latest {
Some(v) => {
if v < self.interval {
return Err(Error::InvalidInterval);
}
thread_rng().gen_range(self.interval..v)
},
None => self.interval,
};
let period = self.unit()?.duration(interval);
self.period = Some(period);
self.next_run = Some(self.now() + period);
if let Some(w) = self.start_day {
if self.unit != Some(Unit::Week) {
return Err(Error::StartDayError);
}
let weekday_num = w.num_days_from_monday();
let mut days_ahead = i64::from(weekday_num)
- i64::from(
self.next_run
.ok_or(Error::NextRunUnreachable)?
.date()
.weekday()
.num_days_from_monday(),
);
if days_ahead <= 0 {
days_ahead += 7;
}
self.next_run = Some(
self.next_run()? + Unit::Day.duration(u32::try_from(days_ahead).unwrap())
- self.period()?,
);
}
if let Some(at_t) = self.at_time {
use Unit::{Day, Hour, Minute};
if ![Some(Day), Some(Hour), Some(Minute)].contains(&self.unit)
&& self.start_day.is_none()
{
return Err(Error::UnspecifiedStartDay);
}
let next_run = self.next_run()?;
let second = at_t.second();
let hour = if self.unit == Some(Day) || self.start_day.is_some() {
at_t.hour()
} else {
next_run.hour()
};
let minute = if [Some(Day), Some(Hour)].contains(&self.unit) || self.start_day.is_some()
{
at_t.minute()
} else {
next_run.minute()
};
let naive_time = NaiveTime::from_hms(hour, minute, second);
let date = next_run.date();
self.next_run = Some(date.and_time(naive_time).ok_or(Error::InvalidUnit)?);
if self.last_run.is_none() || (self.next_run()? - self.last_run()?) > self.period()? {
let now = self.now();
if self.unit == Some(Day)
&& self.at_time.unwrap() > now.time()
&& self.interval == 1
{
self.next_run = Some(self.next_run.unwrap() - Day.duration(1));
} else if self.unit == Some(Hour)
&& (self.at_time.unwrap().minute() > now.minute()
|| self.at_time.unwrap().minute() == now.minute()
&& self.at_time.unwrap().second() > now.second())
{
self.next_run = Some(self.next_run()? - Hour.duration(1));
} else if self.unit == Some(Minute) && self.at_time.unwrap().second() > now.second()
{
self.next_run = Some(self.next_run()? - Minute.duration(1));
}
}
}
if self.start_day.is_some() && self.at_time.is_some() {
let next = self.next_run.unwrap(); if (next - self.now()).num_days() >= 7 {
self.next_run = Some(next - self.period.unwrap());
}
}
Ok(())
}
fn is_overdue(&self, when: Timestamp) -> bool {
self.cancel_after.is_some() && when > self.cancel_after.unwrap()
}
pub(crate) fn last_run(&self) -> Result<Timestamp> {
self.last_run.ok_or(Error::LastRunUnreachable)
}
pub(crate) fn next_run(&self) -> Result<Timestamp> {
self.next_run.ok_or(Error::NextRunUnreachable)
}
pub(crate) fn period(&self) -> Result<Duration> {
self.period.ok_or(Error::PeriodUnreachable)
}
pub(crate) fn unit(&self) -> Result<Unit> {
self.unit.ok_or(Error::UnitUnreachable)
}
}
impl PartialOrd for Job {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Job {
fn cmp(&self, other: &Self) -> Ordering {
self.next_run.cmp(&other.next_run)
}
}
impl fmt::Display for Job {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let name = if self.job.is_none() {
"No Job"
} else {
let j = self.job.as_ref().unwrap();
j.name()
};
let interval = self.interval;
let unit = self.unit;
write!(f, "Job(interval={interval}, unit={unit:?}, run={name})")
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_plural_time_units() -> Result<()> {
use Unit::{Day, Hour, Minute, Month, Second, Week, Year};
assert_eq!(every(2).seconds()?.unit, Some(Second));
assert_eq!(every(2).minutes()?.unit, Some(Minute));
assert_eq!(every(2).hours()?.unit, Some(Hour));
assert_eq!(every(2).days()?.unit, Some(Day));
assert_eq!(every(2).weeks()?.unit, Some(Week));
assert_eq!(every(2).months()?.unit, Some(Month));
assert_eq!(every(2).years()?.unit, Some(Year));
assert_eq!(every(1).seconds()?.unit, Some(Second));
assert_eq!(every(1).minutes()?.unit, Some(Minute));
assert_eq!(every(1).hours()?.unit, Some(Hour));
assert_eq!(every(1).days()?.unit, Some(Day));
assert_eq!(every(1).weeks()?.unit, Some(Week));
assert_eq!(every(1).months()?.unit, Some(Month));
assert_eq!(every(1).years()?.unit, Some(Year));
Ok(())
}
#[test]
fn test_singular_time_units() -> Result<()> {
use Unit::{Day, Hour, Minute, Month, Second, Week, Year};
assert_eq!(every(1), every_single());
assert_eq!(every_single().second()?.unit, Some(Second));
assert_eq!(every_single().minute()?.unit, Some(Minute));
assert_eq!(every_single().hour()?.unit, Some(Hour));
assert_eq!(every_single().day()?.unit, Some(Day));
assert_eq!(every_single().week()?.unit, Some(Week));
assert_eq!(every_single().month()?.unit, Some(Month));
assert_eq!(every_single().year()?.unit, Some(Year));
Ok(())
}
#[test]
fn test_singular_unit_plural_interval_mismatch() {
assert_eq!(
every(2).second().unwrap_err().to_string(),
"Use seconds() instead of second()".to_string()
);
assert_eq!(
every(2).minute().unwrap_err().to_string(),
"Use minutes() instead of minute()".to_string()
);
assert_eq!(
every(2).hour().unwrap_err().to_string(),
"Use hours() instead of hour()".to_string()
);
assert_eq!(
every(2).day().unwrap_err().to_string(),
"Use days() instead of day()".to_string()
);
assert_eq!(
every(2).week().unwrap_err().to_string(),
"Use weeks() instead of week()".to_string()
);
assert_eq!(
every(2).month().unwrap_err().to_string(),
"Use months() instead of month()".to_string()
);
assert_eq!(
every(2).year().unwrap_err().to_string(),
"Use years() instead of year()".to_string()
);
}
#[test]
fn test_singular_units_match_plural_units() -> Result<()> {
assert_eq!(every(1).second()?.unit, every(1).seconds()?.unit);
assert_eq!(every(1).minute()?.unit, every(1).minutes()?.unit);
assert_eq!(every(1).hour()?.unit, every(1).hours()?.unit);
assert_eq!(every(1).day()?.unit, every(1).days()?.unit);
assert_eq!(every(1).week()?.unit, every(1).weeks()?.unit);
assert_eq!(every(1).month()?.unit, every(1).months()?.unit);
assert_eq!(every(1).year()?.unit, every(1).years()?.unit);
Ok(())
}
#[test]
fn test_reject_weekday_multiple_weeks() {
assert_eq!(
every(2).monday().unwrap_err().to_string(),
"Scheduling jobs on Mon is only allowed for weekly jobs. Using specific days on a job scheduled to run every 2 or more weeks is not supported".to_string()
);
assert_eq!(
every(2).tuesday().unwrap_err().to_string(),
"Scheduling jobs on Tue is only allowed for weekly jobs. Using specific days on a job scheduled to run every 2 or more weeks is not supported".to_string()
);
assert_eq!(
every(2).wednesday().unwrap_err().to_string(),
"Scheduling jobs on Wed is only allowed for weekly jobs. Using specific days on a job scheduled to run every 2 or more weeks is not supported".to_string()
);
assert_eq!(
every(2).thursday().unwrap_err().to_string(),
"Scheduling jobs on Thu is only allowed for weekly jobs. Using specific days on a job scheduled to run every 2 or more weeks is not supported".to_string()
);
assert_eq!(
every(2).friday().unwrap_err().to_string(),
"Scheduling jobs on Fri is only allowed for weekly jobs. Using specific days on a job scheduled to run every 2 or more weeks is not supported".to_string()
);
assert_eq!(
every(2).saturday().unwrap_err().to_string(),
"Scheduling jobs on Sat is only allowed for weekly jobs. Using specific days on a job scheduled to run every 2 or more weeks is not supported".to_string()
);
assert_eq!(
every(2).sunday().unwrap_err().to_string(),
"Scheduling jobs on Sun is only allowed for weekly jobs. Using specific days on a job scheduled to run every 2 or more weeks is not supported".to_string()
);
}
#[test]
fn test_reject_start_day_unless_weekly() {
let mut job = every_single();
let expected = "Attempted to use a start day for a unit other than `weeks`".to_string();
job.unit = Some(Unit::Day);
job.start_day = Some(Weekday::Wed);
assert_eq!(job.schedule_next_run().unwrap_err().to_string(), expected);
}
#[test]
fn test_reject_multiple_time_units() -> Result<()> {
assert_eq!(
every_single().day()?.wednesday().unwrap_err().to_string(),
"Cannot set weeks mode, already using days".to_string()
);
assert_eq!(
every_single().minute()?.second().unwrap_err().to_string(),
"Cannot set seconds mode, already using minutes".to_string()
);
Ok(())
}
#[test]
fn test_reject_invalid_at_time() -> Result<()> {
let bad_hour = "Invalid hour (25 is not between 0 and 23)".to_string();
let bad_daily =
"Invalid time format for daily job (valid format is HH:MM(:SS)?)".to_string();
let bad_hourly =
"Invalid time format for hourly job (valid format is (MM)?:SS)".to_string();
let bad_minutely = "Invalid time format for minutely job (valid format is :SS)".to_string();
let bad_unit = "Invalid unit (valid units are `days`, `hours`, and `minutes`)".to_string();
assert_eq!(
every_single()
.second()?
.at("13:15")
.unwrap_err()
.to_string(),
bad_unit
);
assert_eq!(
every_single()
.day()?
.at("25:00:00")
.unwrap_err()
.to_string(),
bad_hour
);
assert_eq!(
every_single()
.day()?
.at("00:61:00")
.unwrap_err()
.to_string(),
bad_daily
);
assert_eq!(
every_single()
.day()?
.at("00:00:61")
.unwrap_err()
.to_string(),
bad_daily
);
assert_eq!(
every_single()
.day()?
.at("00:61:00")
.unwrap_err()
.to_string(),
bad_daily
);
assert_eq!(
every_single().day()?.at("25:0:0").unwrap_err().to_string(),
bad_daily
);
assert_eq!(
every_single().day()?.at("0:61:0").unwrap_err().to_string(),
bad_daily
);
assert_eq!(
every_single().day()?.at("0:0:61").unwrap_err().to_string(),
bad_daily
);
assert_eq!(
every_single()
.hour()?
.at("23:59:29")
.unwrap_err()
.to_string(),
bad_hourly
);
assert_eq!(
every_single().hour()?.at("61:00").unwrap_err().to_string(),
bad_hourly
);
assert_eq!(
every_single().hour()?.at("00:61").unwrap_err().to_string(),
bad_hourly
);
assert_eq!(
every_single().hour()?.at(":61").unwrap_err().to_string(),
bad_hourly
);
assert_eq!(
every_single()
.minute()?
.at("22:45:34")
.unwrap_err()
.to_string(),
bad_minutely
);
assert_eq!(
every_single().minute()?.at(":61").unwrap_err().to_string(),
bad_minutely
);
Ok(())
}
#[test]
fn test_latest_greater_than_interval() {
assert_eq!(
every(2).to(1).unwrap_err().to_string(),
"Latest val is greater than interval val".to_string()
);
assert_eq!(every(2).to(3).unwrap().latest, Some(3));
}
}