feat: SET DELETED filtering, SEEK/LOCATE/CONTINUE, SET command codegen

- 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>
This commit is contained in:
2026-04-02 22:33:59 +09:00
parent 08c0ef13d4
commit 21fd9dc65c
12 changed files with 651 additions and 30 deletions

View File

@@ -491,23 +491,64 @@ func (g *Generator) emitStmt(stmt ast.Stmt, locals localMap) {
g.indent-- g.indent--
g.writeln("}") g.writeln("}")
case *ast.SetCmd: case *ast.SetCmd:
upper := strings.ToUpper(s.Setting)
// Boolean SET toggles — call RTL Set function, no workarea needed
setFuncMap := map[string]string{
"DELETED": "SETDELETED",
"EXACT": "SETEXACT",
"SOFTSEEK": "SETSOFTSEEK",
"EXCLUSIVE": "SETEXCLUSIVE",
"FIXED": "SETFIXED",
"CANCEL": "SETCANCEL",
"BELL": "SETBELL",
"CONFIRM": "SETCONFIRM",
"INSERT": "SETINSERT",
"ESCAPE": "SETESCAPE",
"WRAP": "SETWRAP",
}
if funcName, ok := setFuncMap[upper]; ok {
onOff := strings.ToUpper(s.Extra)
if onOff == "ON" || onOff == "OFF" {
val := "true"
if onOff == "OFF" {
val = "false"
}
g.writeln(fmt.Sprintf(`t.PushSymbol(t.VM().FindSymbol(%q))`, funcName))
g.writeln("t.PushNil()")
g.writeln(fmt.Sprintf("t.PushBool(%s)", val))
g.writeln("t.Do(1)")
}
break
}
// Value SET commands — SET DATE/DECIMALS/EPOCH TO expr
valueFuncMap := map[string]string{
"DATE": "__SETDATEFORMAT",
"DECIMALS": "SETDECIMALS",
"EPOCH": "SETEPOCH",
}
if funcName, ok := valueFuncMap[upper]; ok && s.Expr != nil {
g.writeln(fmt.Sprintf(`t.PushSymbol(t.VM().FindSymbol(%q))`, funcName))
g.writeln("t.PushNil()")
g.emitExpr(s.Expr)
g.writeln("t.Do(1)")
break
}
// Workarea-specific SET commands
g.writeln("{") g.writeln("{")
g.indent++ g.indent++
g.writeln("wa := t.WA.(*hbrdd.WorkAreaManager)") g.writeln("wa := t.WA.(*hbrdd.WorkAreaManager)")
g.writeln("if area := wa.Current(); area != nil {") g.writeln("if area := wa.Current(); area != nil {")
g.indent++ g.indent++
upper := strings.ToUpper(s.Setting)
switch upper { switch upper {
case "FILTER": case "FILTER":
if s.Expr != nil { if s.Expr != nil {
g.writeln("if flt, ok := area.(hbrdd.Filterer); ok {")
g.indent++
g.emitExpr(s.Expr) g.emitExpr(s.Expr)
g.writeln(`flt.SetFilter(t.Pop2().AsString(), nil)`) g.writeln(`area.SetFilter(t.Pop2().AsString(), nil)`)
g.indent--
g.writeln("}")
} else { } else {
g.writeln("if flt, ok := area.(hbrdd.Filterer); ok { flt.ClearFilter() }") g.writeln("area.ClearFilter()")
} }
case "ORDER": case "ORDER":
if s.Expr != nil { if s.Expr != nil {

View File

@@ -1706,10 +1706,23 @@ func (p *Parser) parseSet() *ast.SetCmd {
var extra string var extra string
// SET commands: consume everything until end of line. // SET commands: consume everything until end of line.
// Values like "GR+/B, W+/BG" can't be parsed as expressions. // Boolean toggles: SET DELETED ON/OFF, SET EXACT ON/OFF, etc.
// SET FILTER TO is special — the condition IS an expression. // Value settings: SET FILTER TO expr, SET ORDER TO n, SET DATE TO fmt
upperSetting := strings.ToUpper(setting) upperSetting := strings.ToUpper(setting)
if p.match(token.TO) {
// Check for ON/OFF boolean toggle
booleanSets := map[string]bool{
"DELETED": true, "EXACT": true, "SOFTSEEK": true, "EXCLUSIVE": true,
"FIXED": true, "CANCEL": true, "BELL": true, "CONFIRM": true,
"INSERT": true, "ESCAPE": true, "WRAP": true, "INTENSITY": true,
"SCOREBOARD": true, "CONSOLE": true, "ALTERNATE": true, "PRINTER": true,
}
if booleanSets[upperSetting] {
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
extra = strings.ToUpper(p.expectMethodName().Literal)
}
} else if p.match(token.TO) {
if upperSetting == "FILTER" || upperSetting == "RELATION" || upperSetting == "ORDER" || upperSetting == "INDEX" { if upperSetting == "FILTER" || upperSetting == "RELATION" || upperSetting == "ORDER" || upperSetting == "INDEX" {
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF { if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
expr = p.parseExpr() expr = p.parseExpr()
@@ -1718,6 +1731,10 @@ func (p *Parser) parseSet() *ast.SetCmd {
p.advance() p.advance()
extra = p.expectMethodName().Literal extra = p.expectMethodName().Literal
} }
} else if upperSetting == "DATE" || upperSetting == "DECIMALS" || upperSetting == "EPOCH" {
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
expr = p.parseExpr()
}
} }
} }

View File

@@ -241,10 +241,7 @@ func (p *Parser) stmtRecallPackZap() ast.Stmt {
} }
func (p *Parser) stmtSet() ast.Stmt { func (p *Parser) stmtSet() ast.Stmt {
// SET command — skip to EOL (SET COLOR, SET FILTER, SET ORDER, etc.) return p.parseSet()
p.skipToEndOfLine()
p.expectEndOfStmt()
return &ast.ExprStmt{X: &ast.LiteralExpr{Kind: token.NIL_LIT, Value: "NIL"}}
} }
func (p *Parser) stmtDefer() ast.Stmt { return p.parseDefer() } func (p *Parser) stmtDefer() ast.Stmt { return p.parseDefer() }

229
examples/test_search.prg Normal file
View File

@@ -0,0 +1,229 @@
// Comprehensive search test: SEEK, SOFTSEEK, SET DELETED, LOCATE/CONTINUE
// Tests all search-related RDD functionality end-to-end.
FUNCTION Main()
LOCAL i, aStruct, nCount
? "=========================================="
? " Five Search/Filter Test Suite"
? "=========================================="
?
// --- Setup: Create test table ---
aStruct := {{"ID","N",6,0}, {"NAME","C",20,0}, {"CITY","C",15,0}, {"AGE","N",3,0}, {"ACTIVE","L",1,0}}
dbCreate("search_test", aStruct)
USE "search_test"
// Insert 50 records
FOR i := 1 TO 50
APPEND BLANK
FieldPut(1, i)
FieldPut(2, "Name_" + PadL(LTrim(Str(i)), 3, "0"))
IF i % 5 == 0
FieldPut(3, "Seoul")
ELSEIF i % 5 == 1
FieldPut(3, "Tokyo")
ELSEIF i % 5 == 2
FieldPut(3, "Beijing")
ELSEIF i % 5 == 3
FieldPut(3, "London")
ELSE
FieldPut(3, "NewYork")
ENDIF
FieldPut(4, 20 + (i % 40))
FieldPut(5, (i % 3 != 0))
NEXT
? "Setup: 50 records created, RecCount =", RecCount()
?
// ============================================
// TEST 1: INDEX + SEEK (exact match)
// ============================================
? "--- TEST 1: INDEX + SEEK ---"
INDEX ON NAME TO search_name
? "Index on NAME created."
SEEK "Name_001"
? "SEEK 'Name_001': Found =", Found(), "RecNo =", RecNo()
IF !Found()
? " *** FAIL: should find Name_001"
ENDIF
SEEK "Name_025"
? "SEEK 'Name_025': Found =", Found(), "RecNo =", RecNo()
IF !Found()
? " *** FAIL: should find Name_025"
ENDIF
SEEK "Name_050"
? "SEEK 'Name_050': Found =", Found(), "RecNo =", RecNo()
IF !Found()
? " *** FAIL: should find Name_050"
ENDIF
SEEK "Name_999"
? "SEEK 'Name_999': Found =", Found(), "Eof =", Eof()
IF Found()
? " *** FAIL: should NOT find Name_999"
ENDIF
?
// ============================================
// TEST 2: SOFTSEEK
// ============================================
? "--- TEST 2: SOFTSEEK ---"
SET SOFTSEEK ON
SEEK "Name_030"
? "SOFTSEEK 'Name_030': Found =", Found(), "RecNo =", RecNo()
IF Found()
? " Name =", FieldGet(2)
ENDIF
SEEK "Name_031"
? "SOFTSEEK 'Name_031': Found =", Found(), "RecNo =", RecNo()
IF !Found() .AND. !Eof()
? " Positioned at:", FieldGet(2), "(softseek OK)"
ENDIF
SET SOFTSEEK OFF
?
// ============================================
// TEST 3: Partial key SEEK
// ============================================
? "--- TEST 3: Partial Key SEEK ---"
SEEK "Name_01"
? "SEEK 'Name_01' (partial): Found =", Found()
IF Found()
? " Name =", FieldGet(2), "(should be Name_010 or similar)"
ENDIF
?
// ============================================
// TEST 4: SET DELETED — skip deleted records
// ============================================
? "--- TEST 4: SET DELETED ---"
// Delete records 10,20,30,40,50 (every 10th)
SET ORDER TO 0 // natural order
GO TOP
FOR i := 1 TO 50
GO i
IF FieldGet(1) % 10 == 0
DELETE
ENDIF
NEXT
? "Deleted records: 10, 20, 30, 40, 50"
SET DELETED OFF
GO TOP
nCount := 0
DO WHILE !Eof()
nCount++
SKIP
ENDDO
? "SET DELETED OFF: visible =", nCount, "(should be 50)"
SET DELETED ON
GO TOP
nCount := 0
DO WHILE !Eof()
nCount++
SKIP
ENDDO
? "SET DELETED ON: visible =", nCount, "(should be 45)"
IF nCount != 45
? " *** FAIL: expected 45, got", nCount
ELSE
? " PASS"
ENDIF
// GoTop should skip deleted record if record 1 would be first
// Let's delete record 1 and test
GO 1
DELETE
SET DELETED ON
GO TOP
? "After deleting rec 1, GO TOP recNo =", RecNo(), "(should be 2)"
IF RecNo() != 2
? " *** FAIL: GoTop should skip deleted rec 1"
ELSE
? " PASS"
ENDIF
// Recall record 1 for subsequent tests
GO 1
RECALL
SET DELETED OFF
?
// ============================================
// TEST 5: SET DELETED + INDEX
// ============================================
? "--- TEST 5: SET DELETED + INDEX ---"
SET ORDER TO 1 // NAME index
SET DELETED ON
GO TOP
nCount := 0
DO WHILE !Eof()
nCount++
SKIP
ENDDO
? "Indexed + SET DELETED ON: visible =", nCount, "(should be 45)"
IF nCount != 45
? " *** FAIL"
ELSE
? " PASS"
ENDIF
SET DELETED OFF
?
// ============================================
// TEST 6: Navigation correctness
// ============================================
? "--- TEST 6: Navigation ---"
SET ORDER TO 0 // natural
GO TOP
? "GO TOP: RecNo =", RecNo(), "(should be 1)"
GO BOTTOM
? "GO BOTTOM: RecNo =", RecNo(), "(should be 50)"
SKIP -1
? "SKIP -1: RecNo =", RecNo(), "(should be 49)"
GO TOP
SKIP 5
? "GO TOP + SKIP 5: RecNo =", RecNo(), "(should be 6)"
?
// ============================================
// TEST 7: SEEK on city index
// ============================================
? "--- TEST 7: Second index ---"
INDEX ON CITY TO search_city
? "Index on CITY created."
SEEK "Seoul"
? "SEEK 'Seoul': Found =", Found()
IF Found()
? " City =", FieldGet(3), "ID =", FieldGet(1)
ENDIF
SEEK "Tokyo"
? "SEEK 'Tokyo': Found =", Found()
IF Found()
? " City =", FieldGet(3), "ID =", FieldGet(1)
ENDIF
SEEK "Paris"
? "SEEK 'Paris': Found =", Found(), "(should be .F.)"
?
// ============================================
// Cleanup
// ============================================
CLOSE ALL
? "=========================================="
? " All search tests complete."
? "=========================================="
RETURN

View File

@@ -27,6 +27,10 @@ type BaseArea struct {
filterExpr string filterExpr string
filterBlock func(*hbrt.Thread) bool filterBlock func(*hbrt.Thread) bool
// Locate
locateExpr string
locateBlock func(*hbrt.Thread) bool
// Relations // Relations
relations []*Relation relations []*Relation
} }
@@ -120,3 +124,23 @@ func (a *BaseArea) ClearRelation() error {
a.relations = nil a.relations = nil
return nil return nil
} }
// --- Locate support ---
func (a *BaseArea) SetLocate(expr string, block func(*hbrt.Thread) bool) {
a.locateExpr = expr
a.locateBlock = block
}
func (a *BaseArea) LocateBlock() func(*hbrt.Thread) bool {
return a.locateBlock
}
// --- Global RDD configuration callbacks ---
// These are set by hbrtl to avoid circular imports.
var (
// IsSetDeleted returns true when SET DELETED is ON.
// Set by hbrtl.init() to hbrtl.GetSetDeleted.
IsSetDeleted func() bool
)

View File

@@ -481,6 +481,10 @@ func (a *DBFArea) Skip(count int64) error {
if err := a.GoTo(newRec); err != nil { if err := a.GoTo(newRec); err != nil {
return err return err
} }
// Skip deleted records when SET DELETED ON
if err := a.skipFilter(1); err != nil {
return err
}
} }
} else { } else {
for i := int64(0); i > count; i-- { for i := int64(0); i > count; i-- {
@@ -491,6 +495,9 @@ func (a *DBFArea) Skip(count int64) error {
if err := a.GoTo(a.recNo - 1); err != nil { if err := a.GoTo(a.recNo - 1); err != nil {
return err return err
} }
if err := a.skipFilter(-1); err != nil {
return err
}
} }
} }
@@ -706,9 +713,49 @@ func (a *DBFArea) updateHeader() {
WriteHeader(a.dataFile, &a.header) WriteHeader(a.dataFile, &a.header)
} }
// skipFilter advances cursor past deleted/filtered records.
// Harbour: hb_dbfSkipFilter in dbf1.c
// direction: 1 = forward, -1 = backward
func (a *DBFArea) skipFilter(direction int64) error { func (a *DBFArea) skipFilter(direction int64) error {
// TODO: implement SET FILTER and SET DELETED filtering if direction == 0 {
// For now, no filtering applied direction = 1
}
setDel := hbrdd.IsSetDeleted != nil && hbrdd.IsSetDeleted()
if !setDel {
return nil
}
for !a.FEof && !a.FBof {
if !a.Deleted() {
break
}
// Move to next/prev record
if direction > 0 {
newRec := a.recNo + 1
if newRec > a.recCount {
a.FEof = true
a.recNo = a.recCount + 1
break
}
if err := a.GoTo(newRec); err != nil {
return err
}
} else {
if a.recNo <= 1 {
a.FBof = true
if err := a.GoTo(1); err != nil {
return err
}
break
}
if err := a.GoTo(a.recNo - 1); err != nil {
return err
}
}
}
return nil return nil
} }

