- Compiler: PP → Lexer → Parser → Analyzer → Gengo pipeline - Parser: 232/236 (98%) Harbour compatibility, registry-based dispatch - RTL: 351 Harbour-compatible functions - RDD: DBF/NTX/CDX engines with Rushmore bitmap optimization - Go Interop: IMPORT + pkg.Func() + obj:Method() with FastPath (15M calls/sec) - HB_FUNC API: Full Harbour C API compatible Go bridge - Concurrency: SPAWN/LAUNCH/GOROUTINE, <-, WATCH, PARALLEL FOR, ASYNC/AWAIT - Extensions: Multi-return, DEFER, Slice, f-string, Nil-safe ?:, CONST - Macro Compiler: Runtime AST parsing and evaluation - Debugger: TUI debugger with source display, breakpoints, stepping - FRB: Native + Pcode dual mode runtime binary - Tests: 13 packages ALL PASS Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
225 lines
4.6 KiB
Go
225 lines
4.6 KiB
Go
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
|
// All rights reserved.
|
|
|
|
// Transform function — PICTURE format processing for @ SAY and GET.
|
|
// Harbour: src/rtl/transfrm.c
|
|
//
|
|
// Picture string format:
|
|
// @B Left-align numeric
|
|
// @C Append " CR" for positive numbers
|
|
// @D Date format
|
|
// @E European format (swap . and ,)
|
|
// @( Parentheses for negative
|
|
// @R Non-template chars in mask are inserted (not stored)
|
|
// @! Convert to uppercase
|
|
// @Z Display blank for zero
|
|
// @S<n> Scroll width
|
|
// @L Pad with leading zeros
|
|
//
|
|
// Mask chars: 9 # ! A N X Y L . ,
|
|
|
|
package hbrtl
|
|
|
|
import (
|
|
"five/hbrt"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// transformHbValue applies a PICTURE format to an hbrt.Value.
|
|
func transformHbValue(v hbrt.Value, pic string) string {
|
|
if pic == "" {
|
|
return valueToDisplay(v)
|
|
}
|
|
|
|
funcFlags, mask := parsePicture(pic)
|
|
|
|
if v.IsString() {
|
|
return transformString(v.AsString(), funcFlags, mask)
|
|
}
|
|
if v.IsNumeric() {
|
|
if v.IsNumInt() {
|
|
return transformNumeric(float64(v.AsNumInt()), funcFlags, mask)
|
|
}
|
|
return transformNumeric(v.AsNumDouble(), funcFlags, mask)
|
|
}
|
|
if v.IsLogical() {
|
|
if v.AsBool() {
|
|
return "T"
|
|
}
|
|
return "F"
|
|
}
|
|
return valueToDisplay(v)
|
|
}
|
|
|
|
// parsePicture splits "@flags mask" into flags and mask.
|
|
func parsePicture(pic string) (string, string) {
|
|
if len(pic) == 0 || pic[0] != '@' {
|
|
return "", pic
|
|
}
|
|
// Find space separating function from mask
|
|
idx := strings.IndexByte(pic, ' ')
|
|
if idx < 0 {
|
|
return strings.ToUpper(pic[1:]), ""
|
|
}
|
|
return strings.ToUpper(pic[1:idx]), pic[idx+1:]
|
|
}
|
|
|
|
func transformString(s string, flags string, mask string) string {
|
|
upper := strings.ContainsRune(flags, '!')
|
|
|
|
if mask == "" {
|
|
if upper {
|
|
return strings.ToUpper(s)
|
|
}
|
|
return s
|
|
}
|
|
|
|
// Apply mask
|
|
var out strings.Builder
|
|
si := 0
|
|
for _, m := range mask {
|
|
if si >= len(s) {
|
|
break
|
|
}
|
|
ch := s[si]
|
|
switch m {
|
|
case '!':
|
|
if ch >= 'a' && ch <= 'z' {
|
|
ch = ch - 32
|
|
}
|
|
out.WriteByte(ch)
|
|
si++
|
|
case 'A', 'N', 'X', '9', '#':
|
|
if upper && ch >= 'a' && ch <= 'z' {
|
|
ch = ch - 32
|
|
}
|
|
out.WriteByte(ch)
|
|
si++
|
|
default:
|
|
// Non-template char: if @R, insert literal; else consume
|
|
if strings.ContainsRune(flags, 'R') {
|
|
out.WriteRune(m)
|
|
} else {
|
|
out.WriteByte(ch)
|
|
si++
|
|
}
|
|
}
|
|
}
|
|
return out.String()
|
|
}
|
|
|
|
func transformNumeric(n float64, flags string, mask string) string {
|
|
blankZero := strings.ContainsRune(flags, 'Z')
|
|
leftAlign := strings.ContainsRune(flags, 'B')
|
|
credit := strings.ContainsRune(flags, 'C')
|
|
debit := strings.ContainsRune(flags, 'X')
|
|
paren := strings.ContainsRune(flags, '(')
|
|
padZero := strings.ContainsRune(flags, 'L')
|
|
|
|
if blankZero && n == 0 {
|
|
if mask != "" {
|
|
return strings.Repeat(" ", len(mask))
|
|
}
|
|
return " "
|
|
}
|
|
|
|
if mask == "" {
|
|
s := fmt.Sprintf("%g", n)
|
|
if leftAlign {
|
|
s = strings.TrimLeft(s, " ")
|
|
}
|
|
return s
|
|
}
|
|
|
|
// Count digit positions and decimal
|
|
nDec := 0
|
|
nInt := 0
|
|
decIdx := strings.IndexByte(mask, '.')
|
|
for _, m := range mask {
|
|
if m == '9' || m == '#' {
|
|
if decIdx >= 0 && nInt+nDec >= decIdx {
|
|
nDec++
|
|
} else {
|
|
nInt++
|
|
}
|
|
}
|
|
}
|
|
|
|
// Format the number
|
|
var formatted string
|
|
if nDec > 0 {
|
|
formatted = fmt.Sprintf("%*.*f", len(mask), nDec, n)
|
|
} else {
|
|
formatted = fmt.Sprintf("%*.0f", len(mask), n)
|
|
}
|
|
|
|
// Pad with zeros if @L
|
|
if padZero {
|
|
neg := n < 0
|
|
stripped := strings.TrimLeft(formatted, " ")
|
|
if neg {
|
|
stripped = strings.TrimLeft(stripped, "-")
|
|
formatted = "-" + strings.Repeat("0", len(mask)-len(stripped)-1) + stripped
|
|
} else {
|
|
formatted = strings.Repeat("0", len(mask)-len(stripped)) + stripped
|
|
}
|
|
}
|
|
|
|
// Apply mask character by character
|
|
var out strings.Builder
|
|
fi := 0
|
|
if len(formatted) > len(mask) {
|
|
fi = len(formatted) - len(mask)
|
|
}
|
|
for _, m := range mask {
|
|
if fi >= len(formatted) {
|
|
out.WriteRune(m)
|
|
continue
|
|
}
|
|
switch m {
|
|
case '9', '#':
|
|
out.WriteByte(formatted[fi])
|
|
fi++
|
|
case '.':
|
|
out.WriteByte('.')
|
|
fi++
|
|
case ',':
|
|
if fi < len(formatted) && formatted[fi] == ',' {
|
|
out.WriteByte(',')
|
|
fi++
|
|
} else if fi < len(formatted) && (formatted[fi] >= '0' && formatted[fi] <= '9') {
|
|
out.WriteByte(',')
|
|
} else {
|
|
out.WriteByte(' ')
|
|
fi++
|
|
}
|
|
default:
|
|
out.WriteRune(m)
|
|
fi++
|
|
}
|
|
}
|
|
result := out.String()
|
|
|
|
if leftAlign {
|
|
result = strings.TrimLeft(result, " ")
|
|
result = result + strings.Repeat(" ", len(mask)-len(result))
|
|
}
|
|
|
|
// Parentheses for negative
|
|
if paren && n < 0 {
|
|
result = strings.Replace(result, "-", "(", 1)
|
|
result = strings.TrimRight(result, " ") + ")"
|
|
}
|
|
|
|
// Credit/Debit suffix
|
|
if credit && n >= 0 {
|
|
result += " CR"
|
|
}
|
|
if debit && n < 0 {
|
|
result += " DB"
|
|
}
|
|
|
|
return result
|
|
}
|