use crate::prelude::*;
use alloc::borrow::Cow;
use core::fmt::Display;
use hashbrown::hash_map;
use rand::{
RngExt as _, SeedableRng,
rngs::{SmallRng, SysRng},
};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Library(YarnFnRegistry);
impl Extend<<YarnFnRegistry as IntoIterator>::Item> for Library {
fn extend<T: IntoIterator<Item = (Cow<'static, str>, Box<dyn UntypedYarnFn>)>>(
&mut self,
iter: T,
) {
self.0.extend(iter);
}
}
impl IntoIterator for Library {
type Item = (Cow<'static, str>, Box<dyn UntypedYarnFn>);
type IntoIter = hash_map::IntoIter<Cow<'static, str>, Box<dyn UntypedYarnFn>>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl Library {
pub fn new() -> Self {
Self::default()
}
pub fn import(&mut self, other: Self) {
self.0.extend(other.0.0);
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &dyn UntypedYarnFn)> {
self.0.iter()
}
pub fn get(&self, name: &str) -> Option<&dyn UntypedYarnFn> {
self.0.get(name)
}
pub fn generate_unique_visited_variable_for_node(node_name: &str) -> String {
format!("$Yarn.Internal.Visiting.{node_name}")
}
pub fn standard_library() -> Self {
let mut library = yarn_library!(
"string" => <String as From<YarnValue >>::from,
"number" => |value: YarnValue| f32::try_from(value).expect("Failed to convert a Yarn value to a number"),
"bool" => |value: YarnValue| bool::try_from(value).expect("Failed to convert a Yarn value to a bool"),
"format_invariant" => |value: f32| value.to_string(),
"random" => ||SmallRng::try_from_rng(&mut SysRng).unwrap().random_range(0.0..1.0),
"random_range" => |min: f32, max: f32| {
if let Some(min) = min.as_int()
&& let Some(max_inclusive) = max.as_int()
{
return SmallRng::try_from_rng(&mut SysRng).unwrap().random_range(min..=max_inclusive) as f32;
}
SmallRng::try_from_rng(&mut SysRng).unwrap().random_range(min..max)
},
"random_range_float" => |min: f32, max: f32| SmallRng::try_from_rng(&mut SysRng).unwrap().random_range::<f32, _>(min..=max),
"dice" => |sides: u32| {
if sides == 0 {
return 1;
}
SmallRng::try_from_rng(&mut SysRng).unwrap().random_range(1..=sides)
},
"round" => |value: f32| value.round() as i32,
"round_places" => |value: f32, places: u32| value.round_places(places),
"floor" => |value: f32| value.floor() as i32,
"ceil" => |value: f32| value.ceil() as i32,
"inc" => |value: f32| value.floor() as i32 + 1,
"dec" => |value: f32| value.ceil() as i32 - 1,
"decimal" => |value: f32| value.fract(),
"int" => |value: f32| value.trunc() as i32,
);
for r#type in [Type::Number, Type::String, Type::Boolean] {
library.add_methods(r#type);
}
library
}
pub fn add_function<Marker, F>(
&mut self,
name: impl Into<Cow<'static, str>>,
function: F,
) -> &mut Self
where
Marker: 'static,
F: YarnFn<Marker> + 'static + Clone,
F::Out: IntoYarnValueFromNonYarnValue + 'static + Clone,
{
self.0.register_function(name, function);
self
}
pub fn contains_function(&self, name: &str) -> bool {
self.0.contains_function(name)
}
pub fn names(&self) -> impl Iterator<Item = &str> {
self.0.names()
}
pub fn functions(&self) -> impl Iterator<Item = &dyn UntypedYarnFn> {
self.0.functions()
}
fn add_methods(&mut self, r#type: Type) {
for (name, function) in r#type.methods().into_iter() {
let canonical_name = r#type.get_canonical_name_for_method(name.as_ref());
self.0.add_boxed(canonical_name, function.clone());
}
}
}
impl Display for Library {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let mut functions: Vec<_> = self.0.iter().collect();
functions.sort_by_key(|(name, _)| name.to_string());
writeln!(f, "{{")?;
for (name, function) in functions {
writeln!(f, " {name}: {function}")?;
}
writeln!(f, "}}")?;
Ok(())
}
}
#[macro_export]
macro_rules! yarn_library {
($($name:expr => $function:expr,)*) => {
{
let mut map = Library::default();
$(
map.add_function($name, $function);
)*
map
}
};
}
pub use yarn_library;
trait FloatExt: Copy {
fn as_int(self) -> Option<i32>;
fn round_places(self, places: u32) -> Self;
}
impl FloatExt for f32 {
fn as_int(self) -> Option<i32> {
(self.fract().abs() <= f32::EPSILON).then_some(self as i32)
}
fn round_places(self, places: u32) -> Self {
let factor = 10_u32.pow(places) as f32;
(self * factor).round() / factor
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rounds_places() {
for (num, places, expected) in [
(1.0, 0, 1.0),
(1.2, 1, 1.2),
(0.4, 0, 0.0),
(43.132, 0, 43.0),
(1.1, 2, 1.1),
(123.123, 3, 123.123),
(-10.3, 1, -10.3),
(-11.99, 1, -12.0),
] {
assert_eq!(expected, num.round_places(places));
}
}
}