// 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) { t.Frame(2, 0) defer t.EndProc() s := valueToDisplay(t.Local(1)) n := int(t.Local(2).AsNumInt()) if len(s) >= n { t.PushString(s[:n]) } else { t.PushString(s + strings.Repeat(" ", n-len(s))) } t.RetValue() } // PadL pads a string on the left to a specified length. func PadL(t *hbrt.Thread) { t.Frame(2, 0) defer t.EndProc() s := valueToDisplay(t.Local(1)) n := int(t.Local(2).AsNumInt()) if len(s) >= n { t.PushString(s[len(s)-n:]) } else { t.PushString(strings.Repeat(" ", 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() }