@tishlang/tish 1.4.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -37,6 +37,80 @@ pub fn len(s: &Value) -> Option<usize> {
37
37
  }
38
38
  }
39
39
 
40
+ /// JS `ToIntegerOrInfinity` then clamp for `lastIndexOf` `position` (character index).
41
+ fn last_index_of_position_to_start(position: &Value, len: usize) -> usize {
42
+ let pos = match position {
43
+ Value::Null => 0.0,
44
+ Value::Bool(false) => 0.0,
45
+ Value::Bool(true) => 1.0,
46
+ Value::Number(n) => {
47
+ if n.is_nan() || *n == 0.0 {
48
+ 0.0
49
+ } else if n.is_infinite() {
50
+ *n
51
+ } else {
52
+ n.trunc()
53
+ }
54
+ }
55
+ _ => 0.0,
56
+ };
57
+ if pos.is_infinite() {
58
+ if pos > 0.0 {
59
+ len
60
+ } else {
61
+ 0
62
+ }
63
+ } else if pos <= 0.0 {
64
+ 0
65
+ } else {
66
+ (pos as usize).min(len)
67
+ }
68
+ }
69
+
70
+ /// Character index of last occurrence of `needle` in `haystack`, or `-1`.
71
+ /// `position` is JS `lastIndexOf`'s second argument: use `Number(INFINITY)` when omitted;
72
+ /// `Null` is JS `null` → 0. Indices are Unicode scalar positions (same as `.length` / `indexOf`).
73
+ pub fn last_index_of_str(haystack: &str, needle: &str, position: &Value) -> Value {
74
+ let len = haystack.chars().count();
75
+ let start = last_index_of_position_to_start(position, len);
76
+ let hay: Vec<char> = haystack.chars().collect();
77
+ let needle_chars: Vec<char> = needle.chars().collect();
78
+ let search_len = needle_chars.len();
79
+ if search_len == 0 {
80
+ return Value::Number(start as f64);
81
+ }
82
+ if search_len > len {
83
+ return Value::Number(-1.0);
84
+ }
85
+ // Match must fit in the string and end at or before `start` (ECMA `lastIndexOf` position).
86
+ if start + 1 < search_len {
87
+ return Value::Number(-1.0);
88
+ }
89
+ let k_max_by_len = len - search_len;
90
+ let k_max_by_start = start + 1 - search_len;
91
+ let k_max = k_max_by_len.min(k_max_by_start);
92
+ let mut k = k_max;
93
+ loop {
94
+ if hay[k..k + search_len] == needle_chars[..] {
95
+ return Value::Number(k as f64);
96
+ }
97
+ if k == 0 {
98
+ break;
99
+ }
100
+ k -= 1;
101
+ }
102
+ Value::Number(-1.0)
103
+ }
104
+
105
+ /// Like [`last_index_of_str`] but takes string `Value`s; non-strings → `-1`.
106
+ pub fn last_index_of(s: &Value, search: &Value, position: &Value) -> Value {
107
+ if let (Value::String(h), Value::String(n)) = (s, search) {
108
+ last_index_of_str(h.as_ref(), n.as_ref(), position)
109
+ } else {
110
+ Value::Number(-1.0)
111
+ }
112
+ }
113
+
40
114
  /// Returns character index of first occurrence, or -1. Optional fromIndex (JS indexOf).
41
115
  pub fn index_of(s: &Value, search: &Value, from: Option<&Value>) -> Value {
42
116
  if let (Value::String(s), Value::String(search)) = (s, search) {
@@ -251,3 +325,177 @@ pub fn pad_start(s: &Value, target_len: &Value, pad: &Value) -> Value {
251
325
  pub fn pad_end(s: &Value, target_len: &Value, pad: &Value) -> Value {
252
326
  pad_impl(s, target_len, pad, false)
253
327
  }
328
+
329
+ #[cfg(test)]
330
+ mod tests {
331
+ use super::*;
332
+
333
+ fn s(x: &str) -> Value {
334
+ Value::String(x.into())
335
+ }
336
+
337
+ fn n(x: f64) -> Value {
338
+ Value::Number(x)
339
+ }
340
+
341
+ fn same(a: &Value, b: &Value) -> bool {
342
+ match (a, b) {
343
+ (Value::String(x), Value::String(y)) => x == y,
344
+ (Value::Number(x), Value::Number(y)) => {
345
+ if x.is_nan() && y.is_nan() {
346
+ true
347
+ } else {
348
+ x == y
349
+ }
350
+ }
351
+ (Value::Bool(x), Value::Bool(y)) => x == y,
352
+ (Value::Null, Value::Null) => true,
353
+ (Value::Array(ax), Value::Array(ay)) => {
354
+ let bx = ax.borrow();
355
+ let by = ay.borrow();
356
+ bx.len() == by.len() && bx.iter().zip(by.iter()).all(|(u, v)| same(u, v))
357
+ }
358
+ _ => false,
359
+ }
360
+ }
361
+
362
+ macro_rules! assert_same {
363
+ ($left:expr, $right:expr) => {
364
+ assert!(same(&$left, &$right), "left={:?} right={:?}", $left, $right);
365
+ };
366
+ }
367
+
368
+ #[test]
369
+ fn index_of_basic() {
370
+ assert_same!(index_of(&s("abc"), &s("b"), None), n(1.0));
371
+ assert_same!(index_of(&s("abc"), &s("x"), None), n(-1.0));
372
+ assert_same!(index_of(&s("abca"), &s("a"), Some(&n(1.0))), n(3.0));
373
+ }
374
+
375
+ #[test]
376
+ fn index_of_non_string() {
377
+ assert_same!(index_of(&n(1.0), &s("a"), None), n(-1.0));
378
+ assert_same!(index_of(&s("a"), &n(1.0), None), n(-1.0));
379
+ }
380
+
381
+ #[test]
382
+ fn includes_basic() {
383
+ assert_same!(includes(&s("hello"), &s("ll"), None), Value::Bool(true));
384
+ assert_same!(includes(&s("hello"), &s("x"), None), Value::Bool(false));
385
+ assert_same!(includes(&s("hello"), &s("l"), Some(&n(3.0))), Value::Bool(true));
386
+ assert_same!(includes(&s("hello"), &s("l"), Some(&n(4.0))), Value::Bool(false));
387
+ }
388
+
389
+ #[test]
390
+ fn includes_negative_from() {
391
+ assert_same!(includes(&s("hello"), &s("o"), Some(&n(-1.0))), Value::Bool(true));
392
+ assert_same!(includes(&s("hello"), &s("h"), Some(&n(-5.0))), Value::Bool(true));
393
+ // fromIndex -1 → start at len-1 = 1 ("i" only), "h" not found
394
+ assert_same!(includes(&s("hi"), &s("h"), Some(&n(-1.0))), Value::Bool(false));
395
+ }
396
+
397
+ #[test]
398
+ fn includes_non_string() {
399
+ assert_same!(includes(&n(1.0), &s("a"), None), Value::Bool(false));
400
+ }
401
+
402
+ #[test]
403
+ fn slice_substring() {
404
+ assert_same!(slice(&s("hello"), &n(1.0), &n(4.0)), s("ell"));
405
+ assert_same!(slice(&s("hello"), &n(-3.0), &Value::Null), s("llo"));
406
+ assert_same!(substring(&s("hello"), &n(4.0), &n(1.0)), s("ell"));
407
+ assert_same!(slice(&s("ab"), &n(1.0), &n(1.0)), s(""));
408
+ }
409
+
410
+ #[test]
411
+ fn slice_non_string() {
412
+ assert_same!(slice(&n(1.0), &n(0.0), &Value::Null), Value::Null);
413
+ }
414
+
415
+ #[test]
416
+ fn split_trim() {
417
+ let Value::Array(a) = split(&s("a,b"), &s(",")) else {
418
+ panic!();
419
+ };
420
+ assert_eq!(a.borrow().len(), 2);
421
+ assert_same!(
422
+ split(&s("x"), &n(1.0)),
423
+ Value::Array(Rc::new(RefCell::new(vec![s("x")])))
424
+ );
425
+ assert_same!(split(&n(1.0), &s(",")), Value::Null);
426
+ assert_same!(trim(&s(" x ")), s("x"));
427
+ assert_same!(trim(&n(1.0)), Value::Null);
428
+ }
429
+
430
+ #[test]
431
+ fn case_and_prefix_suffix() {
432
+ assert_same!(to_upper_case(&s("aB")), s("AB"));
433
+ assert_same!(to_lower_case(&s("aB")), s("ab"));
434
+ assert_same!(starts_with(&s("/api"), &s("/api")), Value::Bool(true));
435
+ assert_same!(ends_with(&s("x.js"), &s(".js")), Value::Bool(true));
436
+ assert_same!(starts_with(&n(1.0), &s("")), Value::Bool(false));
437
+ }
438
+
439
+ #[test]
440
+ fn replace_family() {
441
+ assert_same!(replace(&s("aa"), &s("a"), &s("b")), s("ba"));
442
+ assert_same!(replace_all(&s("aa"), &s("a"), &s("b")), s("bb"));
443
+ assert_same!(replace(&n(1.0), &s("a"), &s("b")), Value::Null);
444
+ }
445
+
446
+ #[test]
447
+ fn char_at_code() {
448
+ assert_same!(char_at(&s("ab"), &n(0.0)), s("a"));
449
+ assert_same!(char_at(&s("ab"), &n(99.0)), s(""));
450
+ if let Value::Number(x) = char_code_at(&s("A"), &n(0.0)) {
451
+ assert_eq!(x, 65.0);
452
+ } else {
453
+ panic!();
454
+ }
455
+ assert!(matches!(char_code_at(&s("x"), &n(9.0)), Value::Number(x) if x.is_nan()));
456
+ }
457
+
458
+ #[test]
459
+ fn repeat_pad() {
460
+ assert_same!(repeat(&s("ab"), &n(2.0)), s("abab"));
461
+ assert_same!(repeat(&s("x"), &n(0.0)), s(""));
462
+ assert_same!(pad_start(&s("5"), &n(3.0), &s("0")), s("005"));
463
+ assert_same!(pad_end(&s("hi"), &n(5.0), &s("!")), s("hi!!!"));
464
+ assert_same!(pad_start(&s("hello"), &n(3.0), &Value::Null), s("hello"));
465
+ }
466
+
467
+ #[test]
468
+ fn last_index_of_basic() {
469
+ assert_same!(last_index_of(&s("abcabc"), &s("a"), &n(f64::INFINITY)), n(3.0));
470
+ assert_same!(last_index_of(&s("abcabc"), &s("a"), &n(2.0)), n(0.0));
471
+ assert_same!(last_index_of(&s("hello"), &s("l"), &n(3.0)), n(3.0));
472
+ assert_same!(last_index_of(&s("hello"), &s("l"), &n(1.0)), n(-1.0));
473
+ }
474
+
475
+ #[test]
476
+ fn last_index_of_omit_and_null() {
477
+ assert_same!(last_index_of(&s("aba"), &s("a"), &n(f64::INFINITY)), n(2.0));
478
+ assert_same!(last_index_of(&s("aba"), &s("a"), &Value::Null), n(0.0));
479
+ }
480
+
481
+ #[test]
482
+ fn last_index_of_empty_needle() {
483
+ assert_same!(last_index_of(&s("abc"), &s(""), &n(2.0)), n(2.0));
484
+ }
485
+
486
+ #[test]
487
+ fn last_index_of_nan_position() {
488
+ assert_same!(last_index_of(&s("aba"), &s("a"), &n(f64::NAN)), n(0.0));
489
+ }
490
+
491
+ #[test]
492
+ fn last_index_of_unicode() {
493
+ assert_same!(last_index_of(&s("😀a😀"), &s("a"), &n(f64::INFINITY)), n(1.0));
494
+ assert_same!(last_index_of(&s("😀a😀"), &s("😀"), &n(f64::INFINITY)), n(2.0));
495
+ }
496
+
497
+ #[test]
498
+ fn last_index_of_non_string() {
499
+ assert_same!(last_index_of(&n(1.0), &s("a"), &n(0.0)), n(-1.0));
500
+ }
501
+ }
@@ -14,3 +14,4 @@ tishlang_core = { path = "../tish_core", version = ">=0.1" }
14
14
  tishlang_compile = { path = "../tish_compile", version = ">=0.1" }
15
15
  tishlang_parser = { path = "../tish_parser", version = ">=0.1" }
16
16
  tishlang_opt = { path = "../tish_opt", version = ">=0.1" }
17
+ tishlang_vm = { path = "../tish_vm", version = ">=0.1" }