upload code

This commit is contained in:
Dominic 2022-10-16 20:14:55 +02:00
commit 3f507064ce
Signed by: msrd0
GPG key ID: DCC8C247452E98F9
111 changed files with 9258 additions and 0 deletions

16
stdlib/Cargo.toml Normal file
View file

@ -0,0 +1,16 @@
# -*- eval: (cargo-minor-mode 1) -*-
[package]
name = "stdlib"
version = "0.0.0"
publish = false
edition = "2021"
authors = ["Dominic Meiser <git@msrd0.de>"]
[dependencies]
indexmap = "1.9"
itoa = "1"
num-integer = "0.1.45"
paste = "1"
rand = { version = "0.8", features = ["getrandom"], default-features = false }
svgwriter = "0.1"

145
stdlib/src/lib.rs Normal file
View file

@ -0,0 +1,145 @@
use rand::{
distributions::{Distribution as _, Uniform},
rngs::OsRng,
Rng
};
use std::{
fmt::{self, Debug, Formatter, Write as _},
panic::panic_any
};
pub use std::{
option::Option,
primitive::{bool, isize as int}
};
mod output;
pub use output::{
BarChartPrinter, BubbleChartPrinter, ListPrinter, OutputList, OutputPrinter,
OutputPrinterAny, OutputPrinterTwoVars, OutputTarget
};
#[doc(hidden)]
pub struct FnNoReturn {
msg: String
}
/// Called as the last part of every function to abnormally terminate in case there is a
/// missing return statement.
#[track_caller]
pub fn fn_no_return(fun_ident: &str) -> FnNoReturn {
FnNoReturn {
msg: format!(
r#"Function `{}` has finished without returning a value, but it declares a return type.
Help: The function was called with the following arguments:"#,
fun_ident
)
}
}
impl FnNoReturn {
pub fn add_input<T>(mut self, key: &'static str, value: &T) -> Self
where
T: Debug
{
write!(self.msg, "\n\t{key}: {value:#?}").unwrap();
self
}
pub fn panic(self) -> ! {
panic_any(self.msg)
}
}
/// Return the default (pseudo) random number generator.
fn rng() -> impl Rng {
OsRng::default()
}
/// Used for input argument parsing.
#[derive(Debug, Clone, Copy)]
pub enum ArgValue {
Value(int),
Range { min: int, max: int }
}
impl ArgValue {
pub fn get_value(self) -> int {
match self {
Self::Value(value) => value,
Self::Range { min, max } => {
Uniform::new_inclusive(min, max).sample(&mut rng())
},
}
}
}
/// The result of a coin flip.
pub enum Coin {
Head,
Tail
}
impl Coin {
/// Flip a coin, returning [`Coin::Head`] with probability `prob`.
pub fn flip(prob: f32) -> Self {
debug_assert!(prob > 0.0);
debug_assert!(prob < 1.0);
if rng().gen::<f32>() < prob {
Self::Head
} else {
Self::Tail
}
}
}
/// The `Rc` reference-counting smart pointer of the language.
pub struct Rc<T>(std::rc::Rc<std::cell::RefCell<T>>);
impl<T> Rc<T> {
pub fn new(inner: T) -> Self {
Self(std::rc::Rc::new(std::cell::RefCell::new(inner)))
}
pub fn into_inner(this: Rc<T>) -> T
where
T: Clone
{
match std::rc::Rc::try_unwrap(this.0) {
Ok(cell) => cell.into_inner(),
Err(rc) => rc.borrow().clone()
}
}
pub fn with_mut<F>(&mut self, callback: F)
where
F: FnOnce(&mut T)
{
let mut this = self.0.borrow_mut();
callback(&mut this);
}
}
impl<T> Clone for Rc<T> {
fn clone(&self) -> Self {
Self(std::rc::Rc::clone(&self.0))
}
}
impl<T: Debug> Debug for Rc<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_tuple("Rc")
.field(&DebugPtr(self.0.as_ptr()))
.field(&self.0.borrow())
.finish()
}
}
struct DebugPtr<T>(*const T);
impl<T> Debug for DebugPtr<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{:#X}", self.0 as usize)
}
}

356
stdlib/src/output/bar.rs Normal file
View file

