Compare commits
No commits in common. "bc99011bb66bc9b8a155aef666fbbd960101d3ef" and "b320605f268651b410099bd836882d974767e821" have entirely different histories.
bc99011bb6
...
b320605f26
2 changed files with 44 additions and 409 deletions
120
src/main.rs
120
src/main.rs
|
@ -37,7 +37,6 @@ struct Config {
|
||||||
common_ninjas: Vec<wac_common_ninja::Config>,
|
common_ninjas: Vec<wac_common_ninja::Config>,
|
||||||
icals: Vec<wac_ical::Config>,
|
icals: Vec<wac_ical::Config>,
|
||||||
output: output::Config,
|
output: output::Config,
|
||||||
feeds: Vec<SimpleDownload>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
@ -53,11 +52,10 @@ impl Config {
|
||||||
.chain(self.icals.iter().map(|ical| ical.dl.clone()))
|
.chain(self.icals.iter().map(|ical| ical.dl.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn upstream_calendars(&self) -> Vec<CalendarUi> {
|
fn upstreams(&self) -> Vec<CalendarUi> {
|
||||||
let Self {
|
let Self {
|
||||||
campfires,
|
campfires,
|
||||||
common_ninjas,
|
common_ninjas,
|
||||||
feeds: _,
|
|
||||||
icals,
|
icals,
|
||||||
output: _,
|
output: _,
|
||||||
} = self;
|
} = self;
|
||||||
|
@ -163,7 +161,7 @@ struct Data {
|
||||||
icals: Vec<wac_ical::Calendar>,
|
icals: Vec<wac_ical::Calendar>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_calendars(config: &Config) -> Result<Data> {
|
fn read_data_from_disk(config: &Config) -> Result<Data> {
|
||||||
Ok(Data {
|
Ok(Data {
|
||||||
campfires: config
|
campfires: config
|
||||||
.campfires
|
.campfires
|
||||||
|
@ -258,19 +256,9 @@ async fn do_everything(cli: &CliAuto) -> Result<()> {
|
||||||
std::fs::rename(&temp_path, &dl.file_path)?;
|
std::fs::rename(&temp_path, &dl.file_path)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let events = {
|
let data = read_data_from_disk(&config)?;
|
||||||
let cal_data = read_calendars(&config)?;
|
let instances = process_data(&data, &config.output, now)?;
|
||||||
process_data(&cal_data, &config.output, now)?
|
output::write_html(&config.output, &config.upstreams(), &instances, now)?;
|
||||||
};
|
|
||||||
let feed_items = read_feeds(&config)?;
|
|
||||||
|
|
||||||
output::write_html(
|
|
||||||
&config.output,
|
|
||||||
&feed_items,
|
|
||||||
&config.upstream_calendars(),
|
|
||||||
&events,
|
|
||||||
now,
|
|
||||||
)?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -292,40 +280,31 @@ fn main_auto(cli: CliAuto) -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(clap::Parser)]
|
#[derive(clap::Parser)]
|
||||||
struct CliDebugEvents {
|
struct CliDebugOutput {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
config: Utf8PathBuf,
|
config: Utf8PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main_debug_events(cli: CliDebugEvents) -> Result<()> {
|
fn main_debug_output(cli: CliDebugOutput) -> Result<()> {
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
tracing::info!("Started tracing");
|
tracing::info!("Started tracing");
|
||||||
let config = std::fs::read_to_string(&cli.config).context("Failed to read config file")?;
|
let config = std::fs::read_to_string(&cli.config).context("Failed to read config file")?;
|
||||||
let config: Config = toml::from_str(&config).context("Failed to parse config file")?;
|
let config: Config = toml::from_str(&config).context("Failed to parse config file")?;
|
||||||
|
|
||||||
|
let data = read_data_from_disk(&config).context("Failed to read data from disk")?;
|
||||||
|
|
||||||
let tz = &config.output.timezone;
|
let tz = &config.output.timezone;
|
||||||
let now = Utc::now().with_timezone(tz);
|
let now = Utc::now().with_timezone(tz);
|
||||||
let data = read_calendars(&config).context("Failed to read data from disk")?;
|
let instances = process_data(&data, &config.output, now).context("Failed to process data")?;
|
||||||
|
output::write_html(&config.output, &config.upstreams(), &instances, now)
|
||||||
let events = process_data(&data, &config.output, now).context("Failed to process data")?;
|
.context("Failed to output HTML")?;
|
||||||
|
|
||||||
std::fs::create_dir_all("output")?;
|
|
||||||
output::atomic_write(
|
|
||||||
"output/calendars.html",
|
|
||||||
&output::calendars_page(&config.upstream_calendars(), now),
|
|
||||||
)?;
|
|
||||||
output::atomic_write(
|
|
||||||
"output/index.html",
|
|
||||||
&output::index_page(&config.output, &events, now),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(clap::Parser)]
|
#[derive(clap::Parser)]
|
||||||
struct CliDebugFeed {
|
struct CliDebugRss {
|
||||||
#[arg(long)]
|
paths: Vec<Utf8PathBuf>,
|
||||||
config: Utf8PathBuf,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wraps rss::Item in our own type suitable for merging
|
/// Wraps rss::Item in our own type suitable for merging
|
||||||
|
@ -335,11 +314,12 @@ pub(crate) struct FeedItem {
|
||||||
inner: rss::Item,
|
inner: rss::Item,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_feeds(config: &Config) -> Result<Vec<FeedItem>> {
|
fn main_debug_rss(cli: CliDebugRss) -> Result<()> {
|
||||||
let mut items = Vec::new();
|
let mut items = Vec::new();
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
for feed in &config.feeds {
|
for path in &cli.paths {
|
||||||
let s = std::fs::read(&feed.file_path)?;
|
let s = std::fs::read(path)?;
|
||||||
let channel = rss::Channel::read_from(std::io::BufReader::new(std::io::Cursor::new(s)))?;
|
let channel = rss::Channel::read_from(std::io::BufReader::new(std::io::Cursor::new(s)))?;
|
||||||
|
|
||||||
let channel_title = channel.title.clone();
|
let channel_title = channel.title.clone();
|
||||||
|
@ -360,65 +340,30 @@ fn read_feeds(config: &Config) -> Result<Vec<FeedItem>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items.sort_by_key(|item| std::cmp::Reverse(item.date));
|
items.sort_by_key(|item| item.date);
|
||||||
Ok(items)
|
|
||||||
|
for item in items.iter().rev() {
|
||||||
|
println!("{}", item.channel_title);
|
||||||
|
println!("{}", item.inner.title.as_ref().unwrap());
|
||||||
|
println!("{}", item.date.to_rfc3339());
|
||||||
|
println!("{}", item.inner.link.as_ref().unwrap());
|
||||||
|
println!();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main_debug_feed(cli: CliDebugFeed) -> Result<()> {
|
|
||||||
tracing_subscriber::fmt::init();
|
|
||||||
tracing::info!("Started tracing");
|
|
||||||
let config = std::fs::read_to_string(&cli.config).context("Failed to read config file")?;
|
|
||||||
let config: Config = toml::from_str(&config).context("Failed to parse config file")?;
|
|
||||||
|
|
||||||
let tz = &config.output.timezone;
|
|
||||||
let now = Utc::now().with_timezone(tz);
|
|
||||||
let items = read_feeds(&config)?;
|
|
||||||
|
|
||||||
std::fs::create_dir_all("output")?;
|
std::fs::create_dir_all("output")?;
|
||||||
output::atomic_write("output/feed.html", &output::feed_page(&items, now))?;
|
output::atomic_write(
|
||||||
|
"output/feed.html",
|
||||||
Ok(())
|
&output::feed_page(&items, now.with_timezone(&chrono_tz::UTC)),
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(clap::Parser)]
|
|
||||||
struct CliDebugOutput {
|
|
||||||
#[arg(long)]
|
|
||||||
config: Utf8PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main_debug_output(cli: CliDebugOutput) -> Result<()> {
|
|
||||||
tracing_subscriber::fmt::init();
|
|
||||||
tracing::info!("Started tracing");
|
|
||||||
let config = std::fs::read_to_string(&cli.config).context("Failed to read config file")?;
|
|
||||||
let config: Config = toml::from_str(&config).context("Failed to parse config file")?;
|
|
||||||
|
|
||||||
let tz = &config.output.timezone;
|
|
||||||
let now = Utc::now().with_timezone(tz);
|
|
||||||
|
|
||||||
let events = {
|
|
||||||
let data = read_calendars(&config).context("Failed to read calendars from disk")?;
|
|
||||||
process_data(&data, &config.output, now).context("Failed to process data")?
|
|
||||||
};
|
|
||||||
|
|
||||||
let feed_items = read_feeds(&config)?;
|
|
||||||
|
|
||||||
std::fs::create_dir_all("output")?;
|
|
||||||
output::write_html(
|
|
||||||
&config.output,
|
|
||||||
&feed_items,
|
|
||||||
&config.upstream_calendars(),
|
|
||||||
&events,
|
|
||||||
now,
|
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(clap::Subcommand)]
|
#[derive(clap::Subcommand)]
|
||||||
enum Commands {
|
enum Commands {
|
||||||
Auto(CliAuto),
|
Auto(CliAuto),
|
||||||
DebugEvents(CliDebugEvents),
|
|
||||||
DebugFeed(CliDebugFeed),
|
|
||||||
DebugOutput(CliDebugOutput),
|
DebugOutput(CliDebugOutput),
|
||||||
|
DebugRss(CliDebugRss),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(clap::Parser)]
|
#[derive(clap::Parser)]
|
||||||
|
@ -433,8 +378,7 @@ fn main() -> Result<()> {
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Commands::Auto(x) => main_auto(x),
|
Commands::Auto(x) => main_auto(x),
|
||||||
Commands::DebugEvents(x) => main_debug_events(x),
|
|
||||||
Commands::DebugFeed(x) => main_debug_feed(x),
|
|
||||||
Commands::DebugOutput(x) => main_debug_output(x),
|
Commands::DebugOutput(x) => main_debug_output(x),
|
||||||
|
Commands::DebugRss(x) => main_debug_rss(x),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
333
src/output.rs
333
src/output.rs
|
@ -74,10 +74,7 @@ fn calendar_link(calendar_ui: &crate::CalendarUi) -> maud::PreEscaped<String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn calendars_page(
|
fn calendars_page(upstreams: &[crate::CalendarUi], now: DateTime<chrono_tz::Tz>) -> String {
|
||||||
upstreams: &[crate::CalendarUi],
|
|
||||||
now: DateTime<chrono_tz::Tz>,
|
|
||||||
) -> String {
|
|
||||||
let description = "A list of upstream calendars used by this Wide-Angle Calendar instance";
|
let description = "A list of upstream calendars used by this Wide-Angle Calendar instance";
|
||||||
let title = "Upstream calendars";
|
let title = "Upstream calendars";
|
||||||
|
|
||||||
|
@ -101,13 +98,13 @@ pub(crate) fn calendars_page(
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
h1 { (title) }
|
h1 { (title) }
|
||||||
p { "Written at: " (now.format("%F %T")) }
|
|
||||||
p {
|
p {
|
||||||
a href="index.html" { "Wide-Angle Calendar" }
|
a href="index.html" { "Wide-Angle Calendar" }
|
||||||
" / "
|
" / "
|
||||||
a href="calendars.html" { (title) }
|
a href="calendars.html" { (title) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p { "Written at: " (now.format("%F %T")) }
|
||||||
p { "These are the calendars that Wide-Angle Calendar pulls from." }
|
p { "These are the calendars that Wide-Angle Calendar pulls from." }
|
||||||
|
|
||||||
ol {
|
ol {
|
||||||
|
@ -121,103 +118,11 @@ pub(crate) fn calendars_page(
|
||||||
.into_string()
|
.into_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn feed_page(feed_items: &[crate::FeedItem], now: DateTime<chrono_tz::Tz>) -> String {
|
pub(crate) fn feed_page(_feed_items: &[crate::FeedItem], _now: DateTime<chrono_tz::Tz>) -> String {
|
||||||
let mut feed_items = feed_items.iter();
|
todo!()
|
||||||
let mut machine = date_machine::StateMachine::default();
|
|
||||||
let mut html_list = vec![];
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match machine.pull() {
|
|
||||||
date_machine::Output::Data(date_machine::DataChunk { month, date, items }) => {
|
|
||||||
if let Some(month) = month {
|
|
||||||
html_list.push(maud::html! { h2 { (month) } });
|
|
||||||
}
|
|
||||||
html_list.push(maud::html! {
|
|
||||||
(day_link(date))
|
|
||||||
hr{}
|
|
||||||
ul {
|
|
||||||
@for item in items {
|
|
||||||
li {
|
|
||||||
(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
date_machine::Output::EndOfInput => break,
|
|
||||||
date_machine::Output::NeedInput => {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(item) = feed_items.next() {
|
fn index_page(
|
||||||
let desc = item
|
|
||||||
.inner
|
|
||||||
.description
|
|
||||||
.as_ref()
|
|
||||||
.map(|desc| match desc.get(0..100) {
|
|
||||||
None => desc.to_string(),
|
|
||||||
Some(short_desc) => format!("{short_desc} ..."),
|
|
||||||
});
|
|
||||||
|
|
||||||
machine.push(
|
|
||||||
item.date.date_naive(),
|
|
||||||
maud::html! {
|
|
||||||
@if let Some(link) = &item.inner.link {
|
|
||||||
a href = (link) { (item.inner.title.as_deref().unwrap_or_default()) }
|
|
||||||
} @else {
|
|
||||||
(item.inner.title.as_deref().unwrap_or_default())
|
|
||||||
}
|
|
||||||
ul {
|
|
||||||
@if let Some(desc) = desc {
|
|
||||||
li { (desc) }
|
|
||||||
}
|
|
||||||
li { (item.channel_title) }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
machine.flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let description = "A feed merged from several input feeds";
|
|
||||||
let title = "Feed";
|
|
||||||
|
|
||||||
maud::html! {
|
|
||||||
(maud::PreEscaped("<!DOCTYPE html>"))
|
|
||||||
html lang="en" {
|
|
||||||
head {
|
|
||||||
meta http-equiv="Content-Type" content="text/html; charset=utf-8" {}
|
|
||||||
meta name="viewport" content="width=device-width, initial-scale=1" {}
|
|
||||||
(maud::PreEscaped(CSS))
|
|
||||||
|
|
||||||
meta property="og:locale" content="en" {}
|
|
||||||
meta property="og:type" content="website" {}
|
|
||||||
|
|
||||||
meta name="description" content=(description) {}
|
|
||||||
meta property="description" content=(description) {}
|
|
||||||
meta property="og:description" content=(description) {}
|
|
||||||
|
|
||||||
title { (title) }
|
|
||||||
met property="og:title" content=(title) {}
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
h1 { (title) }
|
|
||||||
p { "Written at: " (now.format("%F %T")) " (But it may lag by several hours - Not for ICE spotting)" }
|
|
||||||
p {
|
|
||||||
a href="index.html" { "Wide-Angle Calendar" }
|
|
||||||
" / "
|
|
||||||
a href="feed.html" { (title) }
|
|
||||||
}
|
|
||||||
@for entry in html_list {
|
|
||||||
(entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.into_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn index_page(
|
|
||||||
config: &Config,
|
config: &Config,
|
||||||
instances: &[EventInstance],
|
instances: &[EventInstance],
|
||||||
now: DateTime<chrono_tz::Tz>,
|
now: DateTime<chrono_tz::Tz>,
|
||||||
|
@ -343,13 +248,7 @@ pub(crate) fn index_page(
|
||||||
h1 { (title) }
|
h1 { (title) }
|
||||||
img src="hero.webp" width="700" height="233" {}
|
img src="hero.webp" width="700" height="233" {}
|
||||||
p { "Written at: " (now.format("%F %T")) }
|
p { "Written at: " (now.format("%F %T")) }
|
||||||
p {
|
p { a href = "calendars.html" { "Upstream calendars" } }
|
||||||
"Sub-pages:"
|
|
||||||
ul {
|
|
||||||
li { a href = "calendars.html" { "Upstream calendars" } }
|
|
||||||
li { a href = "feed.html" { "Feed" } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@for entry in html_list {
|
@for entry in html_list {
|
||||||
(entry)
|
(entry)
|
||||||
}
|
}
|
||||||
|
@ -368,224 +267,16 @@ pub(crate) fn atomic_write(path: &str, content: &str) -> Result<()> {
|
||||||
|
|
||||||
pub(crate) fn write_html(
|
pub(crate) fn write_html(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
feed_items: &[crate::FeedItem],
|
// feed_items: &[crate::FeedItem],
|
||||||
upstream_calendars: &[crate::CalendarUi],
|
upstreams: &[crate::CalendarUi],
|
||||||
events: &[EventInstance],
|
instances: &[EventInstance],
|
||||||
now: DateTime<chrono_tz::Tz>,
|
now: DateTime<chrono_tz::Tz>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
std::fs::create_dir_all("output")?;
|
std::fs::create_dir_all("output")?;
|
||||||
|
|
||||||
atomic_write(
|
atomic_write("output/calendars.html", &calendars_page(upstreams, now))?;
|
||||||
"output/calendars.html",
|
// atomic_write("output/feed.html", &feed_page(feed_items, now)?)?;
|
||||||
&calendars_page(upstream_calendars, now),
|
atomic_write("output/index.html", &index_page(config, instances, now))?;
|
||||||
)?;
|
|
||||||
atomic_write("output/feed.html", &feed_page(feed_items, now))?;
|
|
||||||
atomic_write("output/index.html", &index_page(config, events, now))?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Takes in a stream of items with dates, and formats them out categorized by month and date.
|
|
||||||
mod date_machine {
|
|
||||||
use chrono::NaiveDate;
|
|
||||||
use std::{cmp::PartialEq, collections::VecDeque, fmt};
|
|
||||||
|
|
||||||
pub(crate) struct DataChunk<T> {
|
|
||||||
pub(crate) month: Option<String>,
|
|
||||||
pub(crate) date: NaiveDate,
|
|
||||||
pub(crate) items: Vec<T>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug only if T: Debug
|
|
||||||
impl<T: fmt::Debug> fmt::Debug for DataChunk<T> {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
f.debug_struct("DataChunk")
|
|
||||||
.field("month", &self.month)
|
|
||||||
.field("date", &self.date)
|
|
||||||
.field("items", &self.items)
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PartialEq only if T: PartialEq
|
|
||||||
impl<T: PartialEq> PartialEq for DataChunk<T> {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.month == other.month && self.date == other.date && self.items == other.items
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) enum Output<T> {
|
|
||||||
Data(DataChunk<T>),
|
|
||||||
EndOfInput,
|
|
||||||
NeedInput,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug only if T: Debug
|
|
||||||
impl<T: fmt::Debug> fmt::Debug for Output<T> {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Output::Data(chunk) => f.debug_tuple("Data").field(chunk).finish(),
|
|
||||||
Output::EndOfInput => f.write_str("EndOfInput"),
|
|
||||||
Output::NeedInput => f.write_str("NeedInput"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PartialEq only if T: PartialEq
|
|
||||||
impl<T: PartialEq> PartialEq for Output<T> {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
match (self, other) {
|
|
||||||
(Output::Data(a), Output::Data(b)) => a == b,
|
|
||||||
(Output::EndOfInput, Output::EndOfInput) => true,
|
|
||||||
(Output::NeedInput, Output::NeedInput) => true,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub(crate) struct StateMachine<T> {
|
|
||||||
data: VecDeque<DataChunk<T>>,
|
|
||||||
flushed: bool,
|
|
||||||
last_month_printed: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> StateMachine<T> {
|
|
||||||
pub(crate) fn pull(&mut self) -> Output<T> {
|
|
||||||
if self.data.len() >= 2
|
|
||||||
&& let Some(chunk) = self.data.pop_front()
|
|
||||||
{
|
|
||||||
if chunk.month.is_some() && self.last_month_printed != chunk.month {
|
|
||||||
self.last_month_printed = chunk.month.clone();
|
|
||||||
}
|
|
||||||
return Output::Data(chunk);
|
|
||||||
}
|
|
||||||
if self.flushed {
|
|
||||||
if let Some(chunk) = self.data.pop_front() {
|
|
||||||
return Output::Data(chunk);
|
|
||||||
}
|
|
||||||
return Output::EndOfInput;
|
|
||||||
}
|
|
||||||
Output::NeedInput
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn push(&mut self, date: NaiveDate, item: T) {
|
|
||||||
let month = Some(date.format("%B").to_string());
|
|
||||||
let month = if month == self.last_month_printed {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
month
|
|
||||||
};
|
|
||||||
if let Some(chunk) = self.data.front_mut() {
|
|
||||||
if chunk.date == date {
|
|
||||||
chunk.items.push(item);
|
|
||||||
} else if chunk.month == month {
|
|
||||||
self.data.push_back(DataChunk {
|
|
||||||
month: None,
|
|
||||||
date,
|
|
||||||
items: vec![item],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
self.data.push_back(DataChunk {
|
|
||||||
month,
|
|
||||||
date,
|
|
||||||
items: vec![item],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.data.push_back(DataChunk {
|
|
||||||
month,
|
|
||||||
date,
|
|
||||||
items: vec![item],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn flush(&mut self) {
|
|
||||||
self.flushed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use chrono::NaiveDate;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn date_state_machine() {
|
|
||||||
use super::date_machine::*;
|
|
||||||
|
|
||||||
let mut machine: StateMachine<&str> = StateMachine::default();
|
|
||||||
let mut items = vec![
|
|
||||||
(2025, 09, 10, "alpha"),
|
|
||||||
(2025, 09, 10, "bravo"),
|
|
||||||
(2025, 09, 11, "charlie"),
|
|
||||||
(2025, 09, 12, "delta"),
|
|
||||||
(2025, 09, 13, "echo"),
|
|
||||||
(2025, 10, 1, "foxtrot"),
|
|
||||||
(2025, 10, 2, "golf"),
|
|
||||||
(2025, 10, 3, "hotel"),
|
|
||||||
]
|
|
||||||
.into_iter();
|
|
||||||
let mut output = vec![];
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match machine.pull() {
|
|
||||||
Output::Data(chunk) => {
|
|
||||||
output.push(chunk);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Output::EndOfInput => break,
|
|
||||||
Output::NeedInput => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some((y, m, d, item)) = items.next() {
|
|
||||||
let date = NaiveDate::from_ymd_opt(y, m, d).unwrap();
|
|
||||||
machine.push(date, item);
|
|
||||||
} else {
|
|
||||||
machine.flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let expected = vec![
|
|
||||||
DataChunk {
|
|
||||||
month: Some("September".to_string()),
|
|
||||||
date: NaiveDate::from_ymd_opt(2025, 09, 10).unwrap(),
|
|
||||||
items: vec!["alpha", "bravo"],
|
|
||||||
},
|
|
||||||
DataChunk {
|
|
||||||
month: None,
|
|
||||||
date: NaiveDate::from_ymd_opt(2025, 09, 11).unwrap(),
|
|
||||||
items: vec!["charlie"],
|
|
||||||
},
|
|
||||||
DataChunk {
|
|
||||||
month: None,
|
|
||||||
date: NaiveDate::from_ymd_opt(2025, 09, 12).unwrap(),
|
|
||||||
items: vec!["delta"],
|
|
||||||
},
|
|
||||||
DataChunk {
|
|
||||||
month: None,
|
|
||||||
date: NaiveDate::from_ymd_opt(2025, 09, 13).unwrap(),
|
|
||||||
items: vec!["echo"],
|
|
||||||
},
|
|
||||||
DataChunk {
|
|
||||||
month: Some("October".to_string()),
|
|
||||||
date: NaiveDate::from_ymd_opt(2025, 10, 1).unwrap(),
|
|
||||||
items: vec!["foxtrot"],
|
|
||||||
},
|
|
||||||
DataChunk {
|
|
||||||
month: None,
|
|
||||||
date: NaiveDate::from_ymd_opt(2025, 10, 2).unwrap(),
|
|
||||||
items: vec!["golf"],
|
|
||||||
},
|
|
||||||
DataChunk {
|
|
||||||
month: None,
|
|
||||||
date: NaiveDate::from_ymd_opt(2025, 10, 3).unwrap(),
|
|
||||||
items: vec!["hotel"],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
assert_eq!(output, expected, "{output:#?} != {expected:#?}");
|
|
||||||
|
|
||||||
assert_eq!("abc".get(0..100).unwrap_or("abc"), "abc");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue