/
nv-lang
/
nova
Обзор
Документация
Войти
/
nv-lang
/
nova
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
std/src/runtime/string/transform.nv
219 строк
9 KB
Evgeniy Golovin
lint(std/src,examples): fix W_MANUAL_SLICE_TO_END (98) — canonical open-range slices
01 авг 2026, 17:03
01 авг 2026, 17:03
2ece7ec
Код
Авторство
О чём код?
// Co-equal file of module `runtime.string` (folder = one module). // Role: trim / case(ASCII) / concat / pad / repeat / replace. // // `str` Nova-body methods. #no_prelude breaks the prelude->string->prelude // import cycle. #no_prelude module runtime.string import std.prelude.core.{Option, Some, None} import std.runtime.string_builder.{StringBuilder} // Убирает ASCII whitespace с начала и конца. ZERO-COPY sub-view через // `@[start..end]` (не alloc-копия). str иммутабелен + GC держит буфер живым // через view. Whitespace = байт <= 0x20 (Unicode-WS — Phase B). export fn str @trim_ascii() -> str { ro (bytes, n) = (@bytes(), @byte_len()) mut start = 0 while start < n && is_ascii_ws(bytes[start] as int) { start += 1 } mut end = n while end > start && is_ascii_ws(bytes[end - 1] as int) { end -= 1 } @[start..end] } // Убирает ASCII whitespace только СЛЕВА (zero-copy view). export fn str @trim_ascii_start() -> str { ro (bytes, n) = (@bytes(), @byte_len()) mut start = 0 while start < n && is_ascii_ws(bytes[start] as int) { start += 1 } @[start..n] } // Убирает ASCII whitespace только СПРАВА (zero-copy view). export fn str @trim_ascii_end() -> str { ro (bytes, n) = (@bytes(), @byte_len()) mut end = n while end > 0 && is_ascii_ws(bytes[end - 1] as int) { end -= 1 } @[..end] } // Убирает все ведущие вхождения codepoint'а `c` (zero-copy). Rust `trim_start_matches` // для char-pattern. `c.to_str()` — UTF-8-байты символа; repeated strip_prefix. export fn str @trim_start_matches(c char) -> str { ro pat = c.to_str() ro plen = pat.byte_len() if plen == 0 { return @ } mut s = @ while s.starts_with(pat) { s = s[plen..] } s } // Убирает все хвостовые вхождения codepoint'а `c` (zero-copy). Rust `trim_end_matches`. export fn str @trim_end_matches(c char) -> str { ro pat = c.to_str() ro plen = pat.byte_len() if plen == 0 { return @ } mut s = @ while s.ends_with(pat) { s = s[..s.byte_len() - plen] } s } // Убирает вхождения codepoint'а `c` с обоих концов (zero-copy). Rust `trim_matches`. export fn str @trim_matches(c char) -> str => @trim_start_matches(c).trim_end_matches(c) // `Some(suffix-view)` если строка начинается с `prefix` (zero-copy, prefix снят); // `None` иначе. Rust `strip_prefix` / Kotlin `removePrefix`. export fn str @strip_prefix(prefix str) -> Option[str] { if @starts_with(prefix) { Some(@[prefix.byte_len()..]) } else { None } } // `Some(prefix-view)` если строка заканчивается на `suffix` (zero-copy, suffix снят); // `None` иначе. Rust `strip_suffix` / Kotlin `removeSuffix`. export fn str @strip_suffix(suffix str) -> Option[str] { if @ends_with(suffix) { Some(@[..@byte_len() - suffix.byte_len()]) } else { None } } // ASCII lowercase копия. Bulk-copy then in-place patch: one alloc + memcpy, // then patch only the A-Z bytes. ASCII ops preserve UTF-8 validity. export fn str @to_ascii_lower() -> str { ro (bytes, n) = (@bytes(), @byte_len()) mut out []u8 = []u8.new(cap: n) out.append(bytes) for i in 0..n { ro b = out[i] as int if b >= 65 && b <= 90 { out[i] = (b + 32) as u8 } } // Zero-copy кража буфера: `out` валиден по построению — `alloc_copy` платил // ВТОРУЮ полную копию; `into_str_unchecked` забирает буфер как есть (cap-хвост // висит на той же GC-аллокации — как у SB @into_str). unsafe { out.into_str_unchecked() } } // ASCII uppercase копия. Bulk-copy then in-place patch: one alloc + memcpy, // then patch only the a-z bytes. ASCII ops preserve UTF-8 validity. export fn str @to_ascii_upper() -> str { ro (bytes, n) = (@bytes(), @byte_len()) mut out []u8 = []u8.new(cap: n) out.append(bytes) for i in 0..n { ro b = out[i] as int if b >= 97 && b <= 122 { out[i] = (b - 32) as u8 } } // Zero-copy кража буфера: `out` валиден по построению — `alloc_copy` платил // ВТОРУЮ полную копию; `into_str_unchecked` забирает буфер как есть (cap-хвост // висит на той же GC-аллокации — как у SB @into_str). unsafe { out.into_str_unchecked() } } // Конкатенация двух строк (создаёт новую). O(a+b). // Nova-body: alloc `[]u8` размера `@byte_len() + other.len()`, копирует байты // обоих операндов через @bytes() (zero-copy view), затем // `str.from_bytes_unchecked`. Метод вызываем напрямую (`s.concat(t)`) и из // `@plus` body. Оператор `+` лоуэрится отдельно, напрямую в C // `nova_str_concat` (один memcpy-проход + один alloc) — оптимальнее push-loop. export fn str @concat(other str) -> str { ro a = @bytes() ro b = other.bytes() // One alloc (cap a+b) + two bulk Vec.@append (RawMem.copy memmove) + // steal-into-str (reuse buffer, no second memcpy). Zero push-loop. // Chain-форма: точный pre-alloc + fluent-append (@append -> @). mut out []u8 = []u8.new(cap: a.len() + b.len()).append(a).append(b) // Zero-copy кража буфера: `out` валиден по построению — `alloc_copy` платил // ВТОРУЮ полную копию; `into_str_unchecked` забирает буфер как есть (cap-хвост // висит на той же GC-аллокации — как у SB @into_str). unsafe { out.into_str_unchecked() } } // Оператор `+`: `s1 + s2 == s1.@plus(s2)` → @concat. export fn str @plus(other str) -> str => @concat(other) // Pad строку с начала до width CODEPOINT'ов символом fill. Если width <= длины — s. // Ширина в codepoint'ах (`chars().count()`), не байтах — иначе для multibyte // `"é".pad_start(3,'·')` дало бы 1 паддинг вместо 2. export fn str @pad_start(width int, fill char) -> str { ro pad = width - @chars().count() if pad <= 0 { return @ } ro fill_s = fill.to_str() StringBuilder.new(cap: @byte_len() + pad * fill_s.byte_len()).append_repeat(fill_s, pad).append(@).into_str() } // Pad строку с конца до width CODEPOINT'ов символом fill. Если width <= длины — s. export fn str @pad_end(width int, fill char) -> str { ro pad = width - @chars().count() if pad <= 0 { return @ } ro fill_s = fill.to_str() StringBuilder.new(cap: @byte_len() + pad * fill_s.byte_len()).append(@).append_repeat(fill_s, pad).into_str() } // Pad с обеих сторон до width CODEPOINT'ов (левый паддинг = floor(pad/2)). export fn str @pad_center(width int, fill char) -> str { ro pad = width - @chars().count() if pad <= 0 { return @ } ro left = pad / 2 ro right = pad - left ro fill_s = fill.to_str() StringBuilder.new(cap: @byte_len() + pad * fill_s.byte_len()).append_repeat(fill_s, left).append(@).append_repeat(fill_s, right).into_str() } // Повторение строки n раз. n == 0 → пустая строка. n < 0 — contract // violation: negative repeat-count — caller bug, не graceful degenerate case. export fn str @repeat(n int) -> str requires n >= 0 { if n <= 0 { return "" } StringBuilder.new(cap: @byte_len() * n).append_repeat(@, n).into_str() } // Заменить все non-overlapping occurrences `from` на `to`. Empty `from` → s unchanged. // O(n) via StringBuilder: single scan with find, no concat-loop allocs. export fn str @replace(from str, to str) -> str { ro fn_len = from.byte_len() if fn_len == 0 { return @ } ro to_len = to.byte_len() ro estimated = @byte_len() + (to_len - fn_len).max(0) consume sb = StringBuilder.new(cap: estimated) mut rest = @ while true { match rest.find(from) { Some(k) => { sb.append(rest[..k]).append(to) rest = rest[k + fn_len ..] } None => break } } sb.append(rest).into_str() } // Заменить первые `n` non-overlapping вхождений `from` на `to` (Rust `replacen`). // O(n) via StringBuilder: single scan with find. n == 0 → s unchanged; n < 0 — // contract violation. export fn str @replacen(from str, to str, n int) -> str requires n >= 0 { ro fn_len = from.byte_len() if n <= 0 || fn_len == 0 { return @ } consume sb = StringBuilder.new(cap: @byte_len()) mut rest = @ mut count = 0 while count < n { match rest.find(from) { Some(k) => { sb.append(rest[..k]).append(to) rest = rest[k + fn_len ..] count += 1 } None => break } } sb.append(rest).into_str() }