1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
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);
}
}