@ -0,0 +1,356 @@
use super::{OutputList, OutputPrinter, Svg};
use crate::int;
use std::{
cmp::Ordering,
collections::BTreeMap,
fmt::Write as _,
io::{self, Write},
mem
};
use svgwriter::{
tags::{Group, Path, Rect, Style, TSpan, TagWithPresentationAttributes as _, Text},
Data, Transform
};
/// Output printer for SVG Bar Charts. Works best with one
/// output variable.
pub struct BarChartPrinter {
/// Set to true to enable SVG/XML pretty printing.
pretty: bool
}
impl BarChartPrinter {
pub fn new(pretty: bool) -> Self {
Self { pretty }
}
}
#[derive(Default)]
struct YAxis {
percentages: Vec<f32>,
max: f32
}
impl YAxis {
fn add_percentage(&mut self, percentage: f32) {
self.percentages.push(percentage);
self.max = self.max.max(percentage).ceil();
}
}
#[derive(Clone, Copy, Eq)]
enum Range {
Single(int),
Range { start: int, len: int }
}
impl Range {
fn start(&self) -> int {
match self {
Self::Single(value) => *value,
Self::Range { start, .. } => *start
}
}
fn end(&self) -> int {
match self {
Self::Single(value) => *value,
Self::Range { start, len } => start + len - 1
}
}
}
impl PartialEq for Range {
fn eq(&self, other: &Self) -> bool {
self.start() == other.start() && self.end() == other.end()
}
}
impl PartialOrd for Range {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Range {
fn cmp(&self, other: &Self) -> Ordering {
if self == other {
Ordering::Equal
} else if self.start() == other.start() {
self.end().cmp(&other.end())
} else {
self.start().cmp(&other.start())
}
}
}
#[derive(Default)]
struct XAxis {
values: BTreeMap<Range, usize>,
max: Option<int>,
min: Option<int>
}
impl XAxis {
fn add_value(&mut self, value: int, qty: usize) {
let range = Range::Single(value);
if let Some(range_qty) = self.values.get_mut(&range) {
*range_qty += qty;
} else {
self.values.insert(range, qty);
}
if self.max.map(|max| max < value).unwrap_or(true) {
self.max = Some(value);
}
if self.min.map(|min| min > value).unwrap_or(true) {
self.min = Some(value);
}
}
fn with_step(&mut self, step: int) {
let values = mem::take(&mut self.values);
for (range, qty) in values {
let value = match range {
Range::Single(value) => value,
_ => panic!("with_step may only be called once")
};
let offset = value - self.min.unwrap();
let start = self.min.unwrap() + (offset / step) * step;
let range = Range::Range { start, len: step };
if let Some(range_qty) = self.values.get_mut(&range) {
*range_qty += qty;
} else {
self.values.insert(range, qty);
}
}
}
}
impl OutputPrinter for BarChartPrinter {
fn print<I, E, W>(self, list: OutputList<I>, mut out: W) -> io::Result<()>
where
I: IntoIterator<Item = (E, usize)>,
E: IntoIterator<Item = (&'static str, int)>,
W: Write
{
let mut yaxis = YAxis::default();
let mut xaxis = XAxis::default();
let mut xaxis_name = None;
for (entry, qty) in list.samples {
for (key, value) in entry {
if xaxis_name.is_none() {
xaxis_name = Some(key);
} else if xaxis_name.unwrap() != key {
continue;
}
xaxis.add_value(value, qty);
}
}
let xaxis_name = xaxis_name.expect("Missing output variable");
// group the xaxis together
let mut xaxis_step = 1;
while (xaxis.max.unwrap() - xaxis.min.unwrap()) / xaxis_step > 12 {
xaxis_step += 1;
}
xaxis.with_step(xaxis_step);
// build the yaxis
for qty in xaxis.values.values().copied() {
yaxis.add_percentage(qty as f32 / list.n as f32 * 100.0);
}
let chart_margin = 30;
let chart_margin_x = 70;
let chart_margin_y = 100;
let axis_step_dist = 45;
let bar_width = 20;
let yaxis_steps = 7;
// note that the number of datapoints is equivalent on both axis, therefore
// we can use the yaxis to calculate the width
let chart_width = (yaxis.percentages.len() as i32 + 1) * axis_step_dist;
let chart_height = yaxis_steps * axis_step_dist + axis_step_dist / 2;
let svg_width = chart_width + chart_margin + chart_margin_x;
let svg_height = chart_height + chart_margin + chart_margin_y;
let mut svg = Svg::new(svg_width, svg_height);
// add svg style
let mut style = concat!(
"*{font-family:\"Source Serif 4\",serif}",
".ui{stroke:#000;stroke-width:1px;fill:#000;font-size:12pt}",
".ui text,.bg,.b{stroke-width:0}",
".p{font-size:10pt}",
".bg{fill:#fff}",
".b{stroke:#f60;fill:#f60}",
// dark theme
"@media(prefers-color-scheme: dark){",
".ui{stroke:#ccc;fill:#ccc}",
".bg{fill:#222}",
"}"
)
.to_owned();
// allow svg to be printed to pdf by headless chromium
write!(
style,
"@page{{margin:0;size:calc(20cm*{svg_width}/{svg_height}) 20cm}}"
)
.unwrap();
svg.push(Style::new().append(style));
// add svg background
svg.push(
Rect::new()
.with_class("bg")
.with_x(0)
.with_y(0)
.with_width(chart_width + chart_margin + chart_margin_x)
.with_height(chart_height + chart_margin + chart_margin_y)
);
let mut group = Group::new()
.with_transform(
Transform::new().translate(chart_margin_x, chart_height + chart_margin)
)
.with_class("ui");
// build vertical bars
let mut bars_data = Data::new();
let mut bars_text = Text::new().with_text_anchor("middle");
for (i, percentage) in yaxis.percentages.iter().enumerate() {
let offset = (i + 1) as i32 * axis_step_dist;
let height = percentage / yaxis.max * (yaxis_steps * axis_step_dist) as f32;
bars_data
.move_to(offset - bar_width / 2, 0)
.horiz_line_by(bar_width)
.vert_line_by(-height)
.horiz_line_by(-bar_width)
.close();
bars_text.push(
TSpan::new()
.with_x(offset)
.with_y(-height - 5.0)
.append(format!("{percentage:.2}%"))
);
}
group.push(Path::new().with_d(bars_data).with_class("b"));
group.push(bars_text.with_class("b p"));
// build y axis line
let mut axis_line = Data::new();
axis_line
.move_to(0, -chart_height)
.line_by(3, 5)
.horiz_line_by(-6)
.close()
.vert_line_to(0);
for (i, _) in (axis_step_dist .. chart_height)
.step_by(axis_step_dist as usize)
.enumerate()
{
axis_line
.move_by((i == 0).then_some(-3).unwrap_or(-6), -axis_step_dist)
.horiz_line_by(6);
}
// build x axis line
axis_line
.move_to(chart_width, 0)
.line_by(-5, 3)
.vert_line_by(-6)
.close()
.horiz_line_to(0);
for (i, _) in (axis_step_dist .. chart_width)
.step_by(axis_step_dist as usize)
.enumerate()
{
axis_line
.move_by(axis_step_dist, (i == 0).then_some(-3).unwrap_or(-6))
.vert_line_by(6);
}
group.push(Path::new().with_d(axis_line).with_stroke_linejoin("arcs"));
// build y axis descriptions
let mut yaxis_text = Text::new()
.with_text_anchor("end")
.with_dominant_baseline("middle");
for i in 0 .. yaxis_steps {
yaxis_text.push(
TSpan::new()
.with_x(-5)
.with_y((i as i32 + 1) * -axis_step_dist)
.append(format!(
"{:.2}%",
yaxis.max / yaxis_steps as f32 * (i + 1) as f32
))
);
}
group.push(yaxis_text);
// build x axis descriptions
let mut xaxis_text = Text::new()
.with_class("b")
.with_text_anchor("middle")
.with_dominant_baseline("hanging");
for (i, range) in xaxis.values.keys().copied().enumerate() {
let x = (i + 1) as i32 * axis_step_dist;
let y = 5;
match range {
Range::Single(value) => {
xaxis_text.push(
TSpan::new()
.with_x(x)
.with_y(y)
.append(format!("{}", value))
);
},
Range::Range { start, len } => {
xaxis_text.push(
TSpan::new()
.with_x(x)
.with_y(y)
.append(format!("{}", start))
);
xaxis_text.push(TSpan::new().with_x(x).with_dy(".8em").append("-"));
xaxis_text.push(
TSpan::new()
.with_x(x)
.with_dy("1em")
.append(format!("{}", start + len - 1))
);
}
}
}
group.push(xaxis_text);
// build y axis legend
let mut ylegend = Text::new().with_x(-10).with_y(-chart_height - 5);
ylegend.push(format!("Percentage of {} samples", list.n));
group.push(ylegend);
// build x axis legend
let mut xlegend = Text::new()
.with_x(0)
.with_y(chart_margin_y - chart_margin)
.with_dominant_baseline("hanging");
xlegend.push("Legend: ");
xlegend.push(
TSpan::new()
.with_class("b")
.append(format!("{}", xaxis.min.unwrap_or(1)))
);
xlegend.push(format!(": {xaxis_name}"));
group.push(xlegend);
svg.push(group);
let svg = if self.pretty {
svg.to_string_pretty()
} else {
svg.to_string()
};
writeln!(out, "{}", svg)
}
}

View file

@ -0,0 +1,21 @@
var svg,xn,yn,tt,ttp,ttx,tty;
function e(c){return document.getElementById(c)}
function i(xn0,yn0){
xn=xn0;yn=yn0;
svg=document.getElementsByTagName("svg")[0];
tt=e("tt");ttp=e("ttp");ttx=e("ttx");tty=e("tty");
c()
}
function tr(x,y){tt.setAttribute('transform','translate('+x+','+y+')')}
function s(ev,p,x,y) {
ttp.textContent=p+"%";
ttx.textContent=xn+": "+x;
tty.textContent=yn+": "+y;
var r = ev.target.getBoundingClientRect();
var p = ev.target.ownerSVGElement.createSVGPoint();
p.x=r.x+r.width/2;
p.y=r.top;
p=p.matrixTransform(svg.getScreenCTM().inverse());
tr(p.x,p.y-5);
}
function c(){tr(-999,-999)}

379
stdlib/src/output/bubble.rs Normal file
View file

@ -0,0 +1,379 @@
use super::{OutputList, OutputPrinter, Svg};
use crate::int;
use indexmap::IndexMap;
use std::{fmt::Write as _, io};
use svgwriter::{
tags::{
Circle, Group, Path, Rect, Script, Style, TSpan, TagWithCoreAttributes as _,
TagWithGlobalEventAttributes as _, TagWithPresentationAttributes as _, Text
},
Data, Transform
};
use num_integer::div_ceil;
/// Output printer for SVG Bar Charts. Works only with two
/// output variables.
pub struct BubbleChartPrinter {
/// Set to true to enable SVG/XML pretty printing.
pretty: bool
}
impl BubbleChartPrinter {
pub fn new(pretty: bool) -> Self {
Self { pretty }
}
}
#[derive(Default)]
struct Axis {
values: Vec<int>,
min: Option<int>,
max: Option<int>
}
impl Axis {
fn add_value(&mut self, value: int) {
self.values.push(value);
if self.max.map(|max| max < value).unwrap_or(true) {
self.max = Some(value);
}
if self.min.map(|min| min > value).unwrap_or(true) {
self.min = Some(value);
}
}
fn min(&self) -> int {
self.min.unwrap_or(0)
}
fn max(&self) -> int {
self.max.unwrap_or(0)
}
fn step(&self, preferred_step_count: usize) -> (int, usize) {
let range = self.max() - self.min();
if range == 0 {
return (0, 1);
}
let mut step = 1;
loop {
let count = div_ceil(range, step) + 1;
if (count - preferred_step_count as int).unsigned_abs() as usize
<= preferred_step_count / 2
{
return (step, count as usize);
}
step += 1;
}
}
}
impl OutputPrinter for BubbleChartPrinter {
fn print<I, E, W>(self, list: OutputList<I>, mut out: W) -> io::Result<()>
where
I: IntoIterator<Item = (E, usize)>,
E: IntoIterator<Item = (&'static str, int)>,
W: io::Write
{
let mut percentages = Vec::new();
let mut min_percentage: f32 = 100.0;
let mut max_percentage: f32 = 0.0;
let mut axis: IndexMap<&'static str, Axis> = IndexMap::new();
for (entry, qty) in list.samples {
let percentage = qty as f32 / list.n as f32 * 100.0;
percentages.push(percentage);
min_percentage = min_percentage.min(percentage);
max_percentage = max_percentage.max(percentage);
for (key, value) in entry {
if !axis.contains_key(key) {
axis.insert(key, Axis::default());
}
axis.get_mut(key).unwrap().add_value(value);
}
}
let (yaxis_name, yaxis) = axis
.swap_remove_index(1)
.expect("Cannot use BubbleChartPrinter with less than two variables");
let (xaxis_name, xaxis) = axis.swap_remove_index(0).unwrap();
let chart_margin_x = 60;
let chart_margin_y = 60;
let bubble_min_r: f32 = 2.5;
let bubble_max_r: f32 = 15.0;
let axis_step_dist = 40;
let (xaxis_step, xaxis_step_count) = xaxis.step(8);
let (yaxis_step, yaxis_step_count) = yaxis.step(6);
let chart_width = axis_step_dist * xaxis_step_count as i32;
let chart_height = axis_step_dist * yaxis_step_count as i32;
let svg_width = chart_width + 2 * chart_margin_x;
let svg_height = chart_height + 2 * chart_margin_y;
let mut svg = Svg::new(svg_width, svg_height)
.with_onload(format!("i({xaxis_name:?},{yaxis_name:?})"));
// add svg style and script
let mut style = concat!(
"*{font-family:\"Source Serif 4\",serif}",
".ui,#tt{stroke:#000;stroke-width:1px;fill:#000;font-size:12pt}",
"#tt{font-size:10pt}",
"#tt path{fill:rgba(238,238,238,0.85)}",
"#ttp{font-weight:bold}",
"text,.bg,.b{stroke-width:0}",
".bg{fill:#fff}",
".b,#ttx,#tty{fill:#f60}",
".b circle,circle.b{opacity:0.8}",
// dark theme
"@media(prefers-color-scheme: dark){",
"#tt path{fill:rgba(34,34,34,0.7)}",
".ui,#tt{stroke:#ccc;fill:#ccc}",
".bg{fill:#222}",
"}"
)
.to_owned();
// allow svg to be printed to pdf by headless chromium
write!(
style,
"@page{{margin:0;size:calc(20cm*{svg_width}/{svg_height}) 20cm}}"
)
.unwrap();
svg.push(Style::new().append(style));
svg.push(
Script::new()
.with_ty("text/ecmascript")
.append(include_str!("bubble.js").trim())
);
// add svg background
svg.push(
Rect::new()
.with_class("bg")
.with_x(0)
.with_y(0)
.with_width(chart_width + 2 * chart_margin_x)
.with_height(chart_height + 2 * chart_margin_y)
.with_onmouseover("c()")
);
let mut group = Group::new()
.with_transform(
Transform::new().translate(chart_margin_x, chart_height + chart_margin_y)
)
.with_class("ui");
// build bubbles
let mut bubbles = Group::new().with_class("b").with_transform(
Transform::new()
.translate(axis_step_dist / 2, axis_step_dist / -2)
.scale(1.0, -1.0)
);
for (i, percentage) in percentages.iter().enumerate() {
let xvalue = xaxis.values[i];
let yvalue = yaxis.values[i];
let xpos =
(xvalue - xaxis.min()) as f32 / xaxis_step as f32 * axis_step_dist as f32;
let ypos =
(yvalue - yaxis.min()) as f32 / yaxis_step as f32 * axis_step_dist as f32;
// we want the area to grow proportional to the percentage, so the
// radius grows proportional to the square root of the percentage
let radius = bubble_min_r
+ ((bubble_max_r - bubble_min_r)
* ((percentage - min_percentage)
/ (max_percentage - min_percentage))
.sqrt());
bubbles.push(
Circle::new()
.with_cx(xpos)
.with_cy(ypos)
.with_r(radius)
.with_onmouseover(format!(
"s(evt,\"{:.2}\",{},{})",
percentage, xvalue, yvalue
))
);
}
group.push(bubbles);
// build y axis line
let mut axis_line0 = Data::new();
let mut axis_line1 = Data::new();
axis_line0.move_to(0, -chart_height).vert_line_to(0);
axis_line1
.move_to(chart_width, -chart_height)
.vert_line_to(0);
for i in 0 .. yaxis_step_count {
let (offset, step) = match i {
0 => (-3, -axis_step_dist / 2),
_ => (-6, -axis_step_dist)
};
axis_line0.move_by(offset, step).horiz_line_by(6);
axis_line1.move_by(offset, step).horiz_line_by(6);
}
// build x axis line
axis_line0.move_to(chart_width, 0).horiz_line_to(0);
axis_line1
.move_to(chart_width, -chart_height)
.horiz_line_to(0);
for i in 0 .. xaxis_step_count {
let (offset, step) = match i {
0 => (-3, axis_step_dist / 2),
_ => (-6, axis_step_dist)
};
axis_line0.move_by(step, offset).vert_line_by(6);
axis_line1.move_by(step, offset).vert_line_by(6);
}
group.push(
Group::new()
.with_stroke_linejoin("arcs")
.append(Path::new().with_d(axis_line0))
.append(Path::new().with_d(axis_line1))
);
// build y axis descriptions
let mut yaxis_text0 = Text::new()
.with_text_anchor("end")
.with_dominant_baseline("middle");
let mut yaxis_text1 = Text::new().with_dominant_baseline("middle");
for i in 0 .. yaxis_step_count {
let text = format!("{}", yaxis.min() + i as int * yaxis_step,);
yaxis_text0.push(
TSpan::new()
.with_x(-5)
.with_y(i as i32 * -axis_step_dist - axis_step_dist / 2)
.append(text.clone())
);
yaxis_text1.push(
TSpan::new()
.with_x(chart_width + 5)
.with_y(i as i32 * -axis_step_dist - axis_step_dist / 2)
.append(text)
);
}
group.push(yaxis_text0);
group.push(yaxis_text1);
// build x axis descriptions
let mut xaxis_text0 = Text::new().with_text_anchor("middle");
let mut xaxis_text1 = Text::new()
.with_text_anchor("middle")
.with_dominant_baseline("hanging");
for i in 0 .. xaxis_step_count {
let text = format!("{}", xaxis.min() + i as int * xaxis_step,);
xaxis_text0.push(
TSpan::new()
.with_x(i as i32 * axis_step_dist + axis_step_dist / 2)
.with_y(-chart_height - 5)
.append(text.clone())
);
xaxis_text1.push(
TSpan::new()
.with_x(i as i32 * axis_step_dist + axis_step_dist / 2)
.with_y(5)
.append(text)
);
}
group.push(xaxis_text0);
group.push(xaxis_text1);
// build y axis legend
group.push(
Group::new()
.with_transform(
Transform::new()
.translate(15 - chart_margin_x, chart_height / -2)
.rotate(270.0)
)
.append(
Text::new()
.with_text_anchor("middle")
.with_dominant_baseline("middle")
.append(yaxis_name)
)
);
group.push(
Group::new()
.with_transform(
Transform::new()
.translate(chart_width + chart_margin_x - 15, chart_height / -2)
.rotate(270.0)
)
.append(
Text::new()
.with_text_anchor("middle")
.with_dominant_baseline("middle")
.append(yaxis_name)
)
);
// build x axis legend
group.push(
Text::new()
.with_text_anchor("middle")
.with_dominant_baseline("middle")
.append(
TSpan::new()
.with_x(chart_width / 2)
.with_y(chart_margin_y - 15)
.append(xaxis_name)
)
.append(
TSpan::new()
.with_x(chart_width / 2)
.with_y(15 - chart_margin_y - chart_height)
.append(xaxis_name)
)
);
svg.push(group);
// build tooltip
let mut tt = Group::new()
.with_id("tt")
.with_transform(Transform::new().scale(0.0, 0.0));
{
let tip_straight_h = 5;
let tip_curve_h = 2;
let width = 80;
let height = 50;
let border_r = 5;
let bottom_hline = width / 2 - tip_straight_h - 2 * tip_curve_h;
let mut data = Data::new();
data.move_to(0, 0)
.line_by(tip_straight_h, -tip_straight_h)
.quad_by(tip_curve_h, -tip_curve_h, 2 * tip_curve_h, -tip_curve_h)
.horiz_line_by(bottom_hline)
.arc_by(border_r, border_r, 0, false, false, border_r, -border_r)
.vert_line_by(-height)
.arc_by(border_r, border_r, 0, false, false, -border_r, -border_r)
.horiz_line_by(-width)
.arc_by(border_r, border_r, 0, false, false, -border_r, border_r)
.vert_line_by(height)
.arc_by(border_r, border_r, 0, false, false, border_r, border_r)
.horiz_line_by(bottom_hline)
.quad_by(tip_curve_h, 0, 2 * tip_curve_h, tip_curve_h)
.close();
tt.push(Path::new().with_d(data));
tt.push(
Text::new()
.with_dominant_baseline("hanging")
.with_transform(Transform::new().translate(
width / -2,
-height - tip_straight_h - tip_curve_h - border_r
))
.append(TSpan::new().with_id("ttp"))
.append(TSpan::new().with_id("ttx").with_x(0).with_dy("1.2em"))
.append(TSpan::new().with_id("tty").with_x(0).with_dy("1.2em"))
);
}
svg.push(tt);
let svg = if self.pretty {
svg.to_string_pretty()
} else {
svg.to_string()
};
writeln!(out, "{}", svg)
}
}

46
stdlib/src/output/list.rs Normal file
View file

@ -0,0 +1,46 @@
use super::{OutputList, OutputPrinter};
use crate::int;
use std::io::{self, Write};
/// Default output printer.
pub struct ListPrinter;
impl OutputPrinter for ListPrinter {
fn print<I, E, W>(self, list: OutputList<I>, mut out: W) -> io::Result<()>
where
I: IntoIterator<Item = (E, usize)>,
E: IntoIterator<Item = (&'static str, int)>,
W: Write
{
writeln!(out, "Output Variable Analysis:")?;
for (key, stats) in list.stats() {
writeln!(out, " {key}: {stats:?}")?;
}
writeln!(out, "Samples:")?;
let mut qty_buf = itoa::Buffer::new();
let len_str = qty_buf.format(list.n);
let qty_spacing = len_str.len();
for (entry, qty) in list.samples {
let qty_str = qty_buf.format(qty);
let qty_str_len = qty_str.len();
write!(out, " ")?;
for _ in 0 .. qty_spacing - qty_str_len {
write!(out, " ")?;
}
write!(
out,
"{qty_str} ({:>6.2}%): {{ ",
qty as f32 / list.n as f32 * 100.0
)?;
for (i, (key, value)) in entry.into_iter().enumerate() {
if i > 0 {
write!(out, ", ")?;
}
write!(out, "{key}: {value}")?;
}
writeln!(out, " }}")?;
}
Ok(())
}
}

322
stdlib/src/output/mod.rs Normal file
View file

@ -0,0 +1,322 @@
#![allow(clippy::new_without_default)]
use crate::int;
use indexmap::IndexMap;
use std::{
error::Error,
fmt::{self, Display, Formatter},
fs::File,
io::{self, stdout, Write},
ops::{Deref, DerefMut},
path::PathBuf,
str::FromStr
};
use svgwriter::{tags::TagWithGlobalEventAttributes, Graphic, Value};
mod bar;
mod bubble;
mod list;
pub use bar::BarChartPrinter;
pub use bubble::BubbleChartPrinter;
pub use list::ListPrinter;
#[derive(Debug)]
pub struct NoSuchVariant(String);
impl Display for NoSuchVariant {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "No such variant: {}", self.0)
}
}
impl Error for NoSuchVariant {}
/// Used for argument parsing
#[derive(Clone, Copy, Debug, Default)]
pub enum OutputPrinterAny {
#[default]
List,
Bar
}
impl FromStr for OutputPrinterAny {
type Err = NoSuchVariant;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"list" => Ok(Self::List),
"bar" => Ok(Self::Bar),
_ => Err(NoSuchVariant(s.to_owned()))
}
}
}
/// Used for argument parsing
#[derive(Clone, Copy, Debug, Default)]
pub enum OutputPrinterTwoVars {
#[default]
List,
Bar,
Bubble
}
impl FromStr for OutputPrinterTwoVars {
type Err = NoSuchVariant;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"list" => Ok(Self::List),
"bar" => Ok(Self::Bar),
"bubble" => Ok(Self::Bubble),
_ => Err(NoSuchVariant(s.to_owned()))
}
}
}
/// Used for output printing.
pub trait OutputPrinter {
fn print<I, E, W>(self, list: OutputList<I>, out: W) -> io::Result<()>
where
I: IntoIterator<Item = (E, usize)>,
E: IntoIterator<Item = (&'static str, int)>,
W: Write;
}
/// Used for output printing.
pub struct OutputList<I> {
/// An iterator over all samples. Must yield pairs of (values, qty),
/// where values is an iterator over (variable, value).
pub samples: I,
/// The total quantity of all samples.
pub n: usize,
/// The minimum of each output variable.
min: IndexMap<&'static str, int>,
/// The maximum of each output variable.
max: IndexMap<&'static str, int>,
/// The mean of each output variable.
mean: IndexMap<&'static str, f64>,
/// The variance of each output variable.
variance: IndexMap<&'static str, f64>
}
#[derive(Debug)]
pub struct Stats {
pub min: int,
pub max: int,
pub mean: f64,
pub variance: f64,
pub standard_deviation: f64
}
impl<I, E> OutputList<I>
where
I: IntoIterator<Item = (E, usize)>,
for<'a> &'a I: IntoIterator<Item = (&'a E, &'a usize)>,
E: IntoIterator<Item = (&'static str, int)>,
for<'a> &'a E: IntoIterator<Item = (&'static str, int)>
{
pub fn new(samples: I, n: usize) -> Self {
let mut min = IndexMap::new();
let mut max = IndexMap::new();
let mut mean = IndexMap::new();
let mut qty = 0;
for (sample, sample_qty) in &samples {
let new_qty = (qty + sample_qty) as f64;
for (var, value) in sample {
if min.get(var).map(|v| *v > value).unwrap_or(true) {
min.insert(var, value);
}
if max.get(var).map(|v| *v < value).unwrap_or(true) {
max.insert(var, value);
}
mean.insert(
var,
mean.get(var).copied().unwrap_or(0.0) * (qty as f64 / new_qty)
+ value as f64 * (*sample_qty as f64 / new_qty)
);
}
qty += sample_qty;
}
debug_assert_eq!(qty, n);
let mut variance = IndexMap::new();
let mut qty;
let mut samples_iter = (&samples).into_iter();
let (first_sample, first_sample_qty) =
samples_iter.next().expect("I need at least one sample");
if *first_sample_qty > 1 {
qty = *first_sample_qty;
for (var, value) in first_sample {
variance.insert(
var,
(value as f64 - mean[var]).powi(2) * (qty as f64 / (qty - 1) as f64)
);
}
} else {
let (second_sample, second_sample_qty) =
samples_iter.next().expect("I need at least two samples");
let first_sample: IndexMap<&'static str, int> =
first_sample.into_iter().collect();
qty = first_sample_qty + second_sample_qty;
for (var, second_value) in second_sample {
let first_value = first_sample[var];
variance.insert(
var,
(first_value as f64 - mean[var]).powi(2)
* (*first_sample_qty as f64 / (qty - 1) as f64)
+ (second_value as f64 - mean[var]).powi(2)
* (*second_sample_qty as f64 / (qty - 1) as f64)
);
}
}
for (sample, sample_qty) in samples_iter {
let new_qty = (qty + sample_qty) as f64;
for (var, value) in sample {
variance.insert(
var,
variance[var] * ((qty - 1) as f64 / (new_qty - 1.0))
+ (value as f64 - mean[var]).powi(2)
* (*sample_qty as f64 / (new_qty - 1.0))
);
}
qty += sample_qty;
}
debug_assert_eq!(qty, n);
Self {
samples,
n,
min,
max,
mean,
variance
}
}
}
impl<I> OutputList<I> {
pub fn min(&self, var: &'static str) -> int {
self.min.get(var).copied().unwrap_or(0)
}
pub fn max(&self, var: &'static str) -> int {
self.max.get(var).copied().unwrap_or(0)
}
pub fn mean(&self, var: &'static str) -> f64 {
self.mean.get(var).copied().unwrap_or(0.0)
}
pub fn variance(&self, var: &'static str) -> f64 {
self.variance.get(var).copied().unwrap_or(f64::INFINITY)
}
pub fn stats(&self) -> impl Iterator<Item = (&'static str, Stats)> + '_ {
self.mean.iter().map(|(key, mean)| {
let variance = self.variance(key);
(*key, Stats {
min: self.min(key),
max: self.max(key),
mean: *mean,
variance,
standard_deviation: variance.sqrt()
})
})
}
}
/// Used for output printing.
pub trait OutputTarget {
fn print<I, E>(self, list: OutputList<I>)
where
I: IntoIterator<Item = (E, usize)>,
E: IntoIterator<Item = (&'static str, int)>;
}
fn print_with<I, E, P>(list: OutputList<I>, printer: P, path: Option<PathBuf>)
where
I: IntoIterator<Item = (E, usize)>,
E: IntoIterator<Item = (&'static str, int)>,
P: OutputPrinter
{
match path {
Some(path) => {
let file = File::create(&path).expect("Failed to create output file");
printer.print(list, file).expect("Failed to print output");
eprintln!("Wrote output to {}", path.display());
},
None => {
printer
.print(list, stdout())
.expect("Failed to print output");
}
}
}
impl OutputTarget for (OutputPrinterAny, Option<PathBuf>) {
fn print<I, E>(self, list: OutputList<I>)
where
I: IntoIterator<Item = (E, usize)>,
E: IntoIterator<Item = (&'static str, int)>
{
match self.0 {
OutputPrinterAny::List => print_with(list, ListPrinter, self.1),
OutputPrinterAny::Bar => {
print_with(list, BarChartPrinter::new(self.1.is_none()), self.1)
},
}
}
}
impl OutputTarget for (OutputPrinterTwoVars, Option<PathBuf>) {
fn print<I, E>(self, list: OutputList<I>)
where
I: IntoIterator<Item = (E, usize)>,
E: IntoIterator<Item = (&'static str, int)>
{
match self.0 {
OutputPrinterTwoVars::List => print_with(list, ListPrinter, self.1),
OutputPrinterTwoVars::Bar => {
print_with(list, BarChartPrinter::new(self.1.is_none()), self.1)
},
OutputPrinterTwoVars::Bubble => {
print_with(list, BubbleChartPrinter::new(self.1.is_none()), self.1)
},
}
}
}
/// Helper for creating svgs
struct Svg(Graphic);
impl Deref for Svg {
type Target = Graphic;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Svg {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Svg {
fn new(width: i32, height: i32) -> Self {
let mut svg = Graphic::new();
svg.set_view_box(format!("0 0 {width} {height}"));
svg.set_height("20cm");
Self(svg)
}
fn with_onload<V>(mut self, onload: V) -> Self
where
V: Value + 'static
{
self.0.set_onload(onload);
self
}
}