13 more RTL functions inlined — no Frame/EndProc, no VM dispatch:
- Str(n,w,d) → fmt.Sprintf("%*.*f", w, d, n)
- PadR(s,n) → s + hbrtl.Spaces(n-len(s))
- PadL(s,n[,fill]) → Spaces(pad) + s or Repeat(fill, pad) + s
- SubStr(s,p,l) → s[p:p+l] with bounds check
- Left(s,n) → s[:n], Right(s,n) → s[len-n:]
- At(search,target) → strings.Index + 1
- IIF(cond,a,b) → if/else without function call
Also: Spaces() exported for generated code access.
50K SEEK random: 62ms (Harbour 67ms — Five FASTER!)
82/82 stress PASS. 14 packages ALL PASS.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
446 lines
8.4 KiB
Go
446 lines
8.4 KiB
Go
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
|
// All rights reserved.
|
|
|
|
// String functions for the Five runtime library.
|
|
// Implements Harbour-compatible string functions.
|
|
package hbrtl
|
|
|
|
import (
|
|
"five/hbrt"
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
)
|
|
|
|
// spacesCache: pre-built space strings for common pad sizes.
|
|
// Avoids strings.Repeat(" ", n) allocation in hot paths.
|
|
var spacesCache [257]string
|
|
|
|
func init() {
|
|
for i := range spacesCache {
|
|
spacesCache[i] = strings.Repeat(" ", i)
|
|
}
|
|
}
|
|
|
|
// spaces returns a string of n spaces, using cache for n <= 256.
|
|
func Spaces(n int) string {
|
|
if n <= 0 {
|
|
return ""
|
|
}
|
|
if n < len(spacesCache) {
|
|
return spacesCache[n]
|
|
}
|
|
return strings.Repeat(" ", n)
|
|
}
|
|
|
|
// Str converts a numeric value to a string.
|
|
// Harbour: Str(nValue [, nWidth [, nDec]]) → cString
|
|
func Str(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
|
|
v := t.Local(1)
|
|
if !v.IsNumeric() {
|
|
t.PushString("")
|
|
t.RetValue()
|
|
return
|
|
}
|
|
|
|
d := v.AsNumDouble()
|
|
|
|
// Width and decimals: use caller's args if provided, else Value metadata
|
|
width := int(v.Length())
|
|
dec := int(v.Decimal())
|
|
|
|
if nParams >= 2 && !t.Local(2).IsNil() {
|
|
width = t.Local(2).AsInt()
|
|
}
|
|
if nParams >= 3 && !t.Local(3).IsNil() {
|
|
dec = t.Local(3).AsInt()
|
|
}
|
|
|
|
if width == 0 || width == 255 {
|
|
width = 10 // default width
|
|
}
|
|
if dec == 255 {
|
|
dec = 0
|
|
}
|
|
|
|
s := fmt.Sprintf("%*.*f", width, dec, d)
|
|
// Harbour pads with spaces if shorter
|
|
if len(s) < width {
|
|
s = Spaces(width-len(s)) + s
|
|
}
|
|
// Harbour returns asterisks if wider than width
|
|
if len(s) > width && width > 0 {
|
|
s = strings.Repeat("*", width)
|
|
}
|
|
|
|
t.PushString(s)
|
|
t.RetValue()
|
|
}
|
|
|
|
// Val converts a string to a numeric value.
|
|
// Harbour: Val(cString) → nValue
|
|
func Val(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProcFast()
|
|
|
|
v := t.Local(1)
|
|
if !v.IsString() {
|
|
t.RetInt(0)
|
|
return
|
|
}
|
|
|
|
s := strings.TrimSpace(v.AsString())
|
|
if s == "" {
|
|
t.RetInt(0)
|
|
return
|
|
}
|
|
|
|
// Try integer first
|
|
var n int64
|
|
if _, err := fmt.Sscanf(s, "%d", &n); err == nil {
|
|
// Check if there's a decimal point
|
|
if !strings.Contains(s, ".") {
|
|
t.RetInt(n)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Try float
|
|
var f float64
|
|
if _, err := fmt.Sscanf(s, "%f", &f); err == nil {
|
|
// Count decimal places
|
|
dec := 0
|
|
if idx := strings.Index(s, "."); idx >= 0 {
|
|
dec = len(s) - idx - 1
|
|
}
|
|
t.PushValue(hbrt.MakeDouble(f, uint16(len(s)), uint16(dec)))
|
|
t.RetValue()
|
|
return
|
|
}
|
|
|
|
t.RetInt(0)
|
|
}
|
|
|
|
// Len returns the length of a string or array.
|
|
// Harbour: Len(xValue) → nLen
|
|
func Len(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProcFast()
|
|
|
|
v := t.Local(1)
|
|
switch {
|
|
case v.IsString():
|
|
t.RetInt(int64(v.StringLen()))
|
|
case v.IsArray():
|
|
t.RetInt(int64(len(v.AsArray().Items)))
|
|
case v.IsHash():
|
|
t.RetInt(int64(len(v.AsHash().Keys)))
|
|
default:
|
|
t.RetInt(0)
|
|
}
|
|
}
|
|
|
|
// SubStr extracts a substring.
|
|
// Harbour: SubStr(cString, nStart [, nLen]) → cString
|
|
func SubStr(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
|
|
v := t.Local(1)
|
|
if !v.IsString() {
|
|
t.PushString("")
|
|
t.RetValue()
|
|
return
|
|
}
|
|
|
|
s := v.AsString()
|
|
start := int(t.Local(2).AsNumInt())
|
|
|
|
// Harbour: 1-based index, negative = from end
|
|
if start < 0 {
|
|
start = len(s) + start + 1
|
|
}
|
|
if start < 1 {
|
|
start = 1
|
|
}
|
|
start-- // convert to 0-based
|
|
|
|
if start >= len(s) {
|
|
t.PushString("")
|
|
t.RetValue()
|
|
return
|
|
}
|
|
|
|
result := s[start:]
|
|
|
|
// Optional 3rd param: length
|
|
if nParams >= 3 && !t.Local(3).IsNil() {
|
|
nLen := int(t.Local(3).AsNumInt())
|
|
if nLen < 0 {
|
|
nLen = 0
|
|
}
|
|
if nLen < len(result) {
|
|
result = result[:nLen]
|
|
}
|
|
}
|
|
|
|
t.PushString(result)
|
|
t.RetValue()
|
|
}
|
|
|
|
// Upper converts string to uppercase.
|
|
// Harbour: Upper(cString) → cString
|
|
func Upper(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProcFast()
|
|
v := t.Local(1)
|
|
if v.IsString() {
|
|
t.PushString(strings.ToUpper(v.AsString()))
|
|
} else {
|
|
t.PushString("")
|
|
}
|
|
t.RetValue()
|
|
}
|
|
|
|
// Lower converts string to lowercase.
|
|
func Lower(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProcFast()
|
|
v := t.Local(1)
|
|
if v.IsString() {
|
|
t.PushString(strings.ToLower(v.AsString()))
|
|
} else {
|
|
t.PushString("")
|
|
}
|
|
t.RetValue()
|
|
}
|
|
|
|
// AllTrim removes leading and trailing spaces.
|
|
func AllTrim(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProcFast()
|
|
v := t.Local(1)
|
|
if v.IsString() {
|
|
t.PushString(strings.TrimSpace(v.AsString()))
|
|
} else {
|
|
t.PushString("")
|
|
}
|
|
t.RetValue()
|
|
}
|
|
|
|
// LTrim trims leading spaces only. Harbour: LTRIM(cString) → cString
|
|
func LTrim(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProcFast()
|
|
v := t.Local(1)
|
|
if v.IsString() {
|
|
t.PushString(strings.TrimLeft(v.AsString(), " "))
|
|
} else {
|
|
t.PushString("")
|
|
}
|
|
t.RetValue()
|
|
}
|
|
|
|
// RTrim trims trailing spaces only. Harbour: RTRIM(cString) / TRIM(cString)
|
|
func RTrim(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProcFast()
|
|
v := t.Local(1)
|
|
if v.IsString() {
|
|
t.PushString(strings.TrimRight(v.AsString(), " "))
|
|
} else {
|
|
t.PushString("")
|
|
}
|
|
t.RetValue()
|
|
}
|
|
|
|
// Space returns a string of n spaces.
|
|
// Harbour: Space(nCount) → cString
|
|
func Space(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProcFast()
|
|
n := t.Local(1).AsNumInt()
|
|
if n < 0 {
|
|
n = 0
|
|
}
|
|
t.PushString(Spaces(int(n)))
|
|
t.RetValue()
|
|
}
|
|
|
|
// PadR pads a string on the right to a specified length.
|
|
func PadR(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
s := valueToDisplay(t.Local(1))
|
|
n := int(t.Local(2).AsNumInt())
|
|
fill := " "
|
|
if nParams >= 3 && t.Local(3).IsString() {
|
|
f := t.Local(3).AsString()
|
|
if len(f) > 0 {
|
|
fill = f[:1]
|
|
}
|
|
}
|
|
if len(s) >= n {
|
|
t.PushString(s[:n])
|
|
} else {
|
|
pad := n - len(s)
|
|
if fill == " " {
|
|
t.PushString(s + Spaces(pad))
|
|
} else {
|
|
t.PushString(s + strings.Repeat(fill, pad))
|
|
}
|
|
}
|
|
t.RetValue()
|
|
}
|
|
|
|
// PadL pads a string on the left to a specified length.
|
|
func PadL(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
s := valueToDisplay(t.Local(1))
|
|
n := int(t.Local(2).AsNumInt())
|
|
fill := " "
|
|
if nParams >= 3 && t.Local(3).IsString() {
|
|
f := t.Local(3).AsString()
|
|
if len(f) > 0 {
|
|
fill = f[:1]
|
|
}
|
|
}
|
|
if len(s) >= n {
|
|
t.PushString(s[len(s)-n:])
|
|
} else {
|
|
pad := n - len(s)
|
|
if fill == " " {
|
|
t.PushString(Spaces(pad) + s)
|
|
} else {
|
|
t.PushString(strings.Repeat(fill, pad) + s)
|
|
}
|
|
}
|
|
t.RetValue()
|
|
}
|
|
|
|
// Replicate repeats a string n times.
|
|
func Replicate(t *hbrt.Thread) {
|
|
t.Frame(2, 0)
|
|
defer t.EndProcFast()
|
|
s := t.Local(1).AsString()
|
|
n := int(t.Local(2).AsNumInt())
|
|
if n < 0 {
|
|
n = 0
|
|
}
|
|
t.PushString(strings.Repeat(s, n))
|
|
t.RetValue()
|
|
}
|
|
|
|
// --- Utility functions ---
|
|
|
|
// ValType returns a single character indicating the value type.
|
|
// Harbour: ValType(xValue) → cType
|
|
func ValType(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProcFast()
|
|
v := t.Local(1)
|
|
var c string
|
|
switch {
|
|
case v.IsNil():
|
|
c = "U"
|
|
case v.IsLogical():
|
|
c = "L"
|
|
case v.IsNumeric():
|
|
c = "N"
|
|
case v.IsString():
|
|
c = "C"
|
|
case v.IsDate(), v.IsTimestamp():
|
|
c = "D"
|
|
case v.IsArray():
|
|
c = "A"
|
|
case v.IsHash():
|
|
c = "H"
|
|
case v.IsBlock():
|
|
c = "B"
|
|
case v.IsObject():
|
|
c = "O"
|
|
case v.IsPointer():
|
|
c = "P"
|
|
case v.IsSymbol():
|
|
c = "S"
|
|
default:
|
|
c = "U"
|
|
}
|
|
t.PushString(c)
|
|
t.RetValue()
|
|
}
|
|
|
|
// Empty checks if a value is "empty".
|
|
// Harbour: Empty(xValue) → lEmpty
|
|
func Empty(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProcFast()
|
|
v := t.Local(1)
|
|
var empty bool
|
|
switch {
|
|
case v.IsNil():
|
|
empty = true
|
|
case v.IsLogical():
|
|
empty = !v.AsBool()
|
|
case v.IsNumeric():
|
|
empty = v.AsNumDouble() == 0
|
|
case v.IsString():
|
|
empty = strings.TrimSpace(v.AsString()) == ""
|
|
case v.IsDate():
|
|
empty = v.AsJulian() == 0
|
|
case v.IsArray():
|
|
empty = len(v.AsArray().Items) == 0
|
|
case v.IsHash():
|
|
empty = len(v.AsHash().Keys) == 0
|
|
case v.IsBlock():
|
|
empty = false
|
|
default:
|
|
empty = true
|
|
}
|
|
t.PushValue(hbrt.MakeBool(empty))
|
|
t.RetValue()
|
|
}
|
|
|
|
// Abs returns the absolute value.
|
|
func Abs(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProcFast()
|
|
v := t.Local(1)
|
|
if v.IsNumInt() {
|
|
n := v.AsNumInt()
|
|
if n < 0 {
|
|
n = -n
|
|
}
|
|
t.PushValue(hbrt.MakeNumInt(n))
|
|
} else if v.IsDouble() {
|
|
t.PushValue(hbrt.MakeDouble(math.Abs(v.AsDouble()), v.Length(), v.Decimal()))
|
|
} else {
|
|
t.RetInt(0)
|
|
return
|
|
}
|
|
t.RetValue()
|
|
}
|
|
|
|
// Int returns the integer part of a numeric value.
|
|
func Int(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProcFast()
|
|
v := t.Local(1)
|
|
if v.IsNumInt() {
|
|
t.PushValue(v)
|
|
} else if v.IsDouble() {
|
|
t.PushValue(hbrt.MakeLong(int64(v.AsDouble())))
|
|
} else {
|
|
t.PushValue(hbrt.MakeInt(0))
|
|
}
|
|
t.RetValue()
|
|
}
|