View File

@@ -125,6 +125,16 @@ type Area interface {
// Bulk operations // Bulk operations
Pack() error Pack() error
Zap() error Zap() error
// State — Harbour: hb_waSetFound, locate support
SetFound(b bool)
SetLocate(expr string, block func(*hbrt.Thread) bool)
LocateBlock() func(*hbrt.Thread) bool
// Filter
SetFilter(expr string, block func(*hbrt.Thread) bool) error
ClearFilter() error
HasFilter() bool
} }
// --- Optional interfaces (drivers implement as needed) --- // --- Optional interfaces (drivers implement as needed) ---
@@ -177,14 +187,6 @@ type Locker interface {
IsLocked(recNo uint32) bool IsLocked(recNo uint32) bool
} }
// Filterer provides SET FILTER support.
// Harbour: SELF_SETFILTER, SELF_CLEARFILTER
type Filterer interface {
SetFilter(expr string, block func(*hbrt.Thread) bool) error
ClearFilter() error
HasFilter() bool
}
// Relater provides SET RELATION support. // Relater provides SET RELATION support.
// Harbour: SELF_SETREL, SELF_CLEARREL, SELF_FORCEREL // Harbour: SELF_SETREL, SELF_CLEARREL, SELF_FORCEREL
type Relater interface { type Relater interface {

View File

@@ -131,6 +131,12 @@ type memArea struct {
curIndex int // -1 = natural order, 0+ = index curIndex int // -1 = natural order, 0+ = index
indexPos int // position in current index indexPos int // position in current index
closed bool closed bool
// Filter/Locate
filterExpr string
filterBlock func(*hbrt.Thread) bool
locateExpr string
locateBlock func(*hbrt.Thread) bool
} }
func newMemArea(tbl *memTable, alias string, drv *MemDriver) *memArea { func newMemArea(tbl *memTable, alias string, drv *MemDriver) *memArea {
@@ -173,7 +179,26 @@ func (a *memArea) Flush() error { return nil } // no-op: memory only
func (a *memArea) BOF() bool { return a.bof } func (a *memArea) BOF() bool { return a.bof }
func (a *memArea) EOF() bool { return a.eof } func (a *memArea) EOF() bool { return a.eof }
func (a *memArea) Found() bool { return a.found } func (a *memArea) Found() bool { return a.found }
func (a *memArea) SetFound(b bool) { a.found = b }
func (a *memArea) SetLocate(expr string, block func(*hbrt.Thread) bool) {
a.locateExpr = expr
a.locateBlock = block
}
func (a *memArea) LocateBlock() func(*hbrt.Thread) bool { return a.locateBlock }
func (a *memArea) SetFilter(expr string, block func(*hbrt.Thread) bool) error {
a.filterExpr = expr
a.filterBlock = block
return nil
}
func (a *memArea) ClearFilter() error {
a.filterExpr = ""
a.filterBlock = nil
return nil
}
func (a *memArea) HasFilter() bool { return a.filterBlock != nil }
func (a *memArea) GoTo(recNo uint32) error { func (a *memArea) GoTo(recNo uint32) error {
a.tbl.mu.RLock() a.tbl.mu.RLock()

View File

@@ -400,3 +400,203 @@ func rtlDbZap(t *hbrt.Thread) {
} }
t.RetNil() t.RetNil()
} }
// --- LOCATE / CONTINUE ---
// Harbour: DBLOCATE(bFor, bWhile, nNext, nRec, lRest) → .T./.F.
// Searches from current position for record matching condition.
func rtlDbLocate(t *hbrt.Thread) {
nParams := t.ParamCount()
t.Frame(nParams, 0)
defer t.EndProc()
wam := getWA(t)
if wam == nil {
t.RetBool(false)
return
}
area := wam.Current()
if area == nil {
t.RetBool(false)
return
}
// param 1: bFor condition block
var bFor hbrt.Value
if nParams >= 1 && !t.Local(1).IsNil() {
bFor = t.Local(1)
}
// param 2: bWhile condition block
var bWhile hbrt.Value
if nParams >= 2 && !t.Local(2).IsNil() {
bWhile = t.Local(2)
}
// param 3: nNext — max records to scan
nNext := int64(0) // 0 = all
if nParams >= 3 && !t.Local(3).IsNil() {
nNext = t.Local(3).AsNumInt()
}
// param 4: nRec — specific record number
if nParams >= 4 && !t.Local(4).IsNil() {
nRec := uint32(t.Local(4).AsNumInt())
area.GoTo(nRec)
if bFor.IsBlock() {
t.PendingParams2(0)
bFor.AsBlock().Fn(t)
result := t.Pop2()
area.SetFound(result.AsBool())
} else {
area.SetFound(true)
}
t.RetBool(area.Found())
return
}
// param 5: lRest — .T. = continue from current, .F. = from top
lRest := false
if nParams >= 5 && !t.Local(5).IsNil() {
lRest = t.Local(5).AsBool()
}
if !lRest && nNext == 0 {
area.GoTop()
}
// Store locate block for __dbContinue
if bFor.IsBlock() {
area.SetLocate("", func(lt *hbrt.Thread) bool {
lt.PendingParams2(0)
bFor.AsBlock().Fn(lt)
return lt.Pop2().AsBool()
})
}
found := false
count := int64(0)
for !area.EOF() {
if nNext > 0 && count >= nNext {
break
}
// Check WHILE condition
if bWhile.IsBlock() {
t.PendingParams2(0)
bWhile.AsBlock().Fn(t)
if !t.Pop2().AsBool() {
break
}
}
// Check FOR condition
if bFor.IsBlock() {
t.PendingParams2(0)
bFor.AsBlock().Fn(t)
if t.Pop2().AsBool() {
found = true
break
}
} else {
found = true
break
}
area.Skip(1)
count++
}
area.SetFound(found)
t.RetBool(found)
}
// __DBCONTINUE — continue previous LOCATE search.
// Harbour: __dbContinue()
func rtlDbContinue(t *hbrt.Thread) {
t.Frame(0, 0)
defer t.EndProc()
wam := getWA(t)
if wam == nil {
t.RetBool(false)
return
}
area := wam.Current()
if area == nil {
t.RetBool(false)
return
}
blk := area.LocateBlock()
if blk == nil {
area.SetFound(false)
t.RetBool(false)
return
}
// Skip past current record
area.Skip(1)
found := false
for !area.EOF() {
if blk(t) {
found = true
break
}
area.Skip(1)
}
area.SetFound(found)
t.RetBool(found)
}
// --- DBSETFILTER / DBCLEARFILTER / DBFILTER ---
// DBSETFILTER(bCondition [, cCondition])
func rtlDbSetFilter(t *hbrt.Thread) {
nParams := t.ParamCount()
t.Frame(nParams, 0)
defer t.EndProc()
wam := getWA(t)
if wam == nil {
t.RetNil()
return
}
area := wam.Current()
if area == nil {
t.RetNil()
return
}
if nParams >= 1 && t.Local(1).IsBlock() {
bFilter := t.Local(1)
expr := ""
if nParams >= 2 && t.Local(2).IsString() {
expr = t.Local(2).AsString()
}
area.SetFilter(expr, func(lt *hbrt.Thread) bool {
lt.PendingParams2(0)
bFilter.AsBlock().Fn(lt)
return lt.Pop2().AsBool()
})
}
t.RetNil()
}
// DBCLEARFILTER()
func rtlDbClearFilter(t *hbrt.Thread) {
t.Frame(0, 0)
defer t.EndProc()
wam := getWA(t)
if wam == nil {
t.RetNil()
return
}
if area := wam.Current(); area != nil {
area.ClearFilter()
}
t.RetNil()
}
// DBFILTER() → cFilterExpression
func rtlDbFilter(t *hbrt.Thread) {
t.Frame(0, 0)
defer t.EndProc()
// TODO: return stored filter expression from area
t.RetString("")
}

View File

@@ -179,6 +179,14 @@ func RegisterRTL(vm *hbrt.VM) {
hbrt.Sym("PACK", hbrt.FsPublic, rtlDbPack), hbrt.Sym("PACK", hbrt.FsPublic, rtlDbPack),
hbrt.Sym("ZAP", hbrt.FsPublic, rtlDbZap), hbrt.Sym("ZAP", hbrt.FsPublic, rtlDbZap),
// Locate / Filter
hbrt.Sym("DBLOCATE", hbrt.FsPublic, rtlDbLocate),
hbrt.Sym("__DBLOCATE", hbrt.FsPublic, rtlDbLocate),
hbrt.Sym("__DBCONTINUE", hbrt.FsPublic, rtlDbContinue),
hbrt.Sym("DBSETFILTER", hbrt.FsPublic, rtlDbSetFilter),
hbrt.Sym("DBCLEARFILTER", hbrt.FsPublic, rtlDbClearFilter),
hbrt.Sym("DBFILTER", hbrt.FsPublic, rtlDbFilter),
// Encoding / Hashing (Go stdlib) // Encoding / Hashing (Go stdlib)
hbrt.Sym("HB_MD5", hbrt.FsPublic, HbMD5), hbrt.Sym("HB_MD5", hbrt.FsPublic, HbMD5),
hbrt.Sym("HB_SHA256", hbrt.FsPublic, HbSHA256), hbrt.Sym("HB_SHA256", hbrt.FsPublic, HbSHA256),
@@ -247,6 +255,18 @@ func RegisterRTL(vm *hbrt.VM) {
hbrt.Sym("__SETDATEFORMAT", hbrt.FsPublic, SetDateFunc), hbrt.Sym("__SETDATEFORMAT", hbrt.FsPublic, SetDateFunc),
hbrt.Sym("__SETDECIMALS", hbrt.FsPublic, SetDecimalsFunc), hbrt.Sym("__SETDECIMALS", hbrt.FsPublic, SetDecimalsFunc),
hbrt.Sym("__SETEPOCH", hbrt.FsPublic, SetEpochFunc), hbrt.Sym("__SETEPOCH", hbrt.FsPublic, SetEpochFunc),
// SET toggle functions
hbrt.Sym("SETDELETED", hbrt.FsPublic, SetDeletedFunc),
hbrt.Sym("SETEXACT", hbrt.FsPublic, SetExactFunc),
hbrt.Sym("SETSOFTSEEK", hbrt.FsPublic, SetSoftSeekFunc),
hbrt.Sym("SETEXCLUSIVE", hbrt.FsPublic, SetExclusiveFunc),
hbrt.Sym("SETFIXED", hbrt.FsPublic, SetFixedFunc),
hbrt.Sym("SETCANCEL", hbrt.FsPublic, SetCancelFunc),
hbrt.Sym("SETBELL", hbrt.FsPublic, SetBellFunc),
hbrt.Sym("SETCONFIRM", hbrt.FsPublic, SetConfirmFunc),
hbrt.Sym("SETINSERT", hbrt.FsPublic, SetInsertFunc),
hbrt.Sym("SETESCAPE", hbrt.FsPublic, SetEscapeFunc),
hbrt.Sym("SETWRAP", hbrt.FsPublic, SetWrapFunc),
// SET constants // SET constants
hbrt.Sym("_SET_EXACT", hbrt.FsPublic, SetConstExact), hbrt.Sym("_SET_EXACT", hbrt.FsPublic, SetConstExact),
hbrt.Sym("_SET_DELETED", hbrt.FsPublic, SetConstDeleted), hbrt.Sym("_SET_DELETED", hbrt.FsPublic, SetConstDeleted),

View File

@@ -8,6 +8,7 @@
package hbrtl package hbrtl
import ( import (
"five/hbrdd"
"five/hbrt" "five/hbrt"
"sync" "sync"
) )
@@ -261,8 +262,10 @@ func SetConstDateFmt(t *hbrt.Thread) { t.Frame(0, 0); defer t.EndProc(); t.Pus
func SetConstDecimals(t *hbrt.Thread) { t.Frame(0, 0); defer t.EndProc(); t.PushInt(SetDecimals); t.RetValue() } func SetConstDecimals(t *hbrt.Thread) { t.Frame(0, 0); defer t.EndProc(); t.PushInt(SetDecimals); t.RetValue() }
func SetConstEpoch(t *hbrt.Thread) { t.Frame(0, 0); defer t.EndProc(); t.PushInt(SetEpoch); t.RetValue() } func SetConstEpoch(t *hbrt.Thread) { t.Frame(0, 0); defer t.EndProc(); t.PushInt(SetEpoch); t.RetValue() }
// init registers default DATE format and EPOCH // init registers default DATE format and EPOCH, and hooks into hbrdd.
func init() { func init() {
hbrdd.IsSetDeleted = GetSetDeleted
setMu.Lock() setMu.Lock()
if _, ok := settings[SetDateFmt]; !ok { if _, ok := settings[SetDateFmt]; !ok {
settings[SetDateFmt] = hbrt.MakeString("mm/dd/yy") settings[SetDateFmt] = hbrt.MakeString("mm/dd/yy")

View File

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