roto 0.11.0

a statically-typed, compiled, embedded scripting language
Documentation
use std::{borrow::Borrow, sync::Arc};

use crate::List;

/// Inner data type containing the string
#[derive(Clone, Default, PartialEq, Eq)]
struct StringData(Arc<str>);

/// Roto's built-in string type
#[derive(Clone, Default, PartialEq, Eq)]
#[repr(transparent)]
pub struct RotoString(StringData);

impl RotoString {
    /// Create a new [`String`].
    pub fn new(x: impl Into<Arc<str>>) -> Self {
        Self(StringData(x.into()))
    }

    /// Create a [`String`] from a [`List<char>`].
    pub fn from_chars(list: List<char>) -> Self {
        let mut out = std::string::String::new();
        for item in list.to_vec() {
            out.push(item);
        }
        out.into()
    }

    /// Returns whether `needle` is a substring of `self`.
    pub fn contains(&self, needle: &str) -> bool {
        self.0.0.contains(needle)
    }

    /// Returns whether `self` starts with the substring `prefix`.
    pub fn starts_with(&self, prefix: &str) -> bool {
        self.0.0.starts_with(prefix)
    }

    /// Returns whether `self` ends with the substring `suffix`.
    pub fn ends_with(&self, suffix: &str) -> bool {
        self.0.0.ends_with(suffix)
    }

    /// Convert this string to lowercase.
    pub fn to_lowercase(&self) -> Self {
        self.0.0.to_lowercase().into()
    }

    /// Convert this string to lowercase.
    pub fn to_uppercase(&self) -> Self {
        self.0.0.to_uppercase().into()
    }

    /// Create a new string by repeating this string `n` times.
    pub fn repeat(&self, n: usize) -> Self {
        self.0.0.repeat(n).into()
    }

    /// Create a list of strings by splitting this string by the `separator`.
    pub fn split(&self, separator: &str) -> List<RotoString> {
        self.0.0.split(&separator).map(Into::into).collect()
    }

    /// Replace each substring `from` with `to`.
    pub fn replace(self, from: &str, to: &str) -> Self {
        self.0.0.replace(from, to).into()
    }

    /// Get a view of this string indexed by bytes.
    pub fn bytes(self) -> StringBytes {
        StringBytes(self.0)
    }

    /// Get a view of this string indexed by chars.
    pub fn chars(self) -> StringChars {
        StringChars(self.0)
    }

    /// Get a view of this string indexed by lines.
    pub fn lines(self) -> StringLines {
        StringLines(self.0)
    }

    /// Create a new string by removing leading and trailing
    /// whitespace.
    pub fn trim(self) -> Self {
        self.0.0.trim().into()
    }

    /// Create a new string by removing leading whitespace.
    pub fn trim_start(self) -> Self {
        self.0.0.trim_start().into()
    }

    /// Create a new string by removing trailing whitespace.
    pub fn trim_end(self) -> Self {
        self.0.0.trim_end().into()
    }

    /// Create a new string by removing a given prefix.
    ///
    /// Returns `None` if the string does not contain the prefix.
    pub fn strip_prefix(self, prefix: &str) -> Option<Self> {
        self.0.0.strip_prefix(prefix).map(|s| s.into())
    }

    /// Create a new string by removing a given suffix.
    ///
    /// Returns `None` if the string does not contain the suffix.
    pub fn strip_suffix(self, suffix: &str) -> Option<Self> {
        self.0.0.strip_suffix(suffix).map(|s| s.into())
    }

    /// Splits this string at `separator` at most `n` times.
    pub fn splitn(self, n: usize, separator: &str) -> List<RotoString> {
        self.0.0.splitn(n, separator).map(Into::into).collect()
    }

    /// Splits this string at `separator` at most `n` times starting from the
    /// end.
    pub fn rsplitn(self, n: usize, separator: &str) -> List<RotoString> {
        self.0.0.rsplitn(n, separator).map(Into::into).collect()
    }
}

impl<T: Into<Arc<str>>> From<T> for RotoString {
    fn from(value: T) -> Self {
        RotoString(StringData(value.into()))
    }
}

impl From<RotoString> for std::string::String {
    fn from(value: RotoString) -> std::string::String {
        std::string::String::from(&*value.0.0)
    }
}

impl AsRef<str> for RotoString {
    fn as_ref(&self) -> &str {
        &self.0.0
    }
}

impl Borrow<str> for RotoString {
    fn borrow(&self) -> &str {
        &self.0.0
    }
}

impl std::ops::Deref for RotoString {
    type Target = str;

    fn deref(&self) -> &Self::Target {
        &self.0.0
    }
}

impl std::fmt::Display for RotoString {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        self.0.0.fmt(f)
    }
}

impl std::fmt::Debug for RotoString {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        self.0.0.fmt(f)
    }
}

/// A view into a string indexed by bytes.
#[derive(Clone, PartialEq, Eq)]
#[repr(transparent)]
pub struct StringBytes(StringData);

impl StringBytes {
    /// Get the length of the string in bytes.
    pub fn len(&self) -> usize {
        self.0.0.len()
    }

    /// Get the character at byte offset `idx`.
    pub fn get(&self, idx: usize) -> Option<char> {
        self.0.0.get(idx..).and_then(|s| s.chars().next())
    }

    /// Slice this string based on byte indices.
    ///
    /// This method returns `None` if either `i` or `j` is out of bounds or if
    /// `i` is greater than `j`.
    pub fn slice(&self, i: usize, j: usize) -> Option<RotoString> {
        self.0.0.get(i..j).map(Into::into)
    }

