From 13683aa2297e7c3fb495a24987a96bd4dda09bbe Mon Sep 17 00:00:00 2001 From: Nguyen Duc Huy Date: Sat, 9 Mar 2024 23:01:44 -0500 Subject: [PATCH] Reuse `table_formatting` logic for almost everything (#39) --- youmubot-cf/src/lib.rs | 212 +++++++++++------------- youmubot-core/src/community/roles.rs | 79 ++++----- youmubot-osu/src/discord/display.rs | 162 ++++++++---------- youmubot-osu/src/discord/server_rank.rs | 194 ++++++++-------------- youmubot-prelude/src/table_format.rs | 14 +- 5 files changed, 273 insertions(+), 388 deletions(-) diff --git a/youmubot-cf/src/lib.rs b/youmubot-cf/src/lib.rs index 2cbae58..61c45a8 100644 --- a/youmubot-cf/src/lib.rs +++ b/youmubot-cf/src/lib.rs @@ -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; } -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::>(); - 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!("{:- = [ + vec!["Rank", "Handle", "User", "Total", "Hacks"], + problems + .iter() + .map(|p| p.index.as_str()) + .collect::>(), + ] + .concat(); - // Body - for (mem, handle, row) in ranks { - table.push(format!( - "{:>5} | {:5.0} | {: 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 = [ + vec![Right, Left, Left, Right, Right], + problems.iter().map(|_| Right).collect::>(), + ] + .concat(); + + let score_arr = ranks + .iter() + .map(|(mem, handle, row)| { + let mut p_results: Vec = 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::>(); + + 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) }) }, diff --git a/youmubot-core/src/community/roles.rs b/youmubot-core/src/community/roles.rs index 8d340e6..0e2024e 100644 --- a/youmubot-core/src/community/roles.rs +++ b/youmubot-core/src/community/roles.rs @@ -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::>(); - // 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 { diff --git a/youmubot-osu/src/discord/display.rs b/youmubot-osu/src/discord/display.rs index a8b29be..68755ef 100644 --- a/youmubot-osu/src/discord/display.rs +++ b/youmubot-osu/src/discord/display.rs @@ -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, @@ -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, @@ -196,7 +204,8 @@ mod scores { .collect::>() .map(|v| v.ok()) .collect::>(); - 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::>() .map(|v| v.unwrap_or_else(|_| "-".to_owned())) .collect::>(); - 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::>(); - 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!( - "------{:-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::>(); + + 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, diff --git a/youmubot-osu/src/discord/server_rank.rs b/youmubot-osu/src/discord/server_rank.rs index 40bc22e..2d3307a 100644 --- a/youmubot-osu/src/discord/server_rank.rs +++ b/youmubot-osu/src/discord/server_rank.rs @@ -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::>(); - let aw = accuracies.iter().map(|v| v.len()).max().unwrap().max(3); - let misses = scores - .iter() - .map(|(_, _, v)| format!("{}", v.count_miss)) - .collect::>(); - let mw = misses.iter().map(|v| v.len()).max().unwrap().max(4); - let ranks = scores - .iter() - .map(|(_, _, v)| v.rank.to_string()) - .collect::>(); - 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::>(); - 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::>(); - 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!( - "-------{:-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) }) }, diff --git a/youmubot-prelude/src/table_format.rs b/youmubot-prelude/src/table_format.rs index f44cf06..80d9c31 100644 --- a/youmubot-prelude/src/table_format.rs +++ b/youmubot-prelude/src/table_format.rs @@ -17,9 +17,9 @@ impl Align { } } -pub fn table_formatting + std::fmt::Debug, Ts: AsRef<[[S; N]]>>( - headers: &[&'static str; N], - padding: &[Align; N], +pub fn table_formatting_unsafe + 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 + std::fmt::Debug, Ts: AsR m.push("```"); m.build() } + +pub fn table_formatting + std::fmt::Debug, Ts: AsRef<[[S; N]]>>( + headers: &[&'static str; N], + padding: &[Align; N], + table: Ts, +) -> String { + table_formatting_unsafe(headers, padding, table) +}