Reuse table_formatting logic for almost everything (#39)

This commit is contained in:
huynd2001 2024-03-09 23:01:44 -05:00 committed by GitHub
parent 54426ed477
commit 13683aa229
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 273 additions and 388 deletions

View file

@ -1,3 +1,5 @@
use std::{collections::HashMap, sync::Arc, time::Duration};
use codeforces::Contest;
use serenity::{
builder::{CreateMessage, EditMessage},
@ -8,8 +10,15 @@ use serenity::{
model::{channel::Message, guild::Member},
utils::MessageBuilder,
};
use std::{collections::HashMap, sync::Arc, time::Duration};
use youmubot_prelude::*;
use db::{CfSavedUsers, CfUser};
pub use hook::InfoHook;
use youmubot_prelude::table_format::table_formatting_unsafe;
use youmubot_prelude::table_format::Align::{Left, Right};
use youmubot_prelude::{
table_format::{table_formatting, Align},
*,
};
mod announcer;
mod db;
@ -26,10 +35,6 @@ impl TypeMapKey for CFClient {
type Value = Arc<codeforces::Client>;
}
use db::{CfSavedUsers, CfUser};
pub use hook::InfoHook;
/// Sets up the CF databases.
pub async fn setup(path: &std::path::Path, data: &mut TypeMap, announcers: &mut AnnouncerHandler) {
CfSavedUsers::insert_into(data, path.join("cf_saved_users.yaml"))
@ -174,6 +179,7 @@ pub async fn ranks(ctx: &Context, m: &Message) -> CommandResult {
paginate_reply_fn(
move |page, ctx, msg| {
use Align::*;
let ranks = ranks.clone();
Box::pin(async move {
let page = page as usize;
@ -184,56 +190,37 @@ pub async fn ranks(ctx: &Context, m: &Message) -> CommandResult {
}
let ranks = &ranks[start..end];
let handle_width = ranks.iter().map(|(_, cfu)| cfu.handle.len()).max().unwrap();
let username_width = ranks
const HEADERS: [&'static str; 4] = ["Rank", "Rating", "Handle", "Username"];
const ALIGNS: [Align; 4] = [Right, Right, Left, Left];
let ranks_arr = ranks
.iter()
.map(|(mem, _)| mem.distinct().len())
.max()
.unwrap();
.enumerate()
.map(|(i, (mem, cfu))| {
[
format!("#{}", 1 + i + start),
cfu.rating
.map(|v| v.to_string())
.unwrap_or_else(|| "----".to_owned()),
cfu.handle.clone(),
mem.distinct(),
]
})
.collect::<Vec<_>>();
let mut m = MessageBuilder::new();
m.push_line("```");
let table = table_formatting(&HEADERS, &ALIGNS, ranks_arr);
// Table header
m.push_line(format!(
"Rank | Rating | {:hw$} | {:uw$}",
"Handle",
"Username",
hw = handle_width,
uw = username_width
));
m.push_line(format!(
"----------------{:->hw$}---{:->uw$}",
"",
"",
hw = handle_width,
uw = username_width
));
let content = MessageBuilder::new()
.push_line(table)
.push_line(format!(
"Page **{}/{}**. Last updated **{}**",
page + 1,
total_pages,
last_updated.to_rfc2822()
))
.build();
for (id, (mem, cfu)) in ranks.iter().enumerate() {
let id = id + start + 1;
m.push_line(format!(
"{:>4} | {:>6} | {:hw$} | {:uw$}",
format!("#{}", id),
cfu.rating
.map(|v| v.to_string())
.unwrap_or_else(|| "----".to_owned()),
cfu.handle,
mem.distinct(),
hw = handle_width,
uw = username_width
));
}
m.push_line("```");
m.push(format!(
"Page **{}/{}**. Last updated **{}**",
page + 1,
total_pages,
last_updated.to_rfc2822()
));
msg.edit(ctx, EditMessage::new().content(m.build())).await?;
msg.edit(ctx, EditMessage::new().content(content)).await?;
Ok(true)
})
},
@ -340,79 +327,66 @@ pub(crate) async fn contest_rank_table(
return Ok(false);
}
let ranks = &ranks[start..end];
let hw = ranks
.iter()
.map(|(mem, handle, _)| format!("{} ({})", handle, mem.distinct()).len())
.max()
.unwrap_or(0)
.max(6);
let hackw = ranks
.iter()
.map(|(_, _, row)| {
format!(
"{}/{}",
row.successful_hack_count, row.unsuccessful_hack_count
)
.len()
})
.max()
.unwrap_or(0)
.max(5);
let mut table = MessageBuilder::new();
let mut header = MessageBuilder::new();
// Header
header.push(format!(
" Rank | {:hw$} | Total | {:hackw$}",
"Handle",
"Hacks",
hw = hw,
hackw = hackw
));
for p in &problems {
header.push(format!(" | {:4}", p.index));
}
let header = header.build();
table
.push_line(&header)
.push_line(format!("{:-<w$}", "", w = header.len()));
let score_headers: Vec<&str> = [
vec!["Rank", "Handle", "User", "Total", "Hacks"],
problems
.iter()
.map(|p| p.index.as_str())
.collect::<Vec<&str>>(),
]
.concat();
// Body
for (mem, handle, row) in ranks {
table.push(format!(
"{:>5} | {:<hw$} | {:>5.0} | {:<hackw$}",
row.rank,
format!("{} ({})", handle, mem.distinct()),
row.points,
format!(
"{}/{}",
row.successful_hack_count, row.unsuccessful_hack_count
),
hw = hw,
hackw = hackw
));
for p in &row.problem_results {
table.push(" | ");
if p.points > 0.0 {
table.push(format!("{:^4.0}", p.points));
} else if p.best_submission_time_seconds.is_some() {
table.push(format!("{:^4}", "?"));
} else if p.rejected_attempt_count > 0 {
table.push(format!("{:^4}", format!("-{}", p.rejected_attempt_count)));
} else {
table.push(format!("{:^4}", ""));
let score_aligns: Vec<Align> = [
vec![Right, Left, Left, Right, Right],
problems.iter().map(|_| Right).collect::<Vec<Align>>(),
]
.concat();
let score_arr = ranks
.iter()
.map(|(mem, handle, row)| {
let mut p_results: Vec<String> = Vec::new();
for result in &row.problem_results {
if result.points > 0.0 {
p_results.push(format!("{}", result.points));
} else if result.best_submission_time_seconds.is_some() {
p_results.push(format!("{}", "?"));
} else if result.rejected_attempt_count > 0 {
p_results.push(format!("-{}", result.rejected_attempt_count));
} else {
p_results.push(format!("{}", "----"));
}
}
}
table.push_line("");
}
let mut m = MessageBuilder::new();
m.push_bold_safe(&contest.name)
[
vec![
format!("{}", row.rank),
handle.clone(),
mem.distinct(),
format!("{}", row.points),
format!(
"{}/{}",
row.successful_hack_count, row.unsuccessful_hack_count
),
],
p_results,
]
.concat()
})
.collect::<Vec<_>>();
let score_table = table_formatting_unsafe(&score_headers, &score_aligns, score_arr);
let content = MessageBuilder::new()
.push_bold_safe(&contest.name)
.push(" ")
.push_line(contest.url())
.push_codeblock(table.build(), None)
.push_line(format!("Page **{}/{}**", page + 1, total_pages));
msg.edit(ctx, EditMessage::new().content(m.build())).await?;
.push_line(score_table)
.push_line(format!("Page **{}/{}**", page + 1, total_pages))
.build();
msg.edit(ctx, EditMessage::new().content(content)).await?;
Ok(true)
})
},

View file

@ -1,4 +1,3 @@
use crate::db::Roles as DB;
use serenity::{
builder::EditMessage,
framework::standard::{macros::command, Args, CommandResult},
@ -9,9 +8,13 @@ use serenity::{
},
utils::MessageBuilder,
};
use youmubot_prelude::*;
pub use reaction_watcher::Watchers as ReactionWatchers;
use youmubot_prelude::table_format::Align::Right;
use youmubot_prelude::table_format::{table_formatting, Align};
use youmubot_prelude::*;
use crate::db::Roles as DB;
#[command("listroles")]
#[description = "List all available roles in the server."]
@ -50,59 +53,31 @@ async fn list(ctx: &Context, m: &Message, _: Args) -> CommandResult {
if end <= start {
return Ok(false);
}
let roles = &roles[start..end];
let nw = roles // name width
const ROLE_HEADERS: [&'static str; 3] = ["Name", "ID", "Description"];
const ROLE_ALIGNS: [Align; 3] = [Right, Right, Right];
let roles_arr = roles
.iter()
.map(|(r, _)| r.name.len())
.max()
.unwrap()
.max(6);
let idw = roles[0].0.id.to_string().len();
let dw = roles
.iter()
.map(|v| v.1.len())
.max()
.unwrap()
.max(" Description ".len());
let mut m = MessageBuilder::new();
m.push_line("```");
.map(|(role, description)| {
[
role.name.clone(),
format!("{}", role.id),
description.clone(),
]
})
.collect::<Vec<_>>();
// Table header
m.push_line(format!(
"{:nw$} | {:idw$} | {:dw$}",
"Name",
"ID",
"Description",
nw = nw,
idw = idw,
dw = dw,
));
m.push_line(format!(
"{:->nw$}---{:->idw$}---{:->dw$}",
"",
"",
"",
nw = nw,
idw = idw,
dw = dw,
));
let roles_table = table_formatting(&ROLE_HEADERS, &ROLE_ALIGNS, roles_arr);
for (role, description) in roles.iter() {
m.push_line(format!(
"{:nw$} | {:idw$} | {:dw$}",
role.name,
role.id,
description,
nw = nw,
idw = idw,
dw = dw,
));
}
m.push_line("```");
m.push(format!("Page **{}/{}**", page + 1, pages));
let content = MessageBuilder::new()
.push_line(roles_table)
.push_line(format!("Page **{}/{}**", page + 1, pages))
.build();
msg.edit(ctx, EditMessage::new().content(m.to_string()))
.await?;
msg.edit(ctx, EditMessage::new().content(content)).await?;
Ok(true)
})
},
@ -415,7 +390,6 @@ async fn rmrolemessage(ctx: &Context, m: &Message, _args: Args) -> CommandResult
}
mod reaction_watcher {
use crate::db::{Role, RoleMessage, Roles};
use dashmap::DashMap;
use flume::{Receiver, Sender};
use serenity::{
@ -427,8 +401,11 @@ mod reaction_watcher {
id::{ChannelId, GuildId, MessageId},
},
};
use youmubot_prelude::*;
use crate::db::{Role, RoleMessage, Roles};
/// A set of watchers.
#[derive(Debug)]
pub struct Watchers {

View file

@ -2,10 +2,12 @@ pub use beatmapset::display_beatmapset;
pub use scores::ScoreListStyle;
mod scores {
use crate::models::{Mode, Score};
use serenity::{framework::standard::CommandResult, model::channel::Message};
use youmubot_prelude::*;
use crate::models::{Mode, Score};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// The style for the scores list to be displayed.
pub enum ScoreListStyle {
@ -47,13 +49,15 @@ mod scores {
}
pub mod grid {
use serenity::builder::EditMessage;
use serenity::{framework::standard::CommandResult, model::channel::Message};
use youmubot_prelude::*;
use crate::discord::{
cache::save_beatmap, BeatmapCache, BeatmapMetaCache, BeatmapWithMode,
};
use crate::models::{Mode, Score};
use serenity::builder::EditMessage;
use serenity::{framework::standard::CommandResult, model::channel::Message};
use youmubot_prelude::*;
pub async fn display_scores_grid<'a>(
scores: Vec<Score>,
@ -126,12 +130,16 @@ mod scores {
pub mod table {
use std::borrow::Cow;
use serenity::builder::EditMessage;
use serenity::{framework::standard::CommandResult, model::channel::Message};
use youmubot_prelude::table_format::Align::{Left, Right};
use youmubot_prelude::table_format::{table_formatting, Align};
use youmubot_prelude::*;
use crate::discord::oppai_cache::Accuracy;
use crate::discord::{Beatmap, BeatmapCache, BeatmapInfo, BeatmapMetaCache};
use crate::models::{Mode, Score};
use serenity::builder::EditMessage;
use serenity::{framework::standard::CommandResult, model::channel::Message};
use youmubot_prelude::*;
pub async fn display_scores_table<'a>(
scores: Vec<Score>,
@ -196,7 +204,8 @@ mod scores {
.collect::<stream::FuturesOrdered<_>>()
.map(|v| v.ok())
.collect::<Vec<_>>();
let pp = plays
let pps = plays
.iter()
.map(|p| async move {
match p.pp.map(|pp| format!("{:.2}", pp)) {
@ -226,7 +235,8 @@ mod scores {
.collect::<stream::FuturesOrdered<_>>()
.map(|v| v.unwrap_or_else(|_| "-".to_owned()))
.collect::<Vec<String>>();
let (beatmaps, pp) = future::join(beatmaps, pp).await;
let (beatmaps, pps) = future::join(beatmaps, pps).await;
let ranks = plays
.iter()
@ -274,64 +284,36 @@ mod scores {
})
.collect::<Vec<_>>();
let pw = pp.iter().map(|v| v.len()).max().unwrap_or(2);
/*mods width*/
let mw = plays.iter().map(|v| v.mods.str_len()).max().unwrap().max(4);
/*beatmap names*/
let bw = beatmaps.iter().map(|v| v.len()).max().unwrap().max(7);
/* ranks width */
let rw = ranks.iter().map(|v| v.len()).max().unwrap().max(5);
const SCORE_HEADERS: [&'static str; 6] =
["#", "PP", "Acc", "Ranks", "Mods", "Beatmap"];
const SCORE_ALIGNS: [Align; 6] = [Right, Right, Right, Right, Right, Left];
let mut m = serenity::utils::MessageBuilder::new();
// Table header
m.push_line(format!(
" # | {:pw$} | accuracy | {:rw$} | {:mw$} | {:bw$}",
"pp",
"ranks",
"mods",
"beatmap",
rw = rw,
pw = pw,
mw = mw,
bw = bw
));
m.push_line(format!(
"------{:-<pw$}--------------{:-<rw$}---{:-<mw$}---{:-<bw$}",
"",
"",
"",
"",
rw = rw,
pw = pw,
mw = mw,
bw = bw
));
// Each row
for (id, (play, beatmap)) in plays.iter().zip(beatmaps.iter()).enumerate() {
m.push_line(format!(
"{:>3} | {:>pw$} | {:>8} | {:>rw$} | {} | {:bw$}",
id + start + 1,
pp[id],
format!("{:.2}%", play.accuracy(self.mode)),
ranks[id],
play.mods.to_string_padded(mw),
beatmap,
rw = rw,
pw = pw,
bw = bw
));
}
// End
let table = m.build().replace("```", "\\`\\`\\`");
let mut m = serenity::utils::MessageBuilder::new();
m.push_codeblock(table, None).push_line(format!(
"Page **{}/{}**",
page + 1,
self.total_pages()
));
m.push_line("[?] means pp was predicted by oppai-rs.");
msg.edit(ctx, EditMessage::new().content(m.to_string()))
.await?;
let score_arr = plays
.iter()
.zip(beatmaps.iter())
.zip(ranks.iter().zip(pps.iter()))
.enumerate()
.map(|(id, ((play, beatmap), (rank, pp)))| {
[
format!("{}", id + start + 1),
format!("{}", pp),
format!("{:.2}%", play.accuracy(self.mode)),
format!("{}", rank),
play.mods.to_string(),
beatmap.clone(),
]
})
.collect::<Vec<_>>();
let score_table = table_formatting(&SCORE_HEADERS, &SCORE_ALIGNS, score_arr);
let content = serenity::utils::MessageBuilder::new()
.push_line(score_table)
.push_line(format!("Page **{}/{}**", page + 1, self.total_pages()))
.push_line("[?] means pp was predicted by oppai-rs.")
.build();
msg.edit(ctx, EditMessage::new().content(content)).await?;
hourglass.delete(ctx).await?;
Ok(true)
}
@ -344,20 +326,22 @@ mod scores {
}
mod beatmapset {
use crate::{
discord::{
cache::save_beatmap, oppai_cache::BeatmapInfoWithPP, BeatmapCache, BeatmapWithMode,
},
models::{Beatmap, Mode, Mods},
};
use serenity::{
all::Reaction,
builder::{CreateEmbedFooter, EditMessage},
model::channel::Message,
model::channel::ReactionType,
};
use youmubot_prelude::*;
use crate::{
discord::{
cache::save_beatmap, oppai_cache::BeatmapInfoWithPP, BeatmapCache, BeatmapWithMode,
},
models::{Beatmap, Mode, Mods},
};
const SHOW_ALL_EMOTE: &str = "🗒️";
pub async fn display_beatmapset(
@ -449,24 +433,24 @@ mod beatmapset {
}
};
m.edit(ctx,
EditMessage::new().content(self.message.as_str()).embed(
crate::discord::embeds::beatmap_embed(
map,
self.mode.unwrap_or(map.mode),
self.mods,
info,
)
.footer( {
CreateEmbedFooter::new(format!(
"Difficulty {}/{}. To show all difficulties in a single embed (old style), react {}",
page + 1,
self.maps.len(),
SHOW_ALL_EMOTE,
))
})
)
EditMessage::new().content(self.message.as_str()).embed(
crate::discord::embeds::beatmap_embed(
map,
self.mode.unwrap_or(map.mode),
self.mods,
info,
)
.footer({
CreateEmbedFooter::new(format!(
"Difficulty {}/{}. To show all difficulties in a single embed (old style), react {}",
page + 1,
self.maps.len(),
SHOW_ALL_EMOTE,
))
})
),
)
.await?;
.await?;
save_beatmap(
&*ctx.data.read().await,
m.channel_id,

View file

@ -1,6 +1,19 @@
use std::{collections::HashMap, str::FromStr, sync::Arc};
use super::{db::OsuSavedUsers, ModeArg, OsuClient};
use serenity::{
builder::EditMessage,
framework::standard::{macros::command, Args, CommandResult},
model::channel::Message,
utils::MessageBuilder,
};
use youmubot_prelude::table_format::Align::{Left, Right};
use youmubot_prelude::{
stream::FuturesUnordered,
table_format::{table_formatting, Align},
*,
};
use crate::{
discord::{
display::ScoreListStyle,
@ -10,17 +23,7 @@ use crate::{
request::UserID,
};
use serenity::{
builder::EditMessage,
framework::standard::{macros::command, Args, CommandResult},
model::channel::Message,
utils::MessageBuilder,
};
use youmubot_prelude::{
stream::FuturesUnordered,
table_format::{table_formatting, Align},
*,
};
use super::{db::OsuSavedUsers, ModeArg, OsuClient};
#[derive(Debug, Clone, Copy)]
enum RankQuery {
@ -345,124 +348,63 @@ pub async fn show_leaderboard(ctx: &Context, m: &Message, mut args: Args) -> Com
let scores = scores[start..end].to_vec();
let bm = (bm.0.clone(), bm.1);
Box::pin(async move {
// username width
let uw = scores
const SCORE_HEADERS: [&'static str; 8] =
["#", "Score", "Mods", "Rank", "Acc", "Combo", "Miss", "User"];
const PP_HEADERS: [&'static str; 8] =
["#", "PP", "Mods", "Rank", "Acc", "Combo", "Miss", "User"];
const ALIGNS: [Align; 8] = [Right, Right, Right, Right, Right, Right, Right, Left];
let score_arr = scores
.iter()
.map(|(_, u, _)| u.len())
.max()
.unwrap_or(8)
.max(8);
let accuracies = scores
.iter()
.map(|(_, _, v)| format!("{:.2}%", v.accuracy(bm.1)))
.collect::<Vec<_>>();
let aw = accuracies.iter().map(|v| v.len()).max().unwrap().max(3);
let misses = scores
.iter()
.map(|(_, _, v)| format!("{}", v.count_miss))
.collect::<Vec<_>>();
let mw = misses.iter().map(|v| v.len()).max().unwrap().max(4);
let ranks = scores
.iter()
.map(|(_, _, v)| v.rank.to_string())
.collect::<Vec<_>>();
let rw = ranks.iter().map(|v| v.len()).max().unwrap().max(4);
let pp_label = match order {
OrderBy::PP => "pp",
OrderBy::Score => "score",
};
let pp = scores
.iter()
.map(|((official, pp), _, s)| match order {
OrderBy::PP => format!("{:.2}{}", pp, if *official { "" } else { "[?]" }),
OrderBy::Score => crate::discord::embeds::grouped_number(if has_lazer_score { s.normalized_score as u64 } else { s.score.unwrap() }),
.enumerate()
.map(|(id, ((official, pp), member, score))| {
[
format!("{}", 1 + id + start),
match order {
OrderBy::PP => {
format!("{:.2}{}", pp, if *official { "" } else { "[?]" })
}
OrderBy::Score => {
crate::discord::embeds::grouped_number(if has_lazer_score {
score.normalized_score as u64
} else {
score.score.unwrap()
})
}
},
score.mods.to_string(),
score.rank.to_string(),
format!("{:.2}%", score.accuracy(bm.1)),
format!("{}x", score.max_combo),
format!("{}", score.count_miss),
member.to_string(),
]
})
.collect::<Vec<_>>();
let pw = pp.iter().map(|v| v.len()).max().unwrap_or(pp_label.len());
/*mods width*/
let mdw = scores
.iter()
.map(|(_, _, v)| v.mods.str_len())
.max()
.unwrap()
.max(4);
let combos = scores
.iter()
.map(|(_, _, v)| format!("{}x", v.max_combo))
.collect::<Vec<_>>();
let cw = combos
.iter()
.map(|v| v.len())
.max()
.unwrap()
.max(5);
let mut content = MessageBuilder::new();
content
.push_line("```")
.push_line(format!(
"rank | {:>pw$} | {:mdw$} | {:rw$} | {:>aw$} | {:>cw$} | {:mw$} | {:uw$}",
pp_label,
"mods",
"rank",
"acc",
"combo",
"miss",
"user",
pw = pw,
mdw = mdw,
rw = rw,
aw = aw,
mw = mw,
uw = uw,
cw = cw,
))
.push_line(format!(
"-------{:-<pw$}---{:-<mdw$}---{:-<rw$}---{:-<aw$}---{:-<cw$}---{:-<mw$}---{:-<uw$}",
"",
"",
"",
"",
"",
"",
"",
pw = pw,
mdw = mdw,
rw = rw,
aw = aw,
mw = mw,
uw = uw,
cw = cw,
));
for (id, (_, member, p)) in scores.iter().enumerate() {
content.push_line_safe(format!(
"{:>4} | {:>pw$} | {} | {:>rw$} | {:>aw$} | {:>cw$} | {:>mw$} | {:uw$}",
format!("#{}", 1 + id + start),
pp[id],
p.mods.to_string_padded(mdw),
ranks[id],
accuracies[id],
combos[id],
misses[id],
member,
pw = pw,
rw = rw,
aw = aw,
cw = cw,
mw = mw,
uw = uw,
));
}
content.push_line("```").push_line(format!(
"Page **{}**/**{}**. Not seeing your scores? Run `osu check` to update.",
page + 1,
(total_len + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE,
));
if let crate::models::ApprovalStatus::Ranked(_) = bm.0.approval {
} else if order == OrderBy::PP {
content.push_line("PP was calculated by `oppai-rs`, **not** official values.");
}
m.edit(&ctx, EditMessage::new().content(content.build())).await?;
let score_table = match order {
OrderBy::PP => table_formatting(&PP_HEADERS, &ALIGNS, score_arr),
OrderBy::Score => table_formatting(&SCORE_HEADERS, &ALIGNS, score_arr),
};
let content = MessageBuilder::new()
.push_line(score_table)
.push_line(format!(
"Page **{}**/**{}**. Not seeing your scores? Run `osu check` to update.",
page + 1,
(total_len + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE,
))
.push(
if let crate::models::ApprovalStatus::Ranked(_) = bm.0.approval {
""
} else if order == OrderBy::PP {
"PP was calculated by `oppai-rs`, **not** official values.\n"
} else {
""
},
)
.build();
m.edit(&ctx, EditMessage::new().content(content)).await?;
Ok(true)
})
},

View file

@ -17,9 +17,9 @@ impl Align {
}
}
pub fn table_formatting<const N: usize, S: AsRef<str> + std::fmt::Debug, Ts: AsRef<[[S; N]]>>(
headers: &[&'static str; N],
padding: &[Align; N],
pub fn table_formatting_unsafe<S: AsRef<str> + std::fmt::Debug, Ss: AsRef<[S]>, Ts: AsRef<[Ss]>>(
headers: &[&str],
padding: &[Align],
table: Ts,
) -> String {
let table = table.as_ref();
@ -68,3 +68,11 @@ pub fn table_formatting<const N: usize, S: AsRef<str> + std::fmt::Debug, Ts: AsR
m.push("```");
m.build()
}
pub fn table_formatting<const N: usize, S: AsRef<str> + std::fmt::Debug, Ts: AsRef<[[S; N]]>>(
headers: &[&'static str; N],
padding: &[Align; N],
table: Ts,
) -> String {
table_formatting_unsafe(headers, padding, table)
}