// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com) // All rights reserved. // Missing RTL functions — filling the gap between Five and Harbour. // Reference: /mnt/d/harbour-core/include/hbcompdf.h (HB_F_* list) package hbrtl import ( "five/hbrt" "math" "os" "strings" ) // --- String functions --- // At returns position of cSearch in cTarget (1-based, 0 if not found). func At(t *hbrt.Thread) { t.Frame(2, 0) defer t.EndProcFast() search := t.Local(1).AsString() target := t.Local(2).AsString() idx := strings.Index(target, search) if idx >= 0 { t.RetInt(int64(idx + 1)) } else { t.RetInt(0) } } // Left returns leftmost n characters. func Left(t *hbrt.Thread) { t.Frame(2, 0) defer t.EndProcFast() s := t.Local(1).AsString() n := int(t.Local(2).AsNumInt()) if n >= len(s) { t.PushString(s) } else if n <= 0 { t.PushString("") } else { t.PushString(s[:n]) } t.RetValue() } // Right returns rightmost n characters. func Right(t *hbrt.Thread) { t.Frame(2, 0) defer t.EndProcFast() s := t.Local(1).AsString() n := int(t.Local(2).AsNumInt()) if n >= len(s) { t.PushString(s) } else if n <= 0 { t.PushString("") } else { t.PushString(s[len(s)-n:]) } t.RetValue() } // Asc returns ASCII code of first character. func Asc(t *hbrt.Thread) { t.Frame(1, 0) defer t.EndProcFast() s := t.Local(1).AsString() if len(s) > 0 { t.RetInt(int64(s[0])) } else { t.RetInt(0) } } // Chr returns character from ASCII code. func Chr(t *hbrt.Thread) { t.Frame(1, 0) defer t.EndProcFast() n := int(t.Local(1).AsNumInt()) t.PushString(string([]byte{byte(n)})) t.RetValue() } // StrTran replaces occurrences in string. func StrTran(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProcFast() s := t.Local(1).AsString() search := t.Local(2).AsString() replace := "" if nParams >= 3 { replace = t.Local(3).AsString() } t.PushString(strings.ReplaceAll(s, search, replace)) t.RetValue() } // Stuff inserts/replaces characters in string. func Stuff(t *hbrt.Thread) { t.Frame(4, 0) defer t.EndProcFast() s := t.Local(1).AsString() start := int(t.Local(2).AsNumInt()) - 1 // 1-based nDel := int(t.Local(3).AsNumInt()) insert := t.Local(4).AsString() if start < 0 { start = 0 } if start > len(s) { start = len(s) } end := start + nDel if end > len(s) { end = len(s) } t.PushString(s[:start] + insert + s[end:]) t.RetValue() } // PadC pads string centered. func PadC(t *hbrt.Thread) { t.Frame(2, 0) defer t.EndProcFast() s := valueToDisplay(t.Local(1)) n := int(t.Local(2).AsNumInt()) if len(s) >= n { t.PushString(s[:n]) } else { leftPad := (n - len(s)) / 2 rightPad := n - len(s) - leftPad t.PushString(Spaces(leftPad) + s + Spaces(rightPad)) } t.RetValue() } // --- Math functions --- // Round rounds a number to specified decimal places. func Round(t *hbrt.Thread) { t.Frame(2, 0) defer t.EndProcFast() val := t.Local(1).AsNumDouble() dec := int(t.Local(2).AsNumInt()) mult := math.Pow(10, float64(dec)) result := math.Round(val*mult) / mult t.PushValue(hbrt.MakeDouble(result, 255, uint16(dec))) t.RetValue() } // Max returns larger of two values. func Max(t *hbrt.Thread) { t.Frame(2, 0) defer t.EndProcFast() a := t.Local(1) b := t.Local(2) if a.IsNumeric() && b.IsNumeric() { if a.AsNumDouble() >= b.AsNumDouble() { t.PushValue(a) } else { t.PushValue(b) } } else if a.IsDateTime() && b.IsDateTime() { if a.AsJulian() >= b.AsJulian() { t.PushValue(a) } else { t.PushValue(b) } } else { t.PushValue(a) } t.RetValue() } // Min returns smaller of two values. func Min(t *hbrt.Thread) { t.Frame(2, 0) defer t.EndProcFast() a := t.Local(1) b := t.Local(2) if a.IsNumeric() && b.IsNumeric() { if a.AsNumDouble() <= b.AsNumDouble() { t.PushValue(a) } else { t.PushValue(b) } } else if a.IsDateTime() && b.IsDateTime() { if a.AsJulian() <= b.AsJulian() { t.PushValue(a) } else { t.PushValue(b) } } else { t.PushValue(a) } t.RetValue() } // Sqrt returns square root. func Sqrt(t *hbrt.Thread) { t.Frame(1, 0) defer t.EndProcFast() t.PushValue(hbrt.MakeDoubleAuto(math.Sqrt(t.Local(1).AsNumDouble()))) t.RetValue() } // Log returns natural logarithm. func Log(t *hbrt.Thread) { t.Frame(1, 0) defer t.EndProcFast() t.PushValue(hbrt.MakeDoubleAuto(math.Log(t.Local(1).AsNumDouble()))) t.RetValue() } // Exp returns e^x. func Exp(t *hbrt.Thread) { t.Frame(1, 0) defer t.EndProcFast() t.PushValue(hbrt.MakeDoubleAuto(math.Exp(t.Local(1).AsNumDouble()))) t.RetValue() } // Mod returns modulus (same as %). func Mod(t *hbrt.Thread) { t.Frame(2, 0) defer t.EndProcFast() a := t.Local(1).AsNumDouble() b := t.Local(2).AsNumDouble() if b == 0 { t.PushValue(hbrt.MakeDoubleAuto(0)) } else { t.PushValue(hbrt.MakeDoubleAuto(math.Mod(a, b))) } t.RetValue() } // CToD, CDoW, CMonth moved to datetime.go // --- Type / Misc --- // Type returns type of an expression (as string). func TypeFunc(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" default: c = "U" } t.PushString(c) t.RetValue() } // PCount returns number of parameters passed to the calling PRG function. // Harbour: hb_pcount() — returns the CALLER's param count, not PCount's own. func PCount(t *hbrt.Thread) { t.Frame(0, 0) defer t.EndProcFast() t.RetInt(int64(t.CallerParamCount())) } // PValue returns the nth parameter of the calling PRG function. // Harbour: PValue(nIndex[, xDefault]) → xValue // Returns xDefault when n is out of range, or NIL if no default was given. func PValue(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() n := int(t.Local(1).AsNumInt()) if n >= 1 && n <= t.CallerParamCount() { t.PushValue(t.CallerLocal(n)) t.RetValue() return } if nParams >= 2 { t.PushValue(t.Local(2)) } else { t.PushNil() } t.RetValue() } // Break moved to error.go — full implementation with BreakValue type. // Array creates array of given size. func ArrayFunc(t *hbrt.Thread) { t.Frame(1, 0) defer t.EndProcFast() n := int(t.Local(1).AsNumInt()) t.PushValue(hbrt.MakeArray(n)) t.RetValue() } // FCount returns number of fields in current workarea. func FCount(t *hbrt.Thread) { t.Frame(0, 0) defer t.EndProcFast() wam := getWA(t) if wam != nil { if area := wam.Current(); area != nil { t.RetInt(int64(area.FieldCount())) return } } t.RetInt(0) } // FieldName returns field name by position (1-based). func FieldName(t *hbrt.Thread) { t.Frame(1, 0) defer t.EndProcFast() nField := int(t.Local(1).AsNumInt()) wam := getWA(t) if wam != nil { if area := wam.Current(); area != nil { if nField >= 1 && nField <= area.FieldCount() { fi := area.GetFieldInfo(nField - 1) // 1-based to 0-based t.RetString(fi.Name) return } } } t.RetString("") } // Select([cAlias|nArea]) returns workarea number. // Select() → current area number // Select("ALIAS") → area number for alias (0 if not found) // Select(nArea) → nArea if that area is in use, 0 otherwise func SelectFunc(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProcFast() wam := getWA(t) if wam == nil { t.RetInt(0) return } if nParams == 0 { t.RetInt(int64(wam.CurrentNum())) return } v := t.Local(1) if v.IsString() { t.RetInt(int64(wam.FindByAlias(v.AsString()))) } else if v.IsNumeric() { n := uint16(v.AsNumInt()) if wam.AreaAt(n) != nil { t.RetInt(int64(n)) } else { t.RetInt(0) } } else { t.RetInt(0) } } // File(cPath) → lExists. Harbour's File() also honours SET PATH // and wildcards, but callers in Five use it as "does this exact // path exist". A plain os.Stat covers that without pulling the // whole Harbour SET PATH search order in — matches how HbFileExists // (hb_FileExists) already behaves elsewhere. func FileFunc(t *hbrt.Thread) { t.Frame(1, 0) defer t.EndProcFast() path := t.Local(1).AsString() if path == "" { t.RetBool(false) return } _, err := os.Stat(path) t.RetBool(err == nil) } // Inkey waits for keypress and returns key code. // Harbour: Inkey(nSeconds) — 0 = wait forever // Uses shared raw terminal from rawtty.go func Inkey(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProcFast() // Check keyboard buffer first if k := PopKeyBuffer(); k >= 0 { SetLastKey(k) t.RetInt(int64(k)) return } // Check timeout param: Inkey(0) = wait forever, Inkey(n) = wait n seconds nWait := float64(0) if nParams >= 1 && !t.Local(1).IsNil() { nWait = t.Local(1).AsNumDouble() } _ = nWait // TODO: implement timeout for non-zero key := ReadKey() // from rawtty.go (auto-inits raw mode) var result int switch key { case 'A': result = 5 // K_UP case 'B': result = 24 // K_DOWN case 'C': result = 4 // K_RIGHT case 'D': result = 19 // K_LEFT case '5': result = 18 // K_PGUP case '6': result = 3 // K_PGDN case 'H': result = 1 // K_HOME case 'F': result = 6 // K_END case kESC: result = 27 default: result = key } SetLastKey(result) t.RetInt(int64(result)) } // Transform formats a value with picture string. func Transform(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProcFast() val := t.Local(1) pic := "" if nParams >= 2 && !t.Local(2).IsNil() { pic = t.Local(2).AsString() } t.RetString(transformHbValue(val, pic)) } // hb_StrReplace replaces multiple substrings. func HbStrReplace(t *hbrt.Thread) { t.Frame(3, 0) defer t.EndProcFast() s := t.Local(1).AsString() search := t.Local(2) // array of search strings replace := t.Local(3) // array of replace strings if search.IsArray() && replace.IsArray() { sa := search.AsArray() ra := replace.AsArray() for i := 0; i < len(sa.Items) && i < len(ra.Items); i++ { s = strings.ReplaceAll(s, sa.Items[i].AsString(), ra.Items[i].AsString()) } } t.PushString(s) t.RetValue() } // hb_NToS converts number to string without leading spaces. func HbNToS(t *hbrt.Thread) { t.Frame(1, 0) defer t.EndProcFast() v := t.Local(1) if v.IsNumInt() { t.PushString(fmt_int64(v.AsNumInt())) } else { t.PushString(strings.TrimSpace(valueToDisplay(v))) } t.RetValue() } func fmt_int64(n int64) string { if n == 0 { return "0" } neg := n < 0 if neg { n = -n } var buf [20]byte // stack allocation, no heap i := len(buf) for n > 0 { i-- buf[i] = byte('0' + n%10) n /= 10 } if neg { i-- buf[i] = '-' } return string(buf[i:]) }