- skipFilter: skip deleted records in GoTop/GoBottom/Skip when SET DELETED ON - hbrdd.IsSetDeleted callback: avoids circular import hbrdd→hbrtl - Parser: capture ON/OFF for boolean SET commands (DELETED, EXACT, SOFTSEEK, etc.) - Parser: capture TO expr for SET DATE/DECIMALS/EPOCH - Gengo: emit proper t.Do() calls for 11 SET toggles + 3 value SETs - stmtSet: was stub (skipToEOL), now calls parseSet() - RTL: register 11 SET toggle functions (SETDELETED, SETEXACT, etc.) - RTL: DBLOCATE/DBCONTINUE for sequential search - RTL: DBSETFILTER/DBCLEARFILTER/DBFILTER - PadL/PadR: support 3rd param fill character - Area interface: added SetFound, SetLocate, LocateBlock, filter methods - MemRDD: implements new Area interface methods - Comprehensive PRG test: test_search.prg (7 test suites all pass) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
415 lines
7.7 KiB
Go
415 lines
7.7 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"
|
|
)
|
|
|
|
// 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.EndProc()
|
|
|
|
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 = strings.Repeat(" ", 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.EndProc()
|
|
|
|
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.EndProc()
|
|
|
|
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.EndProc()
|
|
|
|
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.EndProc()
|
|
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.EndProc()
|
|
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.EndProc()
|
|
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.EndProc()
|
|
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.EndProc()
|
|
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.EndProc()
|
|
n := t.Local(1).AsNumInt()
|
|
if n < 0 {
|
|
n = 0
|
|
}
|
|
t.PushString(strings.Repeat(" ", 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.EndProc()
|
|
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 {
|
|
t.PushString(s + strings.Repeat(fill, n-len(s)))
|
|
}
|
|
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.EndProc()
|
|
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 {
|
|
t.PushString(strings.Repeat(fill, n-len(s)) + s)
|
|
}
|
|
t.RetValue()
|
|
}
|
|
|
|
// Replicate repeats a string n times.
|
|
func Replicate(t *hbrt.Thread) {
|
|
t.Frame(2, 0)
|
|
defer t.EndProc()
|
|
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.EndProc()
|
|
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.EndProc()
|
|
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.EndProc()
|
|
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.EndProc()
|
|
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()
|
|
}
|