    /// Returns the list of bytes of this string
    pub fn list(&self) -> List<u8> {
        // TODO: This could be optimized
        self.0.0.as_bytes().iter().copied().collect()
    }
}

/// A view into a string indexed by code points.
#[derive(Clone, PartialEq, Eq)]
#[repr(transparent)]
pub struct StringChars(StringData);

impl StringChars {
    /// Get the number of characters in a string.
    pub fn len(&self) -> usize {
        self.0.0.chars().count()
    }

    /// Get the nth character of this string.
    pub fn get(&self, idx: usize) -> Option<char> {
        self.0.0.chars().nth(idx)
    }

    /// Slice this string based on the character indices.
    ///
    /// This method returns `None` if either `i` or `j` is out of bounds or if
    /// `i` is greater than `j`.
    pub fn slice(&self, i: usize, j: usize) -> Option<RotoString> {
        // If j is less than i, we return None.
        let len = j.checked_sub(i)?;

        // Create an iterator for character indices. We have to chain it
        // with the length of the string because that index won't be
        // returned by the char_indices iterator.
        let mut indices = self
            .0
            .0
            .char_indices()
            .map(|(byte, _)| byte)
            .chain(std::iter::once(self.0.0.len()));

        let byte_i = indices.nth(i)?;

        // We need to determine how many characters we have to advance
        // the iterator, which means subtracting with 1.
        if let Some(idx) = len.checked_sub(1) {
            let byte_j = indices.nth(idx)?;
            Some(self.0.0[byte_i..byte_j].into())
        } else {
            Some("".into())
        }
    }

    /// Get a list of characters that this string consists of.
    pub fn list(&self) -> List<char> {
        let list = List::new();

        for char in self.0.0.chars() {
            list.push(char);
        }

        list
    }
}

/// A view into a string indexed by lines.
#[derive(Clone, PartialEq, Eq)]
#[repr(transparent)]
pub struct StringLines(StringData);

impl StringLines {
    /// Get the number of lines in this string.
    pub fn len(&self) -> usize {
        self.0.0.lines().count()
    }

    /// Get the nth line in this string.
    pub fn get(&self, idx: usize) -> Option<char> {
        self.0.0.get(idx..).and_then(|s| s.chars().next())
    }

    /// Slice this string by lines.
    ///
    /// This method returns `None` if either `i` or `j` is out of bounds or if
    /// `i` is greater than `j`.
    pub fn slice(&self, i: usize, j: usize) -> Option<RotoString> {
        // If j is less than i, we return None.
        let num = j.checked_sub(i)?;

        let s = &self.0.0;

        // Append the length of the string as an index so that we don't go
        // out of bounds on the last line. This can be thought of a putting
        // an extra newline on the end, which we can't do without allocating.
        let end = if s.ends_with('\n') {
            None
        } else {
            Some(s.len())
        };

        let mut iter = s.match_indices('\n').map(|(byte, _)| byte + 1);

        // This is essentially a manual `Iterator::skip` implementation, except
        // that we return `None` if `i` is out of bounds.
        let mut start_idx = 0;
        for _ in 0..i {
            let idx = iter.next()?;
            start_idx = idx;
        }

        if num == 0 {
            return Some(RotoString::new(""));
        }

        let mut iter = iter.chain(end);

        // Same as above but for `Iterator::take`
        let mut end_idx = start_idx;
        for _ in i..j {
            let idx = iter.next()?;
            end_idx = idx;
        }

        Some(self.0.0[start_idx..end_idx].into())
    }

    /// Get a list of lines
    pub fn list(&self) -> List<RotoString> {
        // TODO: This could be optimized
        self.0.0.lines().map(Into::into).collect()
    }
}

#[cfg(test)]
mod tests {
    #[test]
    fn string_line_slice() {
        use super::RotoString;

        let s = RotoString::from("1\n2\n3\n4\n").lines();

        assert_eq!(s.slice(0, 0), Some("".into()));
        assert_eq!(s.slice(0, 1), Some("1\n".into()));
        assert_eq!(s.slice(0, 2), Some("1\n2\n".into()));
        assert_eq!(s.slice(1, 3), Some("2\n3\n".into()));
        assert_eq!(s.slice(1, 4), Some("2\n3\n4\n".into()));
        assert_eq!(s.slice(1, 5), None);

        let s = RotoString::from("1\n2\n3\n4").lines();

        assert_eq!(s.slice(0, 0), Some("".into()));
        assert_eq!(s.slice(0, 1), Some("1\n".into()));
        assert_eq!(s.slice(0, 2), Some("1\n2\n".into()));
        assert_eq!(s.slice(1, 3), Some("2\n3\n".into()));
        assert_eq!(s.slice(1, 4), Some("2\n3\n4".into()));
        assert_eq!(s.slice(1, 5), None);

        let s = RotoString::from("1\n2\n3\n4\n\n").lines();

        assert_eq!(s.slice(0, 0), Some("".into()));
        assert_eq!(s.slice(0, 1), Some("1\n".into()));
        assert_eq!(s.slice(0, 2), Some("1\n2\n".into()));
        assert_eq!(s.slice(1, 3), Some("2\n3\n".into()));
        assert_eq!(s.slice(1, 5), Some("2\n3\n4\n\n".into()));
        assert_eq!(s.slice(1, 6), None);

        let s = RotoString::from("").lines();

        assert_eq!(s.slice(0, 0), Some("".into()));
        assert_eq!(s.slice(0, 1), Some("".into()));
        assert_eq!(s.slice(1, 1), None);
    }
}