Initial project import

This commit is contained in:
Agila Forge Importer committed 2026-07-13 15:20:32 +00:00
commit 23f2d29e32
11957 files changed
+1754577

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
## Description
`strings` provides utilities for efficiently processing large strings.
If you got here looking for methods available on the `string` struct, those
methods are found in the `builtin` module.
+470
View File
@@ -0,0 +1,470 @@
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module strings
// strings.Builder is used to efficiently append many strings to a large
// dynamically growing buffer, then use the resulting large string. Using
// a string builder is much better for performance/memory usage than doing
// constantly string concatenation.
pub type Builder = []u8
// new_builder returns a new string builder, with an initial capacity of `initial_size`.
pub fn new_builder(initial_size int) Builder {
mut res := Builder([]u8{cap: initial_size})
unsafe { res.flags.set(.noslices) }
return res
}
// reuse_as_plain_u8_array allows using the Builder instance as a plain []u8 return value.
// It is useful, when you have accumulated data in the builder, that you want to
// pass/access as []u8 later, without copying or freeing the buffer.
// NB: you *should NOT use* the string builder instance after calling this method.
// Use only the return value after calling this method.
@[unsafe]
pub fn (mut b Builder) reuse_as_plain_u8_array() []u8 {
unsafe { b.flags.clear(.noslices) }
return *b
}
// write_ptr writes `len` bytes provided byteptr to the accumulated buffer
@[unsafe]
pub fn (mut b Builder) write_ptr(ptr &u8, len int) {
if len == 0 {
return
}
unsafe { b.push_many(ptr, len) }
}
// write_rune appends a single rune to the accumulated buffer
@[manualfree]
pub fn (mut b Builder) write_rune(r rune) {
mut buffer := [5]u8{}
res := unsafe { utf32_to_str_no_malloc(u32(r), mut &buffer[0]) }
if res.len == 0 {
return
}
unsafe { b.push_many(res.str, res.len) }
}
// write_runes appends all the given runes to the accumulated buffer.
pub fn (mut b Builder) write_runes(runes []rune) {
mut buffer := [5]u8{}
for r in runes {
res := unsafe { utf32_to_str_no_malloc(u32(r), mut &buffer[0]) }
if res.len == 0 {
continue
}
unsafe { b.push_many(res.str, res.len) }
}
}
// write_u8 appends a single `data` byte to the accumulated buffer
@[inline]
pub fn (mut b Builder) write_u8(data u8) {
b << data
}
// write_byte appends a single `data` byte to the accumulated buffer
@[inline]
pub fn (mut b Builder) write_byte(data u8) {
b << data
}
// write_decimal appends a decimal representation of the number `n` into the builder `b`,
// without dynamic allocation. The higher order digits come first, i.e. 6123 will be written
// with the digit `6` first, then `1`, then `2` and `3` last.
@[direct_array_access]
pub fn (mut b Builder) write_decimal(n i64) {
if n == 0 {
b.write_u8(0x30)
return
}
if n == min_i64 {
b.write_string(n.str())
return
}
mut buf := [25]u8{}
mut x := if n < 0 { -n } else { n }
mut i := 24
for x != 0 {
nextx := x / 10
r := x % 10
buf[i] = u8(r) + 0x30
x = nextx
i--
}
if n < 0 {
buf[i] = `-`
i--
}
unsafe { b.write_ptr(&buf[i + 1], 24 - i) }
}
// write implements the io.Writer interface, that is why it returns how many bytes were written to the string builder.
pub fn (mut b Builder) write(data []u8) !int {
if data.len == 0 {
return 0
}
unsafe { b.push_many(data.data, data.len) }
return data.len
}
// drain_builder writes all of the `other` builder content, then re-initialises
// `other`, so that the `other` strings builder is ready to receive new content.
@[manualfree]
pub fn (mut b Builder) drain_builder(mut other Builder, other_new_cap int) {
if other.len > 0 {
b << *other
}
unsafe { other.free() }
other = new_builder(other_new_cap)
}
// byte_at returns a byte, located at a given index `i`.
// Note: it can panic, if there are not enough bytes in the strings builder yet.
@[inline]
pub fn (b &Builder) byte_at(n int) u8 {
return unsafe { (&[]u8(b))[n] }
}
// write appends the string `s` to the buffer
@[expand_simple_interpolation; inline]
pub fn (mut b Builder) write_string(s string) {
if s.len == 0 {
return
}
unsafe { b.push_many(s.str, s.len) }
// for c in s {
// b.buf << c
// }
// b.buf << []u8(s) // TODO
}
// write_string2 appends the strings `s1` and `s2` to the buffer.
@[inline]
pub fn (mut b Builder) write_string2(s1 string, s2 string) {
if s1.len != 0 {
unsafe { b.push_many(s1.str, s1.len) }
}
if s2.len != 0 {
unsafe { b.push_many(s2.str, s2.len) }
}
}
// go_back discards the last `n` bytes from the buffer.
pub fn (mut b Builder) go_back(n int) {
b.trim(b.len - n)
}
// spart returns a part of the buffer as a string
@[inline]
pub fn (b &Builder) spart(start_pos int, n int) string {
unsafe {
mut x := malloc_noscan(n + 1)
vmemcpy(x, &u8(b.data) + start_pos, n)
x[n] = 0
return tos(x, n)
}
}
// cut_last cuts the last `n` bytes from the buffer and returns them.
pub fn (mut b Builder) cut_last(n int) string {
cut_pos := b.len - n
res := b.spart(cut_pos, n)
b.trim(cut_pos)
return res
}
// cut_to cuts the string after `pos` and returns it.
// if `pos` is superior to builder length, returns an empty string
// and cancel further operations
pub fn (mut b Builder) cut_to(pos int) string {
if pos > b.len {
return ''
}
return b.cut_last(b.len - pos)
}
// go_back_to resets the buffer to the given position `pos`.
// Note: pos should be < than the existing buffer length.
pub fn (mut b Builder) go_back_to(pos int) {
b.trim(pos)
}
// writeln appends the string `s`, and then a newline character.
@[inline]
pub fn (mut b Builder) writeln(s string) {
// for c in s {
// b.buf << c
// }
if s != '' {
unsafe { b.push_many(s.str, s.len) }
}
// b.buf << []u8(s) // TODO
b << u8(`\n`)
}
// writeln2 appends two strings: `s1` + `\n`, and `s2` + `\n`, to the buffer.
@[inline]
pub fn (mut b Builder) writeln2(s1 string, s2 string) {
if s1 != '' {
unsafe { b.push_many(s1.str, s1.len) }
}
b << u8(`\n`)
if s2 != '' {
unsafe { b.push_many(s2.str, s2.len) }
}
b << u8(`\n`)
}
// last_n(5) returns 'world'
// buf == 'hello world'
pub fn (b &Builder) last_n(n int) string {
if n > b.len {
return ''
}
return b.spart(b.len - n, n)
}
// after(6) returns 'world'
// buf == 'hello world'
pub fn (b &Builder) after(n int) string {
if n >= b.len {
return ''
}
return b.spart(n, b.len - n)
}
// str returns a copy of all of the accumulated buffer content.
// Note: after a call to b.str(), the builder b will be empty, and could be used again.
// The returned string *owns* its own separate copy of the accumulated data that was in
// the string builder, before the .str() call.
pub fn (mut b Builder) str() string {
b << u8(0)
bcopy := unsafe { &u8(memdup_noscan(b.data, b.len)) }
s := unsafe { bcopy.vstring_with_len(b.len - 1) }
b.clear()
return s
}
// ensure_cap ensures that the buffer has enough space for at least `n` bytes by growing the buffer if necessary.
pub fn (mut b Builder) ensure_cap(n int) {
// Work through the underlying array pointer, instead of taking a pointer
// cast to the alias receiver. This keeps self-hosted builds from generating
// an invalid `&b` cast in C.
mut arr := unsafe { &[]u8(b) }
arr.ensure_cap(n)
}
// grow_len grows the length of the buffer by `n` bytes if necessary
@[unsafe]
pub fn (mut b Builder) grow_len(n int) {
if n <= 0 {
return
}
new_len := b.len + n
b.ensure_cap(new_len)
unsafe {
b.len = new_len
}
}
// free frees the memory block, used for the buffer.
// Note: do not use the builder, after a call to free().
@[unsafe]
pub fn (mut b Builder) free() {
if b.data != 0 {
mut arr := unsafe { &[]u8(b) }
unsafe { arr.free() }
}
}
// write_repeated_rune appends multiple copies of the same rune to the accumulated buffer
@[direct_array_access]
pub fn (mut b Builder) write_repeated_rune(r rune, count int) {
if count <= 0 {
return
}
// Convert rune to UTF-8 bytes once
mut buffer := [5]u8{}
res := unsafe { utf32_to_str_no_malloc(u32(r), mut &buffer[0]) }
if res.len == 0 {
return
}
if res.len == 1 {
b.ensure_cap(b.len + count)
unsafe {
vmemset(&u8(b.data) + b.len, buffer[0], count)
b.len += count
}
return
} else {
total_needed := count * res.len
b.ensure_cap(b.len + total_needed)
mut dest := unsafe { &u8(b.data) + b.len }
for _ in 0 .. count {
unsafe {
vmemcpy(dest, res.str, res.len)
dest += res.len
}
}
unsafe {
b.len += total_needed
}
}
}
// IndentParam holds configuration parameters for the indent() function
@[params]
pub struct IndentParam {
pub mut:
block_start rune = `{` // Character that starts a new block (+ indent)
block_end rune = `}` // Character that ends a new block (- indent)
indent_char rune = ` ` // Character used for indentation (space or tab)
indent_count int = 4 // Number of indent_char per indentation level
starting_level int // Initial indentation level (0 = no initial indent)
}
// IndentState represents the current parsing state of the indent() function
enum IndentState {
normal // Normal state, processing regular characters
in_string // Inside a string literal, ignoring formatting characters
}
// indent formats a string by applying structured indentation based on block delimiters.
// It processes the input string `s` and writes the formatted output to the `Builder` `b`.
// The function preserves content inside string literals (both single and double quotes) and
// configures indentation behavior through the `param` structure.
//
// Key behaviors:
// 1. Removes existing indentation at the beginning of lines.
// 2. Applies new indentation based on block nesting levels.
// 3. Ignores block delimiters and formatting characters inside string literals.
// 4. Keeps empty blocks (e.g., {}) on the same line.
// 5. Inserts newlines after `block_start` and before `block_end` (except for empty blocks).
// 6. Maintains existing line breaks from the input.
//
// Example:
// ```v
// import strings
// input := 'User{name:"John" settings:{theme:"dark"}}'
// mut b := strings.new_builder(64)
// b.indent(input, indent_count: 2)
// println(b.str()) // Formatted output: 'User{\n name:"John" settings:{\n theme:"dark"\n }\n}'
// ```
@[direct_array_access]
pub fn (mut b Builder) indent(s string, param IndentParam) {
if s.len == 0 {
return
}
mut state := IndentState.normal
mut indent_level := param.starting_level
mut string_char := `\0`
mut at_line_start := true
for i := 0; i < s.len; i++ {
c := rune(s[i])
match state {
// Normal state: process characters outside of string literals
.normal {
match c {
`"`, `'` { // Note: quote characters for editor display "
state = .in_string
string_char = c
// Add indentation if at the start of a line
if at_line_start {
b.write_repeated_rune(param.indent_char,
indent_level * param.indent_count)
at_line_start = false
}
// Write the opening quote
b.write_rune(c)
}
param.block_start {
// Start of a new block
// Add indentation if at the start of a line
if at_line_start {
b.write_repeated_rune(param.indent_char,
indent_level * param.indent_count)
at_line_start = false
}
// Write the block start character
b.write_rune(c)
// Check for empty block (e.g., {})
// Empty blocks stay on the same line
if i + 1 < s.len && s[i + 1] == param.block_end {
b.write_rune(param.block_end)
i++
} else {
// Non-empty block: increase indentation and add newline
indent_level++
b.write_rune(`\n`)
at_line_start = true
}
}
param.block_end {
// End of a block
// Decrease indentation level (but not below 0)
if indent_level > 0 {
indent_level--
}
// If not at the start of a line, add a newline
if !at_line_start {
b.write_rune(`\n`)
}
// Add indentation for the block end
b.write_repeated_rune(param.indent_char, indent_level * param.indent_count)
at_line_start = false
b.write_rune(c)
}
` `, `\t`, `\r`, `\n` {
// Whitespace characters
// Only write whitespace if not at the start of a line
if !at_line_start {
b.write_rune(c)
}
// Newline resets the line start flag
if c == `\n` {
at_line_start = true
}
}
else {
// Any other character
// Add indentation if at the start of a line
if at_line_start {
b.write_repeated_rune(param.indent_char,
indent_level * param.indent_count)
at_line_start = false
}
b.write_rune(c)
}
}
}
.in_string {
// Inside a string literal: preserve all characters as-is
b.write_rune(c)
// Check for string termination
// The character must match the opening quote and not be escaped
if c == string_char {
if s[i - 1] != `\\` {
state = .normal
string_char = `\0`
}
}
}
}
}
}
+125
View File
@@ -0,0 +1,125 @@
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module strings
/*
pub struct Builder {
mut:
buf []u8
pub mut:
len int
initial_size int = 1
}*/
pub type Builder = []u8
pub fn new_builder(initial_size int) Builder {
return []u8{cap: initial_size}
}
pub fn (mut b Builder) write_byte(data u8) {
b << data
}
pub fn (mut b Builder) write_u8(data u8) {
b << data
}
pub fn (mut b Builder) write(data []u8) ?int {
if data.len == 0 {
return 0
}
b << data
return data.len
}
pub fn (b &Builder) byte_at(n int) u8 {
unsafe {
return b[n]
}
}
pub fn (mut b Builder) write_string(s string) {
if s == '' {
return
}
for c in s {
b << c
}
}
pub fn (mut b Builder) writeln(s string) {
if s != '' {
b.write_string(s)
}
b << 10
}
pub fn (mut b Builder) str() string {
s := ''
#for (const c of b.val.arr.arr)
#s.str += String.fromCharCode(+c)
b.clear()
return s
}
pub fn (mut b Builder) cut_last(n int) string {
cut_pos := b.len - n
x := unsafe { b[cut_pos..] }
res := x.bytestr()
b.trim(cut_pos)
return res
}
pub fn (mut b Builder) go_back_to(pos int) {
b.trim(pos)
}
// go_back discards the last `n` bytes from the buffer.
pub fn (mut b Builder) go_back(n int) {
b.trim(b.len - n)
}
// cut_to cuts the string after `pos` and returns it.
// if `pos` is superior to builder length, returns an empty string
// and cancel further operations
pub fn (mut b Builder) cut_to(pos int) string {
if pos > b.len {
return ''
}
return b.cut_last(b.len - pos)
}
pub fn (mut b Builder) write_runes(runes []rune) {
for r in runes {
res := r.str()
#res.str = String.fromCharCode(r.val)
b << res.bytes()
}
}
// after(6) returns 'world'
// buf == 'hello world'
pub fn (mut b Builder) after(n int) string {
if n >= b.len {
return ''
}
x := unsafe { b[n..b.len] }
return x.bytestr()
}
// last_n(5) returns 'world'
// buf == 'hello world'
pub fn (mut b Builder) last_n(n int) string {
if n >= b.len {
return ''
}
x := unsafe { b[b.len - n..b.len] }
return x.bytestr()
}
+98
View File
@@ -0,0 +1,98 @@
// vtest build: present_node?
import strings
type MyInt = int
const maxn = 100000
fn test_sb() {
mut sb := strings.new_builder(100)
sb.write_string('hi')
sb.write_string('!')
sb.write_string('hello')
assert sb.len == 8
sb_end := sb.str()
assert sb_end == 'hi!hello'
assert sb.len == 0
///
sb = strings.new_builder(10)
sb.write_string('a')
sb.write_string('b')
assert sb.len == 2
assert sb.str() == 'ab'
// Test interpolation optimization
sb = strings.new_builder(10)
x := 10
y := MyInt(20)
sb.writeln('x = ${x} y = ${y}')
res := sb.str()
assert res[res.len - 1] == `\n`
println('"${res}"')
assert res.trim_space() == 'x = 10 y = 20'
sb = strings.new_builder(10)
sb.write_string('x = ${x} y = ${y}')
assert sb.str() == 'x = 10 y = 20'
//$if !windows {
sb = strings.new_builder(10)
sb.write_string('123456')
last_2 := sb.cut_last(2)
assert last_2 == '56'
final_sb := sb.str()
assert final_sb == '1234'
sb.clear()
assert sb.str() == ''
//}
}
fn test_big_sb() {
mut sb := strings.new_builder(100)
mut sb2 := strings.new_builder(10000)
for i in 0 .. maxn {
sb.writeln(i.str())
sb2.write_string('+')
}
s := sb.str()
lines := s.split_into_lines()
assert lines.len == maxn
assert lines[0] == '0'
assert lines[1] == '1'
assert lines[777] == '777'
assert lines[98765] == '98765'
println(sb2.len)
assert sb2.len == maxn
}
fn test_byte_write() {
mut sb := strings.new_builder(100)
temp_str := 'byte testing'
mut count := 0
for word in temp_str {
sb.write_u8(word)
count++
assert count == sb.len
}
sb_final := sb.str()
assert sb_final == temp_str
}
fn test_strings_builder_reuse() {
mut sb := strings.new_builder(256)
sb.write_string('world')
assert sb.str() == 'world'
sb.write_string('hello')
assert sb.str() == 'hello'
}
fn test_cut_to() {
mut sb := strings.new_builder(16)
sb.write_string('hello')
assert sb.cut_to(3) == 'lo'
assert sb.len == 3
assert sb.cut_to(3) == ''
assert sb.len == 3
assert sb.cut_to(0) == 'hel'
assert sb.cut_to(32) == ''
assert sb.len == 0
}
+417
View File
@@ -0,0 +1,417 @@
import strings
type MyInt = int
const maxn = 100000
fn test_sb() {
mut sb := strings.new_builder(100)
sb.write_string('hi')
sb.write_string('!')
sb.write_string('hello')
assert sb.len == 8
sb_end := sb.str()
assert sb_end == 'hi!hello'
assert sb.len == 0
///
sb = strings.new_builder(10)
sb.write_string('a')
sb.write_string('b')
assert sb.len == 2
assert sb.str() == 'ab'
// Test interpolation optimization
sb = strings.new_builder(10)
x := 10
y := MyInt(20)
sb.writeln('x = ${x} y = ${y}')
res := sb.str()
assert res[res.len - 1] == `\n`
println('"${res}"')
assert res.trim_space() == 'x = 10 y = 20'
sb = strings.new_builder(10)
sb.write_string('x = ${x} y = ${y}')
assert sb.str() == 'x = 10 y = 20'
//$if !windows {
sb = strings.new_builder(10)
sb.write_string('123456')
last_2 := sb.cut_last(2)
assert last_2 == '56'
final_sb := sb.str()
assert final_sb == '1234'
//}
sb.clear()
assert sb.str() == ''
}
fn test_big_sb() {
mut sb := strings.new_builder(100)
mut sb2 := strings.new_builder(10000)
for i in 0 .. maxn {
sb.writeln(i.str())
sb2.write_string('+')
}
s := sb.str()
lines := s.split_into_lines()
assert lines.len == maxn
assert lines[0] == '0'
assert lines[1] == '1'
assert lines[777] == '777'
assert lines[98765] == '98765'
println(sb2.len)
assert sb2.len == maxn
}
fn test_byte_write() {
mut sb := strings.new_builder(100)
temp_str := 'byte testing'
mut count := 0
for word in temp_str {
sb.write_u8(word)
count++
assert count == sb.len
}
sb_final := sb.str()
assert sb_final == temp_str
}
fn test_strings_builder_reuse() {
mut sb := strings.new_builder(256)
sb.write_string('world')
assert sb.str() == 'world'
sb.write_string('hello')
assert sb.str() == 'hello'
}
fn test_cut_to() {
mut sb := strings.new_builder(16)
sb.write_string('hello')
assert sb.cut_to(3) == 'lo'
assert sb.len == 3
assert sb.cut_to(3) == ''
assert sb.len == 3
assert sb.cut_to(0) == 'hel'
assert sb.cut_to(32) == ''
assert sb.len == 0
}
fn test_write_rune() {
mut sb := strings.new_builder(10)
sb.write_rune(`h`)
sb.write_rune(`e`)
sb.write_rune(`l`)
sb.write_rune(`l`)
sb.write_rune(`o`)
x := sb.str()
assert x == 'hello'
}
fn test_write_runes() {
mut sb := strings.new_builder(20)
sb.write_runes([`h`, `e`, `l`, `l`, `o`])
sb.write_rune(` `)
sb.write_runes([`w`, `o`, `r`, `l`, `d`])
x := sb.str()
assert x == 'hello world'
}
fn test_ensure_cap() {
mut sb := strings.new_builder(0)
assert sb.cap == 0
sb.ensure_cap(10)
assert sb.cap >= 10
old_cap := sb.cap
sb.ensure_cap(10)
assert sb.cap == old_cap
sb.ensure_cap(15)
assert sb.cap >= 15
old_cap2 := sb.cap
sb.ensure_cap(10)
assert sb.cap == old_cap2
sb.ensure_cap(-1)
assert sb.cap == old_cap2
}
fn test_drain_builder() {
mut sb := strings.new_builder(0)
mut target_sb := strings.new_builder(0)
assert sb.cap == 0
assert target_sb.cap == 0
sb.write_string('abc')
assert sb.len == 3
target_sb.drain_builder(mut sb, 0)
assert sb.len == 0
assert target_sb.len == 3
assert target_sb.str() == 'abc'
}
@[manualfree]
fn sb_i64_str(n i64) string {
mut sb := strings.new_builder(24)
defer {
unsafe { sb.free() }
}
sb.write_decimal(n)
return sb.str()
}
fn test_write_decimal() {
assert sb_i64_str(0) == '0'
assert sb_i64_str(1) == '1'
assert sb_i64_str(-1) == '-1'
assert sb_i64_str(1001) == '1001'
assert sb_i64_str(-1001) == '-1001'
assert sb_i64_str(1234567890) == '1234567890'
assert sb_i64_str(-1234567890) == '-1234567890'
assert sb_i64_str(9223372036854775807) == '9223372036854775807'
assert sb_i64_str(-9223372036854775807) == '-9223372036854775807'
assert sb_i64_str(min_i64) == '-9223372036854775808'
}
fn test_grow_len() {
mut sb := strings.new_builder(10)
assert sb.len == 0
assert sb.cap == 10
sb.write_string('0123456789')
assert sb.len == 10
unsafe { sb.grow_len(-5) }
assert sb.len == 10
assert sb.cap == 10
unsafe { sb.grow_len(10) }
assert sb.len == 20
assert sb.cap == 20
sb.ensure_cap(35)
assert sb.len == 20
assert sb.cap >= 35
cap_after_ensure := sb.cap
unsafe { sb.grow_len(5) }
assert sb.len == 25
assert sb.cap == cap_after_ensure
}
fn test_write_repeated_rune() {
mut sb := strings.new_builder(20)
sb.write_repeated_rune(`h`, 5)
sb.write_repeated_rune(`w`, 5)
sb.write_repeated_rune(`√`, 5)
sb.write_rune(` `)
x := sb.str()
assert x == 'hhhhhwwwww√√√√√ '
}
struct IndentTest {
param strings.IndentParam
input string
output string
}
// vfmt off
const indent_test_data = [
IndentTest{
input: 'User1 {
name: "John"
settings: {
theme: "dark"
language: "en"
}
}'
output: 'User1 {
name: "John"
settings: {
theme: "dark"
language: "en"
}
}'
},
IndentTest{
input: 'User2{name:"John" settings:{theme:"dark" language:"en" hobbies:["reading","sports"]}}'
output: 'User2{
name:"John" settings:{
theme:"dark" language:"en" hobbies:["reading","sports"]
}
}'
},
IndentTest{
input: 'message {text: "Hello {world}!" count: 5 nested: {data: "Test {inner}"}}'
output: 'message {
text: "Hello {world}!" count: 5 nested: {
data: "Test {inner}"
}
}'
},
IndentTest{
input: 'if x > 0 {println("Positive") for i in 0..x {println(i) if i % 2 == 0 {println("even")}}} else {println("Not positive")}'
output: 'if x > 0 {
println("Positive") for i in 0..x {
println(i) if i % 2 == 0 {
println("even")
}
}
} else {
println("Not positive")
}'
},
IndentTest{
input: 'config{database:{host:"localhost" port:5432} server:{port:8080 routes:{api:"/api" static:"/static"}} log:{level:"info"}}'
output: 'config{
database:{
host:"localhost" port:5432
} server:{
port:8080 routes:{
api:"/api" static:"/static"
}
} log:{
level:"info"
}
}'
},
IndentTest{
input: "MyS{
a: 0
b: 0
son: Son{
k: ''
m: ''
}
}
"
output: "MyS{
a: 0
b: 0
son: Son{
k: ''
m: ''
}
}
"
},
IndentTest{
input: 'config {message: "He said: \\"Hello World!\\"" code: "if (x) { return \\"value\\"; }"}'
output: 'config {
message: "He said: \\"Hello World!\\"" code: "if (x) { return \\"value\\"; }"
}'
},
IndentTest {
input : 'data {text: "String with {curly braces} and \\"quotes\\"" nested: {value: "Inner \\"quoted\\" string"}}'
output : 'data {
text: "String with {curly braces} and \\"quotes\\"" nested: {
value: "Inner \\"quoted\\" string"
}
}'
},
IndentTest {
input : 'escape {backslash: "Path: C:\\\\Users\\\\Test" newline: "Line1\\nLine2" tab: "Column1\\tColumn2"}'
output : 'escape {
backslash: "Path: C:\\\\Users\\\\Test" newline: "Line1\\nLine2" tab: "Column1\\tColumn2"
}'
},
IndentTest {
input : 'nested {outer: "Outer \\"quote\\" with {braces}" inner: {value: "Inner string with \\\\backslash and \\"quotes\\""}}'
output : 'nested {
outer: "Outer \\"quote\\" with {braces}" inner: {
value: "Inner string with \\\\backslash and \\"quotes\\""
}
}'
},
IndentTest {
input : 'complex {str1: "\\"Escaped quotes\\"" str2: "Backslash: \\\\" str3: "Mixed: \\"quoted\\" and \\\\backslash" regex: "Pattern: \\\\d+\\\\.\\\\d+"}'
output : 'complex {
str1: "\\"Escaped quotes\\"" str2: "Backslash: \\\\" str3: "Mixed: \\"quoted\\" and \\\\backslash" regex: "Pattern: \\\\d+\\\\.\\\\d+"
}'
},
IndentTest {
input : 'json {data: "{\\"name\\": \\"John\\", \\"age\\": 30, \\"city\\": \\"New York\\"}"}'
output : 'json {
data: "{\\"name\\": \\"John\\", \\"age\\": 30, \\"city\\": \\"New York\\"}"
}'
},
IndentTest {
input : 'code {function: "fn test() { if (x) { return \\\"value\\\"; } }" error: "Error: \\\"File not found\\\" at line 10"}'
output : 'code {
function: "fn test() { if (x) { return \\\"value\\\"; } }" error: "Error: \\\"File not found\\\" at line 10"
}'
},
IndentTest {
input : 'multiline {message: "Line 1\\nLine 2\\nLine 3 with {braces}\\nLine 4 with \\\"quotes\\\""}'
output : 'multiline {
message: "Line 1\\nLine 2\\nLine 3 with {braces}\\nLine 4 with \\\"quotes\\\""
}'
},
IndentTest {
input : 'extreme {str: "\\\\\\\\\\\\\\"Quadruple escaped\\\\\\\\\\\\\\""}'
output : 'extreme {
str: "\\\\\\\\\\\\\\"Quadruple escaped\\\\\\\\\\\\\\""
}'
},
IndentTest{
param: strings.IndentParam {
block_start: `[`
block_end: `]`
}
input: 'message [text: "Hello {world}!" count: 5 nested: [data: "Test {inner}"]]'
output: 'message [
text: "Hello {world}!" count: 5 nested: [
data: "Test {inner}"
]
]'
},
IndentTest{
param: strings.IndentParam {
block_start: `[`
block_end: `]`
indent_char: `#`
}
input: 'message [text: "Hello {world}!" count: 5 nested: [data: "Test {inner}"]]'
output: 'message [
####text: "Hello {world}!" count: 5 nested: [
########data: "Test {inner}"
####]
]'
},
IndentTest{
param: strings.IndentParam {
block_start: `[`
block_end: `]`
indent_char: `\t`
indent_count: 2
}
input: 'message [text: "Hello {world}!" count: 5 nested: [data: "Test {inner}"]]'
output: 'message [
\t\ttext: "Hello {world}!" count: 5 nested: [
\t\t\t\tdata: "Test {inner}"
\t\t]
]'
},
IndentTest{
param: strings.IndentParam {
block_start: `[`
block_end: `]`
indent_char: `#`
indent_count: 1
starting_level: 2
}
input: 'message [text: "Hello {world}!" count: 5 nested: [data: "Test {inner}"]]'
output: '##message [
###text: "Hello {world}!" count: 5 nested: [
####data: "Test {inner}"
###]
##]'
},
]
// vfmt on
fn test_indent() {
for t in indent_test_data {
mut sb := strings.new_builder(256)
sb.indent(t.input, t.param)
assert sb.str() == t.output
}
}
+224
View File
@@ -0,0 +1,224 @@
module lorem
const lorem_corpora = {
'lorem': lorem_corpus
'poe': poe_corpus
'darwin': darwin_corpus
'bard': shakespeare_corpus
}
const lorem_corpus = '
lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua Ut enim ad minim veniam quis
nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore
eu fugiat nulla pariatur Excepteur sint occaecat cupidatat non proident sunt
in culpa qui officia deserunt mollit anim id est laborum
'
const poe_corpus = '
once upon a midnight dreary while I pondered weak and weary
over many a quaint and curious volume of forgotten lore
while I nodded nearly napping suddenly there came a tapping
as of someone gently rapping at my chamber door
'
const darwin_corpus = '
when we look to the individuals of the same variety or sub variety of our
older cultivated plants and animals one of the first points which strikes
us is that they generally differ much more from each other than do the
individuals of any one species or variety in a state of nature
'
const shakespeare_corpus = '
to be or not to be that is the question
all the worlds a stage and all the men and women merely players
the lady doth protest too much methinks
a rose by any other name would smell as sweet
et tu brute
if music be the food of love play on
now is the winter of our discontent
we are such stuff as dreams are made on
brevity is the soul of wit
some are born great some achieve greatness and some have greatness thrust upon them
cry havoc and let slip the dogs of war
all that glisters is not gold
the fault dear brutus is not in our stars but in ourselves
to thine own self be true
lord what fools these mortals be
shall i compare thee to a summers day
'
// ---------------- Seed Phrases ----------------
const lorem_seed_phrases = [
'in the beginning',
'once upon a time',
'it was the first',
'when we consider',
'there was a moment',
]
// ---------------- Text Generation ----------------
// LoremCfg
// - `corpus_name` random if not specified
// - `seed_text` random if not specified
// - `rng_seed` random if not specified
@[params]
pub struct LoremCfg {
pub:
words_per_sentence int = 10
sentences_per_paragraph int = 5
paragraphs int = 3
markov_order int = 2
corpus_name string
seed_text string
rng_seed int
}
// lorum - pseudo-random text using a Markov chain built from
// one of several embedded corpora. It produces structured output in the
// form of paragraphs and sentences, with configurable parameters for:
//
// - Markov order (n-gram size)
// - Words per sentence
// - Sentences per paragraph
// - Paragraph count
// - Optional seed phrases and RNG seed
// - Optional corpus selection
//
pub fn generate(cfg LoremCfg) string {
// Initialize LCG with provided seed or default
// If rng_seed is 0, we use a default seed (making it deterministic by default)
initial_seed := if cfg.rng_seed != 0 { u32(cfg.rng_seed) } else { u32(123456789) }
mut rng := LorumRNG{
seed: initial_seed
}
seed := match cfg.seed_text != '' {
true { cfg.seed_text }
else { lorem_random_seed_phrase(mut rng) }
}
corpus := lorem_select_corpus(mut rng, cfg.corpus_name)
tokens := lorem_tokenize(corpus)
if tokens.len <= cfg.markov_order {
eprintln('corpus too small for selected order')
return ''
}
model := lorem_build_markov(tokens, cfg.markov_order)
mut state := lorem_tokenize(seed)
if state.len < cfg.markov_order {
start := rng.intn(tokens.len - cfg.markov_order)
state = tokens[start..start + cfg.markov_order].clone()
}
mut out := []string{}
for pi in 0 .. cfg.paragraphs {
if pi != 0 {
out << '\n\n'
}
sentences := lorem_vary(mut rng, cfg.sentences_per_paragraph, 1)
for si in 0 .. sentences {
if si != 0 {
out << ' '
}
words := lorem_vary(mut rng, cfg.words_per_sentence, 3)
mut sentence := []string{}
for _ in 0 .. words {
key := state.join('\u0001')
nexts := model[key] or {
start := rng.intn(tokens.len - cfg.markov_order)
state = tokens[start..start + cfg.markov_order].clone()
continue
}
next := nexts[rng.intn(nexts.len)]
sentence << next
state = state[1..].clone()
state << next
}
if sentence.len > 0 {
out << sentence.join(' ').capitalize()
out << '.'
}
}
}
return out.join('')
}
// ---------------- Utilities ----------------
// LorumRNG is a simple internal PRNG used to avoid importing the `rand` module,
// which would cause an import cycle (strings -> rand -> time -> ... -> strings).
struct LorumRNG {
mut:
seed u32
}
// intn returns a pseudo-random number in [0, max).
// It is a minimal implementation sufficient for text generation.
fn (mut r LorumRNG) intn(max int) int {
if max <= 0 {
return 0
}
r.seed = r.seed * 1664525 + 1013904223
return int(r.seed % u32(max))
}
fn lorem_vary(mut rng LorumRNG, base int, min int) int {
delta := int(f32(base) * 0.2)
if delta == 0 {
return base
}
offset := rng.intn(delta * 2 + 1) - delta
val := base + offset
return if val < min { min } else { val }
}
fn lorem_select_corpus(mut rng LorumRNG, name string) string {
if name != '' {
if corpus := lorem_corpora[name] {
return corpus
}
eprintln('unknown corpus: ${name}')
exit(1)
}
keys := lorem_corpora.keys()
key := keys[rng.intn(keys.len)]
return lorem_corpora[key]
}
fn lorem_random_seed_phrase(mut rng LorumRNG) string {
return lorem_seed_phrases[rng.intn(lorem_seed_phrases.len)]
}
fn lorem_tokenize(text string) []string {
return text
.replace_each(['\n', ' ', '\t', ' '])
.split(' ')
.filter(it.len > 0)
}
fn lorem_build_markov(tokens []string, order int) map[string][]string {
mut model := map[string][]string{}
for i in 0 .. tokens.len - order {
key := tokens[i..i + order].join('\u0001')
if key !in model {
model[key] = []string{}
}
model[key] << tokens[i + order]
}
return model
}
+66
View File
@@ -0,0 +1,66 @@
module lorem
fn test_lorem_generate_basic() {
output := generate(LoremCfg{
paragraphs: 2
sentences_per_paragraph: 3
words_per_sentence: 5
})
assert output.len > 0
// 2 paragraphs should be separated by "\n\n"
assert output.count('\n\n') == 1
}
fn test_lorem_generate_deterministic() {
cfg := LoremCfg{
rng_seed: 12345
paragraphs: 1
}
out1 := generate(cfg)
out2 := generate(cfg)
assert out1 == out2
}
fn test_lorem_generate_counts() {
cfg := LoremCfg{
paragraphs: 3
}
output := generate(cfg)
// There should be 2 separators for 3 paragraphs
assert output.count('\n\n') == 2
}
fn test_lorem_custom_corpus() {
// 'bard' is shakespeare
cfg := LoremCfg{
corpus_name: 'bard'
rng_seed: 999
paragraphs: 1
}
output := generate(cfg)
assert output.len > 0
// Hard to check exact content due to randomness, but it should not crash
}
fn test_lorem_vary() {
// lorem_vary is private, effectively testing internal logic
// base 10, min 5
// delta = 10 * 0.2 = 2
// range = [-2, 2]
// result = 10 + [-2, 2] -> [8, 12]
// We can't guarantee a specific value, but we can check bounds
mut rng := LorumRNG{
seed: 12345
}
for _ in 0 .. 100 {
val := lorem_vary(mut rng, 10, 5)
assert val >= 8
assert val <= 12
}
}
fn test_lorem_tokenize() {
text := 'Hello\nWorld Test'
tokens := lorem_tokenize(text)
assert tokens == ['Hello', 'World', 'Test']
}
+224
View File
@@ -0,0 +1,224 @@
module strings
@[inline]
fn min(a int, b int, c int) int {
mut m := a
if b < m {
m = b
}
if c < m {
m = c
}
return m
}
@[inline]
fn max2(a int, b int) int {
if a < b {
return b
}
return a
}
@[inline]
fn min2(a int, b int) int {
if a < b {
return a
}
return b
}
@[inline]
fn abs2(a int, b int) int {
if a < b {
return b - a
}
return a - b
}
// levenshtein_distance uses the Levenshtein Distance algorithm to calculate
// the distance between between two strings `a` and `b` (lower is closer).
@[direct_array_access]
pub fn levenshtein_distance(a string, b string) int {
if a.len == 0 {
return b.len
}
if b.len == 0 {
return a.len
}
if a == b {
return 0
}
mut row := []int{len: a.len + 1, init: index}
for i := 1; i < b.len + 1; i++ {
mut prev := i
for j := 1; j < a.len + 1; j++ {
mut current := row[j - 1] // match
if b[i - 1] != a[j - 1] {
// insertion, substitution, deletion
current = min(row[j - 1] + 1, prev + 1, row[j] + 1)
}
row[j - 1] = prev
prev = current
}
row[a.len] = prev
}
return row[a.len]
}
// levenshtein_distance_percentage uses the Levenshtein Distance algorithm to calculate how similar two strings are as a percentage (higher is closer).
pub fn levenshtein_distance_percentage(a string, b string) f32 {
d := levenshtein_distance(a, b)
l := if a.len >= b.len { a.len } else { b.len }
return (1.00 - f32(d) / f32(l)) * 100.00
}
// dice_coefficient implements the Sørensen–Dice coefficient.
// It finds the similarity between two strings, and returns a coefficient
// between 0.0 (not similar) and 1.0 (exact match).
pub fn dice_coefficient(s1 string, s2 string) f32 {
if s1.len == 0 || s2.len == 0 {
return 0.0
}
if s1 == s2 {
return 1.0
}
if s1.len < 2 || s2.len < 2 {
return 0.0
}
a := if s1.len > s2.len { s1 } else { s2 }
b := if a == s1 { s2 } else { s1 }
mut first_bigrams := map[string]int{}
for i in 0 .. a.len - 1 {
bigram := a[i..i + 2]
q := if bigram in first_bigrams { first_bigrams[bigram] + 1 } else { 1 }
first_bigrams[bigram] = q
}
mut intersection_size := 0
for i in 0 .. b.len - 1 {
bigram := b[i..i + 2]
count := if bigram in first_bigrams { first_bigrams[bigram] } else { 0 }
if count > 0 {
first_bigrams[bigram] = count - 1
intersection_size++
}
}
return (2.0 * f32(intersection_size)) / (f32(a.len) + f32(b.len) - 2)
}
// hamming_distance uses the Hamming Distance algorithm to calculate
// the distance between two strings `a` and `b` (lower is closer).
@[direct_array_access]
pub fn hamming_distance(a string, b string) int {
if a.len == 0 && b.len == 0 {
return 0
}
mut match_len := min2(a.len, b.len)
mut diff_count := abs2(a.len, b.len)
for i in 0 .. match_len {
if a[i] != b[i] {
diff_count++
}
}
return diff_count
}
// hamming_similarity uses the Hamming Distance algorithm to calculate the distance between two strings `a` and `b`.
// It returns a coefficient between 0.0 (not similar) and 1.0 (exact match).
pub fn hamming_similarity(a string, b string) f32 {
l := max2(a.len, b.len)
if l == 0 {
// Both are empty strings, should return 1.0
return 1.0
}
d := hamming_distance(a, b)
return 1.00 - f32(d) / f32(l)
}
// jaro_similarity uses the Jaro Distance algorithm to calculate
// the distance between two strings `a` and `b`.
// It returns a coefficient between 0.0 (not similar) and 1.0 (exact match).
@[direct_array_access]
pub fn jaro_similarity(a string, b string) f64 {
a_len := a.len
b_len := b.len
if a_len == 0 && b_len == 0 {
// Both are empty strings, should return 1.0
return 1.0
}
if a_len == 0 || b_len == 0 {
return 0
}
// Maximum distance upto which matching is allowed
match_distance := max2(max2(a_len, b_len) / 2 - 1, 0)
mut a_matches := []bool{len: a_len}
mut b_matches := []bool{len: b_len}
mut matches := 0
mut transpositions := 0.0
// Traverse through the first string
for i in 0 .. a_len {
start := max2(0, i - match_distance)
end := min2(b_len, i + match_distance + 1)
for k in start .. end {
// If there is a match
if b_matches[k] {
continue
}
if a[i] != b[k] {
continue
}
a_matches[i] = true
b_matches[k] = true
matches++
break
}
}
// If there is no match
if matches == 0 {
return 0
}
mut k := 0
// Count number of occurrences where two characters match but
// there is a third matched character in between the indices
for i in 0 .. a_len {
if !a_matches[i] {
continue
}
// Find the next matched character in second string
for !b_matches[k] {
k++
}
if a[i] != b[k] {
transpositions++
}
k++
}
transpositions /= 2
return (matches / f64(a_len) + matches / f64(b_len) + (matches - transpositions) / matches) / 3
}
// jaro_winkler_similarity uses the Jaro Winkler Distance algorithm to calculate
// the distance between two strings `a` and `b`.
// It returns a coefficient between 0.0 (not similar) and 1.0 (exact match).
// The scaling factor(`p=0.1`) in Jaro-Winkler gives higher weight to prefix
// similarities, making it especially effective for cases where slight misspellings
// or prefixes are common.
@[direct_array_access]
pub fn jaro_winkler_similarity(a string, b string) f64 {
// Maximum of 4 characters are allowed in prefix
mut lmax := min2(4, min2(a.len, b.len))
mut l := 0
for i in 0 .. lmax {
if a[i] == b[i] {
l++
}
}
js := jaro_similarity(a, b)
// select a multiplier (Winkler suggested p=0.1) for the relative importance of the prefix for the word similarity
p := 0.1
ws := js + f64(l) * p * (1 - js)
return ws
}
+14
View File
@@ -0,0 +1,14 @@
// vtest build: present_node?
import strings
fn test_levenshtein_distance() {
assert strings.levenshtein_distance('', '') == 0
assert strings.levenshtein_distance('one', 'one') == 0
assert strings.levenshtein_distance('', 'two') == 3
assert strings.levenshtein_distance('three', '') == 5
assert strings.levenshtein_distance('bananna', '') == 7
assert strings.levenshtein_distance('cats', 'hats') == 1
assert strings.levenshtein_distance('hugs', 'shrugs') == 2
assert strings.levenshtein_distance('broom', 'shroom') == 2
assert strings.levenshtein_distance('flomax', 'volmax') == 3
}
+66
View File
@@ -0,0 +1,66 @@
import strings
fn test_levenshtein_distance() {
assert strings.levenshtein_distance('', '') == 0
assert strings.levenshtein_distance('one', 'one') == 0
assert strings.levenshtein_distance('', 'two') == 3
assert strings.levenshtein_distance('three', '') == 5
assert strings.levenshtein_distance('bananna', '') == 7
assert strings.levenshtein_distance('cats', 'hats') == 1
assert strings.levenshtein_distance('hugs', 'shrugs') == 2
assert strings.levenshtein_distance('broom', 'shroom') == 2
assert strings.levenshtein_distance('flomax', 'volmax') == 3
assert strings.levenshtein_distance('ab', 'cd') == 2
}
fn test_hamming_distance() {
assert strings.hamming_distance('', '') == 0
assert strings.hamming_distance('one', 'one') == 0
assert strings.hamming_distance('', 'two') == 3
assert strings.hamming_distance('three', '') == 5
assert strings.hamming_distance('bananna', '') == 7
assert strings.hamming_distance('cats', 'hats') == 1
assert strings.hamming_distance('hugs', 'shrugs') == 6
assert strings.hamming_distance('broom', 'shroom') == 5
assert strings.hamming_distance('flomax', 'volmax') == 3
assert strings.hamming_distance('ab', 'cd') == 2
}
fn test_hamming_similarity() {
assert strings.hamming_similarity('', '') == 1.0
assert strings.hamming_similarity('one', 'one') == 1.0
assert strings.hamming_similarity('', 'two') == 0
assert strings.hamming_similarity('three', '') == 0
assert strings.hamming_similarity('bananna', '') == 0
assert strings.hamming_similarity('cats', 'hats') == 0.75
assert strings.hamming_similarity('hugs', 'shrugs') == 0
assert strings.hamming_similarity('broom', 'shroom') == 0.1666666865348816
assert strings.hamming_similarity('flomax', 'volmax') == 0.5
assert strings.hamming_similarity('ab', 'cd') == 0
}
fn test_jaro_similarity() {
assert strings.jaro_similarity('', '') == 1
assert strings.jaro_similarity('one', 'one') == 1
assert strings.jaro_similarity('', 'two') == 0
assert strings.jaro_similarity('three', '') == 0
assert strings.jaro_similarity('bananna', '') == 0
assert strings.jaro_similarity('MARTHA', 'MARHTA') == 0.9444444444444445
assert strings.jaro_similarity('DIXON', 'DICKSONX') == 0.7666666666666666
assert strings.jaro_similarity('JELLYFISH', 'SMELLYFISH') == 0.8962962962962964
assert strings.jaro_similarity('a', 'a') == 1
assert strings.jaro_similarity('a', 'b') == 0
}
fn test_jaro_winkler_similarity() {
assert strings.jaro_winkler_similarity('', '') == 1
assert strings.jaro_winkler_similarity('one', 'one') == 1
assert strings.jaro_winkler_similarity('', 'two') == 0
assert strings.jaro_winkler_similarity('three', '') == 0
assert strings.jaro_winkler_similarity('bananna', '') == 0
assert strings.jaro_winkler_similarity('accomodate', 'accommodate') == 0.9818181818181818
assert strings.jaro_winkler_similarity('accomodate', 'accompanist') == 0.8672727272727273
assert strings.jaro_winkler_similarity('untill', 'huntsville') == 0.8666666666666667
assert strings.jaro_winkler_similarity('wich', 'wichita') == 0.9142857142857143
assert strings.jaro_winkler_similarity('a', 'a') == 1
}
+40
View File
@@ -0,0 +1,40 @@
module strings
// strings.repeat - fill a string with `n` repetitions of the character `c`
@[direct_array_access]
pub fn repeat(c u8, n int) string {
if n <= 0 {
return ''
}
mut bytes := unsafe { malloc_noscan(n + 1) }
unsafe {
C.memset(bytes, c, n)
bytes[n] = 0
}
return unsafe { bytes.vstring_with_len(n) }
}
// strings.repeat_string - gives you `n` repetitions of the substring `s`
// Note: strings.repeat, that repeats a single byte, is between 2x
// and 24x faster than strings.repeat_string called for a 1 char string.
@[direct_array_access]
pub fn repeat_string(s string, n int) string {
if n <= 0 || s.len == 0 {
return ''
}
slen := s.len
blen := slen * n
mut bytes := unsafe { malloc_noscan(blen + 1) }
for bi in 0 .. n {
bislen := bi * slen
for si in 0 .. slen {
unsafe {
bytes[bislen + si] = s[si]
}
}
}
unsafe {
bytes[blen] = 0
}
return unsafe { bytes.vstring_with_len(blen) }
}
+16
View File
@@ -0,0 +1,16 @@
module strings
pub fn repeat(c u8, n int) string {
if n <= 0 {
return ''
}
arr := [c].repeat(n)
return arr.bytestr()
}
pub fn repeat_string(s string, n int) string {
res := ''
#res.str = s.str.repeat(n.valueOf())
return res
}
+147
View File
@@ -0,0 +1,147 @@
module strings
// import rand
// random returns a random string with `n` characters
/*
pub fn random(n int) string {
buf := vmalloc(n)
for i in 0..n {
buf[i] = rand.next()
}
return tos(buf)
}
*/
// find_between_pair_byte returns the string found between the pair of marks defined
// by `start` and `end`.
// As opposed to the `find_between`, `all_after*`, `all_before*` methods defined on the
// `string` type, this function can extract content between *nested* marks in `input`.
// If `start` and `end` marks are nested in `input`, the characters
// between the *outermost* mark pair is returned. It is expected that `start` and `end`
// marks are *balanced*, meaning that the amount of `start` marks equal the
// amount of `end` marks in the `input`. An empty string is returned otherwise.
// Using two identical marks as `start` and `end` results in undefined output behavior.
// find_between_pair_byte is the fastest in the find_between_pair_* family of functions.
// Example: assert strings.find_between_pair_u8('(V) (NOT V)',`(`,`)`) == 'V'
// Example: assert strings.find_between_pair_u8('s {X{Y}} s',`{`,`}`) == 'X{Y}'
pub fn find_between_pair_u8(input string, start u8, end u8) string {
mut marks := 0
mut start_index := -1
for i, b in input {
if b == start {
if start_index == -1 {
start_index = i + 1
}
marks++
continue
}
if start_index > 0 {
if b == end {
marks--
if marks == 0 {
return input[start_index..i]
}
}
}
}
return ''
}
// find_between_pair_rune returns the string found between the pair of marks defined by `start` and `end`.
// As opposed to the `find_between`, `all_after*`, `all_before*` methods defined on the
// `string` type, this function can extract content between *nested* marks in `input`.
// If `start` and `end` marks are nested in `input`, the characters
// between the *outermost* mark pair is returned. It is expected that `start` and `end`
// marks are *balanced*, meaning that the amount of `start` marks equal the
// amount of `end` marks in the `input`. An empty string is returned otherwise.
// Using two identical marks as `start` and `end` results in undefined output behavior.
// find_between_pair_rune is inbetween the fastest and slowest in the find_between_pair_* family of functions.
// Example: assert strings.find_between_pair_rune('(V) (NOT V)',`(`,`)`) == 'V'
// Example: assert strings.find_between_pair_rune('s {X{Y}} s',`{`,`}`) == 'X{Y}'
pub fn find_between_pair_rune(input string, start rune, end rune) string {
mut marks := 0
mut start_index := -1
runes := input.runes()
for i, r in runes {
if r == start {
if start_index == -1 {
start_index = i + 1
}
marks++
continue
}
if start_index > 0 {
if r == end {
marks--
if marks == 0 {
return runes[start_index..i].string()
}
}
}
}
return ''
}
// find_between_pair_string returns the string found between the pair of marks defined by `start` and `end`.
// As opposed to the `find_between`, `all_after*`, `all_before*` methods defined on the
// `string` type, this function can extract content between *nested* marks in `input`.
// If `start` and `end` marks are nested in `input`, the characters
// between the *outermost* mark pair is returned. It is expected that `start` and `end`
// marks are *balanced*, meaning that the amount of `start` marks equal the
// amount of `end` marks in the `input`. An empty string is returned otherwise.
// Using two identical marks as `start` and `end` results in undefined output behavior.
// find_between_pair_string is the slowest in the find_between_pair_* function family.
// Example: assert strings.find_between_pair_string('/*V*/ /*NOT V*/','/*','*/') == 'V'
// Example: assert strings.find_between_pair_string('s {{X{{Y}}}} s','{{','}}') == 'X{{Y}}'
pub fn find_between_pair_string(input string, start string, end string) string {
mut start_index := -1
mut marks := 0
start_runes := start.runes()
end_runes := end.runes()
runes := input.runes()
mut i := 0
for ; i < runes.len; i++ {
start_slice := runes#[i..i + start_runes.len]
if start_slice == start_runes {
i = i + start_runes.len - 1
if start_index < 0 {
start_index = i + 1
}
marks++
continue
}
if start_index > 0 {
end_slice := runes#[i..i + end_runes.len]
if end_slice == end_runes {
marks--
if marks == 0 {
return runes[start_index..i].string()
}
i = i + end_runes.len - 1
continue
}
}
}
return ''
}
// split_capital returns an array containing the contents of `s` split by capital letters.
// Example: assert strings.split_capital('XYZ') == ['X', 'Y', 'Z']
// Example: assert strings.split_capital('XYStar') == ['X', 'Y', 'Star']
pub fn split_capital(s string) []string {
mut res := []string{}
mut word_start := 0
for idx, c in s {
if c.is_capital() {
if word_start != idx {
res << s#[word_start..idx]
}
word_start = idx
continue
}
}
if word_start != s.len {
res << s#[word_start..]
}
return res
}
+15
View File
@@ -0,0 +1,15 @@
// vtest build: present_node?
import strings
fn test_repeat() {
assert strings.repeat(`x`, 10) == 'xxxxxxxxxx'
assert strings.repeat(`a`, 1) == 'a'
assert strings.repeat(`a`, 0) == ''
}
fn test_repeat_string() {
assert strings.repeat_string('abc', 3) == 'abcabcabc'
assert strings.repeat_string('abc', 1) == 'abc'
assert strings.repeat_string('abc', 0) == ''
assert strings.repeat_string('', 200) == ''
}
+107
View File
@@ -0,0 +1,107 @@
import strings
fn test_repeat() {
assert strings.repeat(`x`, 10) == 'xxxxxxxxxx'
assert strings.repeat(`a`, 1) == 'a'
assert strings.repeat(`a`, 0) == ''
}
fn test_repeat_string() {
assert strings.repeat_string('abc', 3) == 'abcabcabc'
assert strings.repeat_string('abc', 1) == 'abc'
assert strings.repeat_string('abc', 0) == ''
assert strings.repeat_string('', 200) == ''
}
const test_rune_and_byte = [
'xxx[ok1]xxx',
'xxx[[ok2]okok]',
'xxx[ok3[[[ok]okok]]]',
'yyy[ok4]',
'[]',
']',
'[',
'yyy[ok5][]zzz',
'yyy[xxx',
'xxx[xxx
xxx]',
]
const test_strings = [
'xxx/*ok1*/xxx',
'xxx/*/*ok2*/okok*/',
'xxx/*ok3/*/*/*ok*/okok*/*/*/',
'yyy/*ok4*/',
'/**/',
'*/',
'/*',
'yyy/*ok5*//**/zzz',
'yyy/*xxx',
'xxx/*xxx
xxx*/xxx',
]
const expected_rune_and_byte_outputs = [
'ok1',
'[ok2]okok',
'ok3[[[ok]okok]]',
'ok4',
'',
'',
'',
'ok5',
'',
'xxx
xxx',
]
const expected_string_outputs = [
'ok1',
'/*ok2*/okok',
'ok3/*/*/*ok*/okok*/*/',
'ok4',
'',
'',
'',
'ok5',
'',
'xxx
xxx',
]
fn test_find_between_pair_family() {
assert strings.find_between_pair_rune('xx♡ok❦yy', `♡`, `❦`) == 'ok'
assert strings.find_between_pair_u8('xx{ok}yy', `{`, `}`) == 'ok'
assert strings.find_between_pair_string('xx/*ok*/yy', '/*', '*/') == 'ok'
assert strings.find_between_pair_u8('xx{ok}yy', `{`, `}`) == 'ok'
assert strings.find_between_pair_string('xxxxokyyyy', 'xxx', 'yyy') == 'xok'
for i, tstr in test_rune_and_byte {
e1 := strings.find_between_pair_rune(tstr, `[`, `]`)
e2 := expected_rune_and_byte_outputs[i]
assert '${e1}' == '${e2}'
}
for i, tstr in test_rune_and_byte {
e1 := strings.find_between_pair_u8(tstr, `[`, `]`)
e2 := expected_rune_and_byte_outputs[i]
assert '${e1}' == '${e2}'
}
for i, tstr in test_strings {
e1 := strings.find_between_pair_string(tstr, '/*', '*/')
e2 := expected_string_outputs[i]
assert '${e1}' == '${e2}'
}
}
fn test_split_capital() {
assert strings.split_capital('') == []
assert strings.split_capital('abc') == ['abc']
assert strings.split_capital('X') == ['X']
assert strings.split_capital('XX') == ['X', 'X']
assert strings.split_capital('XYZ') == ['X', 'Y', 'Z']
assert strings.split_capital('JohnWilliams') == ['John', 'Williams']
assert strings.split_capital('JDStar') == ['J', 'D', 'Star']
assert strings.split_capital('cpDumpRotarySpring') == ['cp', 'Dump', 'Rotary', 'Spring']
}
@@ -0,0 +1,186 @@
module textscanner
// TextScanner simplifies writing small scanners/parsers.
// It helps by providing safe methods to scan texts character by character, peek for the next characters, go back, etc.
pub struct TextScanner {
pub:
input string
ilen int
pub mut:
pos int // current position; pos is *always* kept in [0,ilen]
}
// new returns a stack allocated instance of TextScanner.
pub fn new(input string) TextScanner {
return TextScanner{
input: input
ilen: input.len
}
}
// free frees all allocated resources.
@[unsafe]
pub fn (mut ss TextScanner) free() {
unsafe {
ss.input.free()
}
}
// remaining returns how many characters remain from current position.
@[inline]
pub fn (ss &TextScanner) remaining() int {
return ss.ilen - ss.pos
}
// next returns the next character code from the input text.
// next returns `-1` if it can't reach the next character.
// next advances the scanner position.
@[direct_array_access; inline]
pub fn (mut ss TextScanner) next() int {
if ss.pos < ss.ilen {
opos := ss.pos
ss.pos++
return ss.input[opos]
}
return -1
}
// skip skips one character ahead; `skip()` is slightly faster than `.next()`.
// `skip()` does not return a result.
@[inline]
pub fn (mut ss TextScanner) skip() {
if ss.pos < ss.ilen {
ss.pos++
}
}
// skip_n skips ahead `n` characters, stopping at the end of the input.
@[inline]
pub fn (mut ss TextScanner) skip_n(n int) {
ss.pos += n
if ss.pos > ss.ilen {
ss.pos = ss.ilen
}
}
// peek returns the *next* character code from the input text.
// peek returns `-1` if it can't peek the next character.
// unlike `next()`, `peek()` does not change the state of the scanner.
@[direct_array_access; inline]
pub fn (ss &TextScanner) peek() int {
if ss.pos < ss.ilen {
return ss.input[ss.pos]
}
return -1
}
// peek_u8 returns the *next* character code from the input text, as a byte/u8.
// unlike `next()`, `peek_u8()` does not change the state of the scanner.
// Note: peek_u8 returns `0`, if it can't peek the next character.
// Note: use `peek()`, instead of `peek_u8()`, if your input itself can
// legitimately contain bytes with value `0`.
@[direct_array_access; inline]
pub fn (ss &TextScanner) peek_u8() u8 {
if ss.pos < ss.ilen {
return ss.input[ss.pos]
}
return 0
}
// peek_n returns the character code from the input text at position + `n`.
// peek_n returns `-1` if it can't peek `n` characters ahead.
// ts.peek_n(0) == ts.current() .
// ts.peek_n(1) == ts.peek() .
@[direct_array_access; inline]
pub fn (ss &TextScanner) peek_n(n int) int {
if ss.pos + n < ss.ilen {
return ss.input[ss.pos + n]
}
return -1
}
// peek_n_u8 returns the character code from the input text, at position + `n`,
// as a byte/u8.
// Note: peek_n_u8 returns `0`, if it can't peek the next character.
// Note: use `peek_n()`, instead of `peek_n_u8()`, if your input itself can
// legitimately contain bytes with value `0`.
@[direct_array_access; inline]
pub fn (ss &TextScanner) peek_n_u8(n int) u8 {
if ss.pos + n < ss.ilen {
return ss.input[ss.pos + n]
}
return 0
}
// back goes back one character from the current scanner position.
@[inline]
pub fn (mut ss TextScanner) back() {
if ss.pos > 0 {
ss.pos--
}
}
// back_n goes back `n` characters from the current scanner position.
pub fn (mut ss TextScanner) back_n(n int) {
ss.pos -= n
if ss.pos < 0 {
ss.pos = 0
}
if ss.pos > ss.ilen {
ss.pos = ss.ilen
}
}
// peek_back returns the *previous* character code from the input text.
// peek_back returns `-1` if it can't peek the previous character.
// unlike `back()`, `peek_back()` does not change the state of the scanner.
@[direct_array_access; inline]
pub fn (ss &TextScanner) peek_back() int {
return ss.peek_back_n(1)
}
// peek_back_n returns the character code from the input text at position - `n`.
// peek_back_n returns `-1` if it can't peek `n` characters back.
// ts.peek_back_n(0) == ts.current()
// ts.peek_back_n(1) == ts.peek_back()
@[direct_array_access; inline]
pub fn (ss &TextScanner) peek_back_n(n int) int {
offset := n + 1
if ss.pos >= offset {
return ss.input[ss.pos - offset]
}
return -1
}
// current returns the current character code from the input text.
// current returns `-1` at the start of the input text.
// Note: after `c := ts.next()`, `ts.current()` will also return `c`.
@[direct_array_access; inline]
pub fn (mut ss TextScanner) current() int {
if ss.pos > 0 {
return ss.input[ss.pos - 1]
}
return -1
}
// reset resets the internal state of the scanner.
// After calling .reset(), .next() will start reading
// again from the start of the input text.
pub fn (mut ss TextScanner) reset() {
ss.pos = 0
}
// goto_end has the same effect as `for ts.next() != -1 {}`.
// i.e. after calling .goto_end(), the scanner will be at
// the end of the input text. Further .next() calls will
// return -1, unless you go back.
pub fn (mut ss TextScanner) goto_end() {
ss.pos = ss.ilen
}
// skip_whitespace advances the scanner pass any space characters in the input.
pub fn (mut ss TextScanner) skip_whitespace() {
for ss.ilen - ss.pos > 0 && ss.peek_u8().is_space() {
ss.next()
}
}
@@ -0,0 +1,186 @@
// vtest build: present_node?
import strings.textscanner
fn test_remaining() {
mut s := textscanner.new('abc')
assert s.remaining() == 3
s.next()
s.next()
assert s.remaining() == 1
s.next()
assert s.remaining() == 0
s.next()
s.next()
assert s.remaining() == 0
s.reset()
assert s.remaining() == 3
}
fn test_next() {
mut s := textscanner.new('abc')
assert s.next() == `a`
assert s.next() == `b`
assert s.next() == `c`
assert s.next() == -1
assert s.next() == -1
assert s.next() == -1
}
fn test_skip() {
mut s := textscanner.new('abc')
assert s.next() == `a`
s.skip()
assert s.next() == `c`
assert s.next() == -1
s.reset()
assert s.peek() == `a`
s.skip()
assert s.peek() == `b`
s.skip()
assert s.peek() == `c`
s.skip()
assert s.peek() == -1
}
fn test_skip_n() {
mut s := textscanner.new('abc')
s.skip_n(2)
assert s.next() == `c`
assert s.next() == -1
s.reset()
assert s.peek() == `a`
s.skip_n(2)
assert s.peek() == `c`
s.skip_n(2)
assert s.peek() == -1
s.reset()
assert s.peek() == `a`
s.skip_n(3)
assert s.peek() == -1
s.reset()
assert s.peek() == `a`
s.skip_n(4)
assert s.peek() == -1
}
fn test_peek() {
mut s := textscanner.new('abc')
assert s.peek() == `a`
assert s.peek() == `a`
assert s.peek() == `a`
assert s.next() == `a`
assert s.next() == `b`
assert s.next() == `c`
assert s.next() == -1
}
fn test_peek_n() {
mut s := textscanner.new('abc')
assert s.peek_n(0) == `a`
assert s.peek_n(1) == `b`
assert s.peek_n(2) == `c`
assert s.peek_n(3) == -1
assert s.peek_n(4) == -1
assert s.next() == `a`
assert s.next() == `b`
assert s.next() == `c`
assert s.next() == -1
}
fn test_back() {
mut s := textscanner.new('abc')
assert s.next() == `a`
s.back()
assert s.next() == `a`
assert s.next() == `b`
s.back()
assert s.next() == `b`
assert s.next() == `c`
assert s.next() == -1
}
fn test_back_n() {
mut s := textscanner.new('abc')
assert s.next() == `a`
s.back_n(10)
assert s.next() == `a`
assert s.next() == `b`
assert s.next() == `c`
s.back_n(2)
assert s.next() == `b`
}
fn test_peek_back() {
mut s := textscanner.new('abc')
assert s.next() == `a`
assert s.next() == `b`
// check that calling .peek_back() multiple times
// does not change the state:
assert s.peek_back() == `a`
assert s.peek_back() == `a`
assert s.peek_back() == `a`
// advance, then peek_back again:
assert s.next() == `c`
assert s.peek_back() == `b`
// peeking before the start:
s.reset()
assert s.peek_back() == -1
// peeking right at the end:
s.goto_end()
assert s.peek_back() == `b`
}
fn test_peek_back_n() {
mut s := textscanner.new('abc')
s.goto_end()
assert s.peek_back_n(0) == `c`
assert s.peek_back_n(1) == `b`
assert s.peek_back_n(2) == `a`
assert s.peek_back_n(3) == -1
assert s.peek_back_n(4) == -1
}
fn test_reset() {
mut s := textscanner.new('abc')
assert s.next() == `a`
s.next()
s.next()
assert s.next() == -1
s.reset()
assert s.next() == `a`
}
fn test_current() {
mut s := textscanner.new('abc')
assert s.current() == -1
assert s.next() == `a`
assert s.current() == `a`
assert s.current() == `a`
assert s.peek_back() == -1
assert s.next() == `b`
assert s.current() == `b`
assert s.current() == `b`
assert s.peek_back() == `a`
assert s.next() == `c`
assert s.current() == `c`
assert s.next() == -1
assert s.current() == `c`
assert s.next() == -1
assert s.current() == `c`
s.reset()
assert s.current() == -1
assert s.next() == `a`
assert s.current() == `a`
}
fn test_goto_end() {
mut s := textscanner.new('abc')
s.goto_end()
assert s.current() == `c`
}
@@ -0,0 +1,216 @@
import strings.textscanner
fn test_remaining() {
mut s := textscanner.new('abc')
assert s.remaining() == 3
s.next()
s.next()
assert s.remaining() == 1
s.next()
assert s.remaining() == 0
s.next()
s.next()
assert s.remaining() == 0
s.reset()
assert s.remaining() == 3
}
fn test_next() {
mut s := textscanner.new('abc')
assert s.next() == `a`
assert s.next() == `b`
assert s.next() == `c`
assert s.next() == -1
assert s.next() == -1
assert s.next() == -1
}
fn test_skip() {
mut s := textscanner.new('abc')
assert s.next() == `a`
s.skip()
assert s.next() == `c`
assert s.next() == -1
s.reset()
assert s.peek() == `a`
s.skip()
assert s.peek() == `b`
s.skip()
assert s.peek() == `c`
s.skip()
assert s.peek() == -1
}
fn test_skip_n() {
mut s := textscanner.new('abc')
s.skip_n(2)
assert s.next() == `c`
assert s.next() == -1
s.reset()
assert s.peek() == `a`
s.skip_n(2)
assert s.peek() == `c`
s.skip_n(2)
assert s.peek() == -1
s.reset()
assert s.peek() == `a`
s.skip_n(3)
assert s.peek() == -1
s.reset()
assert s.peek() == `a`
s.skip_n(4)
assert s.peek() == -1
}
fn test_peek() {
mut s := textscanner.new('abc')
assert s.peek() == `a`
assert s.peek() == `a`
assert s.peek() == `a`
assert s.next() == `a`
assert s.next() == `b`
assert s.next() == `c`
assert s.next() == -1
}
fn test_peek_n() {
mut s := textscanner.new('abc')
assert s.peek_n(0) == `a`
assert s.peek_n(1) == `b`
assert s.peek_n(2) == `c`
assert s.peek_n(3) == -1
assert s.peek_n(4) == -1
assert s.next() == `a`
assert s.next() == `b`
assert s.next() == `c`
assert s.next() == -1
}
fn test_back() {
mut s := textscanner.new('abc')
assert s.next() == `a`
s.back()
assert s.next() == `a`
assert s.next() == `b`
s.back()
assert s.next() == `b`
assert s.next() == `c`
assert s.next() == -1
}
fn test_back_n() {
mut s := textscanner.new('abc')
assert s.next() == `a`
s.back_n(10)
assert s.next() == `a`
assert s.next() == `b`
assert s.next() == `c`
s.back_n(2)
assert s.next() == `b`
}
fn test_peek_back() {
mut s := textscanner.new('abc')
assert s.next() == `a`
assert s.next() == `b`
// check that calling .peek_back() multiple times
// does not change the state:
assert s.peek_back() == `a`
assert s.peek_back() == `a`
assert s.peek_back() == `a`
// advance, then peek_back again:
assert s.next() == `c`
assert s.peek_back() == `b`
// peeking before the start:
s.reset()
assert s.peek_back() == -1
// peeking right at the end:
s.goto_end()
assert s.peek_back() == `b`
}
fn test_peek_back_n() {
mut s := textscanner.new('abc')
s.goto_end()
assert s.peek_back_n(0) == `c`
assert s.peek_back_n(1) == `b`
assert s.peek_back_n(2) == `a`
assert s.peek_back_n(3) == -1
assert s.peek_back_n(4) == -1
}
fn test_reset() {
mut s := textscanner.new('abc')
assert s.next() == `a`
s.next()
s.next()
assert s.next() == -1
s.reset()
assert s.next() == `a`
}
fn test_current() {
mut s := textscanner.new('abc')
assert s.current() == -1
assert s.next() == `a`
assert s.current() == `a`
assert s.current() == `a`
assert s.peek_back() == -1
assert s.next() == `b`
assert s.current() == `b`
assert s.current() == `b`
assert s.peek_back() == `a`
assert s.next() == `c`
assert s.current() == `c`
assert s.next() == -1
assert s.current() == `c`
assert s.next() == -1
assert s.current() == `c`
s.reset()
assert s.current() == -1
assert s.next() == `a`
assert s.current() == `a`
}
fn test_goto_end() {
mut s := textscanner.new('abc')
s.goto_end()
assert s.current() == `c`
}
fn test_skip_whitespace() {
mut s := textscanner.new('abc d \n xyz')
assert s.current() == -1
assert s.next() == `a`
assert s.next() == `b`
assert s.next() == `c`
s.skip_whitespace()
assert s.next() == `d`
s.skip_whitespace()
assert s.next() == `x`
assert s.next() == `y`
assert s.next() == `z`
}
fn test_peek_u8() {
mut s := textscanner.new('abc')
assert s.peek_u8() == `a`
assert !s.peek_u8().is_digit()
assert s.next() == `a`
assert s.peek_u8() == `b`
}
fn test_peek_n_u8() {
mut s := textscanner.new('abc')
assert s.peek_n_u8(0) == `a`
assert s.peek_n_u8(1) == `b`
assert s.peek_n_u8(2) == `c`
assert s.peek_n_u8(3) == 0
assert s.peek_n_u8(4) == 0
}