`TOTAL TO <file> ON <key> [FIELDS <list>] [FOR ...] [WHILE ...]
[NEXT ...] [RECORD ...] [REST] [ALL]` joins the family of std.ch
DML rewrites. New RTL primitive __dbTotal:
* Walk the source under dbEval-style FOR/WHILE/NEXT/RECORD/REST
bounds. The source must already be sorted/indexed on the key —
same precondition as Harbour's dbtotal.prg.
* Track the current group key. On each key change, flush the
accumulated row to the destination (writing the running totals
back into the most recently appended record's sum-fields,
preserving each field's declared length/decimals).
* On the *first* record of every group, append a fresh dst row
and copy all non-memo source fields into it; subsequent records
in the group only contribute to the sums. Net effect: non-summed
fields take the first record's value, summed fields hold the
group total. Same shape as harbour-core/src/rdd/dbtotal.prg.
* Memo fields are dropped from the destination structure (Harbour
does the same).
Parser cleanup: TOTAL removed from the IDENT-statement no-op switch.
Gates green:
go test ./... : PASS
FiveSql2 SQL:1999 : 43/43
Harbour compat : 56/56
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1522 lines
31 KiB
Go
1522 lines
31 KiB
Go
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
|
// All rights reserved.
|
|
|
|
// Database callable functions: FIELDPUT, ALIAS, DBEVAL, DBUSEAREA, DBCLOSEAREA,
|
|
// DBGOTO, DBSKIP, DBAPPEND, DBDELETE, DBRECALL, DBCOMMIT, DBSEEK,
|
|
// DBGOTOP, DBGOBOTTOM, DBRLOCKLIST, DBSETFILTER, DBCLEARFILTER
|
|
|
|
package hbrtl
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"five/hbrt"
|
|
"five/hbrdd"
|
|
"five/hbrdd/dbf"
|
|
)
|
|
|
|
// FIELDPUT(nField, xValue) → xValue
|
|
func rtlFieldPut(t *hbrt.Thread) {
|
|
t.Frame(2, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
nField := t.Local(1).AsInt()
|
|
val := t.Local(2)
|
|
area.PutValue(nField-1, val) // 1-based to 0-based
|
|
t.RetVal(val)
|
|
}
|
|
|
|
// ALIAS([nWorkArea]) → cAlias
|
|
func rtlAlias(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetString("")
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area != nil {
|
|
t.RetString(area.Alias())
|
|
} else {
|
|
t.RetString("")
|
|
}
|
|
}
|
|
|
|
// DBEVAL(bBlock [, bFor [, bWhile [, nCount [, nRecord [, lRest]]]]]) → NIL
|
|
func rtlDbEval(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
|
|
block := t.Local(1)
|
|
if !block.IsBlock() {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
|
|
var bFor, bWhile hbrt.Value
|
|
nCount := -1
|
|
lRest := false
|
|
|
|
if nParams >= 2 {
|
|
bFor = t.Local(2)
|
|
}
|
|
if nParams >= 3 {
|
|
bWhile = t.Local(3)
|
|
}
|
|
if nParams >= 4 && !t.Local(4).IsNil() {
|
|
nCount = t.Local(4).AsInt()
|
|
}
|
|
if nParams >= 5 && !t.Local(5).IsNil() {
|
|
nRec := t.Local(5).AsInt()
|
|
area.GoTo(uint32(nRec))
|
|
}
|
|
if nParams >= 6 && !t.Local(6).IsNil() {
|
|
lRest = t.Local(6).AsBool()
|
|
}
|
|
|
|
// If not lRest and no record specified, go top
|
|
if !lRest && (nParams < 5 || t.Local(5).IsNil()) {
|
|
area.GoTop()
|
|
}
|
|
|
|
count := 0
|
|
for !area.EOF() {
|
|
if nCount >= 0 && count >= nCount {
|
|
break
|
|
}
|
|
|
|
// While condition
|
|
if !bWhile.IsNil() && bWhile.IsBlock() {
|
|
blk := bWhile.AsBlock()
|
|
t.PendingParams2(0)
|
|
blk.Fn(t)
|
|
if !t.GetRetValue().AsBool() {
|
|
break
|
|
}
|
|
}
|
|
|
|
// For condition
|
|
doBlock := true
|
|
if !bFor.IsNil() && bFor.IsBlock() {
|
|
blk := bFor.AsBlock()
|
|
t.PendingParams2(0)
|
|
blk.Fn(t)
|
|
doBlock = t.GetRetValue().AsBool()
|
|
}
|
|
|
|
if doBlock {
|
|
blk := block.AsBlock()
|
|
t.PendingParams2(0)
|
|
blk.Fn(t)
|
|
}
|
|
|
|
area.Skip(1)
|
|
count++
|
|
}
|
|
|
|
t.RetNil()
|
|
}
|
|
|
|
// DBUSEAREA([lNewArea], [cDriver], cName, [cAlias], [lShared], [lReadOnly]) → NIL
|
|
func rtlDbUseArea(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
cName := ""
|
|
cAlias := ""
|
|
cDriver := "DBFNTX"
|
|
if nParams >= 3 && !t.Local(3).IsNil() {
|
|
cName = t.Local(3).AsString()
|
|
}
|
|
if nParams >= 4 && !t.Local(4).IsNil() {
|
|
cAlias = t.Local(4).AsString()
|
|
}
|
|
if nParams >= 2 && !t.Local(2).IsNil() {
|
|
cDriver = t.Local(2).AsString()
|
|
}
|
|
shared := false
|
|
readOnly := false
|
|
if nParams >= 5 && !t.Local(5).IsNil() {
|
|
shared = t.Local(5).AsBool()
|
|
}
|
|
if nParams >= 6 && !t.Local(6).IsNil() {
|
|
readOnly = t.Local(6).AsBool()
|
|
}
|
|
_, err := wam.Open(cDriver, cName, cAlias, shared, readOnly)
|
|
if err != nil {
|
|
panic(&hbrt.HbError{
|
|
Description: err.Error(),
|
|
Operation: "DBUSEAREA",
|
|
SubSystem: "BASE",
|
|
})
|
|
}
|
|
t.RetNil()
|
|
}
|
|
|
|
// DBCLOSEALL() → NIL — closes all open work areas
|
|
func rtlDbCloseAll(t *hbrt.Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam != nil {
|
|
wam.CloseAll()
|
|
}
|
|
t.RetNil()
|
|
}
|
|
|
|
// DBCLOSEAREA() → NIL
|
|
func rtlDbCloseArea(t *hbrt.Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam != nil {
|
|
wam.Close()
|
|
}
|
|
t.RetNil()
|
|
}
|
|
|
|
// DBGOTO(nRecNo) → NIL
|
|
func rtlDbGoTo(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area != nil {
|
|
area.GoTo(uint32(t.Local(1).AsLong()))
|
|
}
|
|
t.RetNil()
|
|
}
|
|
|
|
// DBSKIP([nRecords]) → NIL
|
|
func rtlDbSkip(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
n := int64(1)
|
|
if nParams >= 1 && !t.Local(1).IsNil() {
|
|
n = t.Local(1).AsLong()
|
|
}
|
|
area.Skip(n)
|
|
t.RetNil()
|
|
}
|
|
|
|
// DBGOTOP() → NIL
|
|
func rtlDbGoTop(t *hbrt.Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area != nil {
|
|
area.GoTop()
|
|
}
|
|
t.RetNil()
|
|
}
|
|
|
|
// DBGOBOTTOM() → NIL
|
|
func rtlDbGoBottom(t *hbrt.Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area != nil {
|
|
area.GoBottom()
|
|
}
|
|
t.RetNil()
|
|
}
|
|
|
|
// DBAPPEND([lUnlock]) → NIL
|
|
func rtlDbAppend(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area != nil {
|
|
area.Append()
|
|
}
|
|
t.RetNil()
|
|
}
|
|
|
|
// DBDELETE() → NIL
|
|
func rtlDbDelete(t *hbrt.Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area != nil {
|
|
area.Delete()
|
|
}
|
|
t.RetNil()
|
|
}
|
|
|
|
// DBRECALL() → NIL
|
|
func rtlDbRecall(t *hbrt.Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area != nil {
|
|
area.Recall()
|
|
}
|
|
t.RetNil()
|
|
}
|
|
|
|
// DBRLOCK([nRecNo]) → lSuccess
|
|
// Acquires an advisory byte-range lock on a single record via fcntl(F_SETLK).
|
|
// Non-blocking: returns .F. if another process already holds the lock.
|
|
// Harbour: SELF_LOCK(a, &lockInfo) with DBLM_EXCLUSIVE.
|
|
func rtlDbRLock(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area == nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
da, ok := area.(*dbf.DBFArea)
|
|
if !ok {
|
|
// Non-DBF drivers: assume success (in-memory, etc.)
|
|
t.RetBool(true)
|
|
return
|
|
}
|
|
var recNo uint32
|
|
if nParams >= 1 && !t.Local(1).IsNil() {
|
|
recNo = uint32(t.Local(1).AsNumInt())
|
|
}
|
|
locked, err := da.LockRecord(recNo)
|
|
if err != nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
t.RetBool(locked)
|
|
}
|
|
|
|
// DBRUNLOCK([nRecNo]) → NIL
|
|
// Releases a specific record lock, or all record locks held by this
|
|
// workarea if called without arguments. Harbour: SELF_UNLOCK.
|
|
func rtlDbRUnlock(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
da, ok := area.(*dbf.DBFArea)
|
|
if !ok {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
var recNo uint32
|
|
if nParams >= 1 && !t.Local(1).IsNil() {
|
|
recNo = uint32(t.Local(1).AsNumInt())
|
|
}
|
|
_ = da.UnlockRecord(recNo)
|
|
t.RetNil()
|
|
}
|
|
|
|
// FLOCK() → lSuccess
|
|
// Acquires an exclusive file-wide lock via fcntl. Non-blocking: returns .F.
|
|
// if another process holds the file lock. Harbour: SELF_LOCK with DBLM_FILE.
|
|
func rtlFLock(t *hbrt.Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area == nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
da, ok := area.(*dbf.DBFArea)
|
|
if !ok {
|
|
t.RetBool(true) // in-memory etc.
|
|
return
|
|
}
|
|
locked, err := da.LockFile()
|
|
if err != nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
t.RetBool(locked)
|
|
}
|
|
|
|
// DBUNLOCK() → NIL
|
|
// Releases all locks (file + record) held by the current workarea.
|
|
// Harbour: SELF_UNLOCK with no record number.
|
|
func rtlDbUnlock(t *hbrt.Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
if da, ok := area.(*dbf.DBFArea); ok {
|
|
_ = da.UnlockRecord(0)
|
|
_ = da.UnlockFile()
|
|
}
|
|
t.RetNil()
|
|
}
|
|
|
|
// DBCOMMIT() → NIL
|
|
func rtlDbCommit(t *hbrt.Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area != nil {
|
|
area.Flush()
|
|
}
|
|
t.RetNil()
|
|
}
|
|
|
|
// DBSEEK(xValue [, lSoftSeek [, lLast]]) → lFound
|
|
func rtlDbSeek(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area == nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
val := t.Local(1)
|
|
softSeek := GetSetSoftSeek() // default: check SET SOFTSEEK
|
|
findLast := false
|
|
if nParams >= 2 && !t.Local(2).IsNil() {
|
|
softSeek = t.Local(2).AsBool()
|
|
}
|
|
if nParams >= 3 && !t.Local(3).IsNil() {
|
|
findLast = t.Local(3).AsBool()
|
|
}
|
|
// Check if area implements Indexer
|
|
if idx, ok := area.(hbrdd.Indexer); ok {
|
|
found, _ := idx.Seek(val, softSeek, findLast)
|
|
t.RetBool(found)
|
|
} else {
|
|
t.RetBool(false)
|
|
}
|
|
}
|
|
|
|
// DBSELECTAREA(nArea | cAlias) → NIL
|
|
func rtlDbSelectArea(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
v := t.Local(1)
|
|
if v.IsString() {
|
|
wam.Select(v.AsString())
|
|
} else {
|
|
wam.Select(uint16(v.AsInt()))
|
|
}
|
|
t.RetNil()
|
|
}
|
|
|
|
// DBPACK() → NIL
|
|
func rtlDbPack(t *hbrt.Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area != nil {
|
|
area.Pack()
|
|
}
|
|
t.RetNil()
|
|
}
|
|
|
|
// DBZAP() → NIL
|
|
func rtlDbZap(t *hbrt.Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area != nil {
|
|
area.Zap()
|
|
}
|
|
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.EndProcFast()
|
|
|
|
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.EndProcFast()
|
|
|
|
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)
|
|
}
|
|
|
|
// rtlDbAverage implements __dbAverage(bExpr, bFor, bWhile, nNext, nRec,
|
|
// lRest) — sum the expression over visible records and return the
|
|
// arithmetic mean. Returns 0 when the loop visits no records (mirrors
|
|
// Harbour's idiom of avoiding a divide-by-zero in the expansion).
|
|
//
|
|
// Used by `AVERAGE <x> TO <v>` in std.ch.
|
|
func rtlDbAverage(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetDouble(0, 10, 2)
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area == nil {
|
|
t.RetDouble(0, 10, 2)
|
|
return
|
|
}
|
|
|
|
bExpr := t.Local(1)
|
|
if !bExpr.IsBlock() {
|
|
t.RetDouble(0, 10, 2)
|
|
return
|
|
}
|
|
var bFor, bWhile hbrt.Value
|
|
if nParams >= 2 {
|
|
bFor = t.Local(2)
|
|
}
|
|
if nParams >= 3 {
|
|
bWhile = t.Local(3)
|
|
}
|
|
nCount := -1
|
|
if nParams >= 4 && !t.Local(4).IsNil() {
|
|
nCount = t.Local(4).AsInt()
|
|
}
|
|
if nParams >= 5 && !t.Local(5).IsNil() {
|
|
area.GoTo(uint32(t.Local(5).AsInt()))
|
|
}
|
|
lRest := false
|
|
if nParams >= 6 && !t.Local(6).IsNil() {
|
|
lRest = t.Local(6).AsBool()
|
|
}
|
|
if !lRest && (nParams < 5 || t.Local(5).IsNil()) {
|
|
area.GoTop()
|
|
}
|
|
|
|
sum := 0.0
|
|
n := 0
|
|
scanned := 0
|
|
for !area.EOF() {
|
|
if nCount >= 0 && scanned >= nCount {
|
|
break
|
|
}
|
|
// WHILE
|
|
if bWhile.IsBlock() {
|
|
t.PendingParams2(0)
|
|
bWhile.AsBlock().Fn(t)
|
|
if !t.GetRetValue().AsBool() {
|
|
break
|
|
}
|
|
}
|
|
// FOR
|
|
eval := true
|
|
if bFor.IsBlock() {
|
|
t.PendingParams2(0)
|
|
bFor.AsBlock().Fn(t)
|
|
eval = t.GetRetValue().AsBool()
|
|
}
|
|
if eval {
|
|
t.PendingParams2(0)
|
|
bExpr.AsBlock().Fn(t)
|
|
sum += t.GetRetValue().AsNumDouble()
|
|
n++
|
|
}
|
|
area.Skip(1)
|
|
scanned++
|
|
}
|
|
if n == 0 {
|
|
t.RetDouble(0, 10, 2)
|
|
return
|
|
}
|
|
t.RetDouble(sum/float64(n), 10, 2)
|
|
}
|
|
|
|
// rtlDbCopy implements __dbCopy(cFile, aFields, bFor, bWhile, nNext,
|
|
// xRec, lRest) — copy visible records from the current workarea into a
|
|
// freshly created DBF. Field projection: an empty/missing aFields
|
|
// copies the whole structure; otherwise only fields whose names match
|
|
// (case-insensitive) are carried over. Used by `COPY TO <f> [FIELDS]
|
|
// [FOR] [WHILE] [NEXT] [RECORD] [REST] [ALL]` in std.ch.
|
|
//
|
|
// Harbour's __dbCopy also accepts cRDD / nConnection / cCodepage / xDelim
|
|
// (params 8..11). Five only supports DBFNTX→DBFNTX for now; SDF/DELIMITED
|
|
// copies stay parser no-ops until that backend lands.
|
|
func rtlDbCopy(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
srcArea := wam.Current()
|
|
if srcArea == nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
|
|
if nParams < 1 || t.Local(1).IsNil() {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
cFile := t.Local(1).AsString()
|
|
if cFile == "" {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
|
|
// Field projection. Harbour passes `{ <(fields)> }` so each entry
|
|
// is a string literal already; uppercase for case-insensitive
|
|
// matching against the source's field names.
|
|
var srcIdx []int
|
|
var dstFields []hbrdd.FieldInfo
|
|
nSrcFields := srcArea.FieldCount()
|
|
useAll := true
|
|
if nParams >= 2 && t.Local(2).IsArray() {
|
|
arr := t.Local(2).AsArray()
|
|
if arr != nil && len(arr.Items) > 0 {
|
|
useAll = false
|
|
wanted := make(map[string]struct{}, len(arr.Items))
|
|
for _, it := range arr.Items {
|
|
s := strings.ToUpper(strings.TrimSpace(it.AsString()))
|
|
if s != "" {
|
|
wanted[s] = struct{}{}
|
|
}
|
|
}
|
|
for i := 0; i < nSrcFields; i++ {
|
|
fi := srcArea.GetFieldInfo(i)
|
|
if _, ok := wanted[strings.ToUpper(fi.Name)]; ok {
|
|
srcIdx = append(srcIdx, i)
|
|
dstFields = append(dstFields, fi)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if useAll {
|
|
srcIdx = make([]int, nSrcFields)
|
|
dstFields = make([]hbrdd.FieldInfo, nSrcFields)
|
|
for i := 0; i < nSrcFields; i++ {
|
|
srcIdx[i] = i
|
|
dstFields[i] = srcArea.GetFieldInfo(i)
|
|
}
|
|
}
|
|
if len(dstFields) == 0 {
|
|
// Nothing to copy — empty FIELDS list with no matches.
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
|
|
// Loop bounds — same shape as dbEval.
|
|
var bFor, bWhile hbrt.Value
|
|
if nParams >= 3 {
|
|
bFor = t.Local(3)
|
|
}
|
|
if nParams >= 4 {
|
|
bWhile = t.Local(4)
|
|
}
|
|
nCount := -1
|
|
if nParams >= 5 && !t.Local(5).IsNil() {
|
|
nCount = t.Local(5).AsInt()
|
|
}
|
|
if nParams >= 6 && !t.Local(6).IsNil() {
|
|
srcArea.GoTo(uint32(t.Local(6).AsInt()))
|
|
}
|
|
lRest := false
|
|
if nParams >= 7 && !t.Local(7).IsNil() {
|
|
lRest = t.Local(7).AsBool()
|
|
}
|
|
if !lRest && (nParams < 6 || t.Local(6).IsNil()) {
|
|
srcArea.GoTop()
|
|
}
|
|
|
|
// Create + open the destination. Use a temp alias so we don't
|
|
// clash with whatever the caller may have open under a name
|
|
// matching the file's basename.
|
|
drv, err := hbrdd.GetDriver("DBFNTX")
|
|
if err != nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
if _, err := drv.Create(hbrdd.CreateParams{Path: cFile, Fields: dstFields}); err != nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
srcSel := wam.CurrentNum()
|
|
dstSel, err := wam.Open("DBFNTX", cFile, "__copytmp", false, false)
|
|
if err != nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
dstArea := wam.AreaAt(dstSel)
|
|
wam.SelectByNum(srcSel)
|
|
|
|
scanned := 0
|
|
for !srcArea.EOF() {
|
|
if nCount >= 0 && scanned >= nCount {
|
|
break
|
|
}
|
|
if bWhile.IsBlock() {
|
|
t.PendingParams2(0)
|
|
bWhile.AsBlock().Fn(t)
|
|
if !t.GetRetValue().AsBool() {
|
|
break
|
|
}
|
|
}
|
|
emit := true
|
|
if bFor.IsBlock() {
|
|
t.PendingParams2(0)
|
|
bFor.AsBlock().Fn(t)
|
|
emit = t.GetRetValue().AsBool()
|
|
}
|
|
if emit {
|
|
vals := make([]hbrt.Value, len(srcIdx))
|
|
for i, idx := range srcIdx {
|
|
v, _ := srcArea.GetValue(idx)
|
|
vals[i] = v
|
|
}
|
|
wam.SelectByNum(dstSel)
|
|
dstArea.Append()
|
|
for i, v := range vals {
|
|
dstArea.PutValue(i, v)
|
|
}
|
|
wam.SelectByNum(srcSel)
|
|
}
|
|
srcArea.Skip(1)
|
|
scanned++
|
|
}
|
|
|
|
// Close the destination, leaving the source selected as on entry.
|
|
wam.SelectByNum(dstSel)
|
|
wam.Close()
|
|
wam.SelectByNum(srcSel)
|
|
t.RetBool(true)
|
|
}
|
|
|
|
// rtlDbSort implements __dbSort(cFile, aFields, bFor, bWhile, nNext,
|
|
// xRec, lRest) — same loop semantics as __dbCopy but the visible
|
|
// records are buffered, sorted by the named keys, and written in
|
|
// order. Each entry of aFields may be a plain field name (ascending)
|
|
// or `name/D` for descending. Unrecognized suffixes are ignored.
|
|
//
|
|
// Used by `SORT TO <f> [ON <field-list>] [FOR/WHILE/NEXT/...]` in
|
|
// std.ch.
|
|
func rtlDbSort(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
srcArea := wam.Current()
|
|
if srcArea == nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
|
|
if nParams < 1 || t.Local(1).IsNil() {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
cFile := t.Local(1).AsString()
|
|
if cFile == "" {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
|
|
nSrcFields := srcArea.FieldCount()
|
|
dstFields := make([]hbrdd.FieldInfo, nSrcFields)
|
|
for i := 0; i < nSrcFields; i++ {
|
|
dstFields[i] = srcArea.GetFieldInfo(i)
|
|
}
|
|
if nSrcFields == 0 {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
|
|
// Parse sort-key spec: name[/D] entries → (fieldIdx, descending).
|
|
type sortKey struct {
|
|
idx int
|
|
desc bool
|
|
}
|
|
var keys []sortKey
|
|
if nParams >= 2 && t.Local(2).IsArray() {
|
|
for _, item := range t.Local(2).AsArray().Items {
|
|
s := strings.TrimSpace(item.AsString())
|
|
if s == "" {
|
|
continue
|
|
}
|
|
desc := false
|
|
// Suffix `/D` (descending), `/A` (ascending), `/C`
|
|
// (case-insensitive — treated as ascending).
|
|
if i := strings.LastIndexByte(s, '/'); i > 0 {
|
|
suffix := strings.ToUpper(strings.TrimSpace(s[i+1:]))
|
|
switch suffix {
|
|
case "D":
|
|
desc = true
|
|
}
|
|
s = strings.TrimSpace(s[:i])
|
|
}
|
|
upper := strings.ToUpper(s)
|
|
idx := -1
|
|
for k := 0; k < nSrcFields; k++ {
|
|
if strings.EqualFold(dstFields[k].Name, upper) {
|
|
idx = k
|
|
break
|
|
}
|
|
}
|
|
if idx >= 0 {
|
|
keys = append(keys, sortKey{idx: idx, desc: desc})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Loop bounds.
|
|
var bFor, bWhile hbrt.Value
|
|
if nParams >= 3 {
|
|
bFor = t.Local(3)
|
|
}
|
|
if nParams >= 4 {
|
|
bWhile = t.Local(4)
|
|
}
|
|
nCount := -1
|
|
if nParams >= 5 && !t.Local(5).IsNil() {
|
|
nCount = t.Local(5).AsInt()
|
|
}
|
|
if nParams >= 6 && !t.Local(6).IsNil() {
|
|
srcArea.GoTo(uint32(t.Local(6).AsInt()))
|
|
}
|
|
lRest := false
|
|
if nParams >= 7 && !t.Local(7).IsNil() {
|
|
lRest = t.Local(7).AsBool()
|
|
}
|
|
if !lRest && (nParams < 6 || t.Local(6).IsNil()) {
|
|
srcArea.GoTop()
|
|
}
|
|
|
|
// Buffer visible records' field values.
|
|
var rows [][]hbrt.Value
|
|
scanned := 0
|
|
for !srcArea.EOF() {
|
|
if nCount >= 0 && scanned >= nCount {
|
|
break
|
|
}
|
|
if bWhile.IsBlock() {
|
|
t.PendingParams2(0)
|
|
bWhile.AsBlock().Fn(t)
|
|
if !t.GetRetValue().AsBool() {
|
|
break
|
|
}
|
|
}
|
|
emit := true
|
|
if bFor.IsBlock() {
|
|
t.PendingParams2(0)
|
|
bFor.AsBlock().Fn(t)
|
|
emit = t.GetRetValue().AsBool()
|
|
}
|
|
if emit {
|
|
row := make([]hbrt.Value, nSrcFields)
|
|
for i := 0; i < nSrcFields; i++ {
|
|
v, _ := srcArea.GetValue(i)
|
|
row[i] = v
|
|
}
|
|
rows = append(rows, row)
|
|
}
|
|
srcArea.Skip(1)
|
|
scanned++
|
|
}
|
|
|
|
// Sort if any keys were given. Stable so equal keys keep input
|
|
// order. Comparison is type-aware: numeric by AsNumDouble, date by
|
|
// AsNumInt julian, logical by truth, otherwise string.
|
|
if len(keys) > 0 && len(rows) > 1 {
|
|
less := func(i, j int) bool {
|
|
for _, k := range keys {
|
|
a := rows[i][k.idx]
|
|
b := rows[j][k.idx]
|
|
cmp := compareValues(a, b)
|
|
if cmp == 0 {
|
|
continue
|
|
}
|
|
if k.desc {
|
|
cmp = -cmp
|
|
}
|
|
return cmp < 0
|
|
}
|
|
return false
|
|
}
|
|
stableSort(rows, less)
|
|
}
|
|
|
|
// Create + open destination, then append in order.
|
|
drv, err := hbrdd.GetDriver("DBFNTX")
|
|
if err != nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
if _, err := drv.Create(hbrdd.CreateParams{Path: cFile, Fields: dstFields}); err != nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
srcSel := wam.CurrentNum()
|
|
dstSel, err := wam.Open("DBFNTX", cFile, "__sorttmp", false, false)
|
|
if err != nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
dstArea := wam.AreaAt(dstSel)
|
|
for _, row := range rows {
|
|
dstArea.Append()
|
|
for i, v := range row {
|
|
dstArea.PutValue(i, v)
|
|
}
|
|
}
|
|
wam.Close()
|
|
wam.SelectByNum(srcSel)
|
|
t.RetBool(true)
|
|
}
|
|
|
|
// rtlDbList implements __dbList(lOff, aBlocks, lAll, bFor, bWhile,
|
|
// nNext, nRec, lRest, lPrn, cFile) — output visible records to
|
|
// stdout. aBlocks is an array of column-evaluation code blocks (one
|
|
// per LIST / DISPLAY column expression). If aBlocks is empty or
|
|
// contains only NIL placeholders, every field of the current
|
|
// workarea is emitted.
|
|
//
|
|
// Used by both `LIST [<v,...>]` and `DISPLAY [<v,...>]` in std.ch.
|
|
// lAll distinguishes them: LIST always passes .T. (all matching
|
|
// records); DISPLAY passes .T. only for `DISPLAY ALL`, otherwise .F.
|
|
// (just the current record).
|
|
//
|
|
// TO PRINTER / TO FILE redirection (lPrn / cFile) is accepted but
|
|
// not yet implemented — both paths still write to stdout. OFF (lOff)
|
|
// suppresses the record-number prefix.
|
|
func rtlDbList(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
srcArea := wam.Current()
|
|
if srcArea == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
|
|
lOff := false
|
|
if nParams >= 1 && !t.Local(1).IsNil() {
|
|
lOff = t.Local(1).AsBool()
|
|
}
|
|
|
|
// Decode column blocks. Empty / `{ NIL }` → fall back to "all fields".
|
|
var blocks []hbrt.Value
|
|
useAllFields := true
|
|
if nParams >= 2 && t.Local(2).IsArray() {
|
|
arr := t.Local(2).AsArray()
|
|
if arr != nil {
|
|
for _, it := range arr.Items {
|
|
if it.IsBlock() {
|
|
blocks = append(blocks, it)
|
|
useAllFields = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
lAll := true
|
|
if nParams >= 3 && !t.Local(3).IsNil() {
|
|
lAll = t.Local(3).AsBool()
|
|
}
|
|
|
|
// Loop bounds — same shape as dbEval.
|
|
var bFor, bWhile hbrt.Value
|
|
if nParams >= 4 {
|
|
bFor = t.Local(4)
|
|
}
|
|
if nParams >= 5 {
|
|
bWhile = t.Local(5)
|
|
}
|
|
nCount := -1
|
|
if nParams >= 6 && !t.Local(6).IsNil() {
|
|
nCount = t.Local(6).AsInt()
|
|
}
|
|
if nParams >= 7 && !t.Local(7).IsNil() {
|
|
srcArea.GoTo(uint32(t.Local(7).AsInt()))
|
|
}
|
|
lRest := false
|
|
if nParams >= 8 && !t.Local(8).IsNil() {
|
|
lRest = t.Local(8).AsBool()
|
|
}
|
|
// DISPLAY without ALL emits exactly one record; LIST always emits
|
|
// the full filtered range. Encode the difference by clamping
|
|
// nCount to 1 when lAll is false and no explicit NEXT was given.
|
|
if !lAll && nCount < 0 {
|
|
nCount = 1
|
|
}
|
|
if !lRest && lAll && (nParams < 7 || t.Local(7).IsNil()) {
|
|
srcArea.GoTop()
|
|
}
|
|
|
|
nFields := srcArea.FieldCount()
|
|
scanned := 0
|
|
for !srcArea.EOF() {
|
|
if nCount >= 0 && scanned >= nCount {
|
|
break
|
|
}
|
|
if bWhile.IsBlock() {
|
|
t.PendingParams2(0)
|
|
bWhile.AsBlock().Fn(t)
|
|
if !t.GetRetValue().AsBool() {
|
|
break
|
|
}
|
|
}
|
|
emit := true
|
|
if bFor.IsBlock() {
|
|
t.PendingParams2(0)
|
|
bFor.AsBlock().Fn(t)
|
|
emit = t.GetRetValue().AsBool()
|
|
}
|
|
if emit {
|
|
parts := []string{}
|
|
if !lOff {
|
|
parts = append(parts, fmt.Sprintf("%6d", srcArea.RecNo()))
|
|
}
|
|
if useAllFields {
|
|
for i := 0; i < nFields; i++ {
|
|
v, _ := srcArea.GetValue(i)
|
|
parts = append(parts, valueToDisplay(v))
|
|
}
|
|
} else {
|
|
for _, blk := range blocks {
|
|
t.PendingParams2(0)
|
|
blk.AsBlock().Fn(t)
|
|
parts = append(parts, valueToDisplay(t.GetRetValue()))
|
|
}
|
|
}
|
|
fmt.Print("\r\n" + strings.Join(parts, " "))
|
|
}
|
|
srcArea.Skip(1)
|
|
scanned++
|
|
}
|
|
t.RetNil()
|
|
}
|
|
|
|
// rtlDbTotal implements __dbTotal(cFile, bKey, aFields, bFor, bWhile,
|
|
// nNext, xRec, lRest) — emit one record per *consecutive* run of
|
|
// equal key values from the current workarea, summing the named
|
|
// numeric fields and copying every other field from the run's first
|
|
// record. The source must already be sorted/indexed on the key for
|
|
// the grouping to produce one row per distinct value (Harbour's
|
|
// dbtotal.prg has the same precondition).
|
|
//
|
|
// Used by `TOTAL TO <f> ON <key> [FIELDS <list>] [FOR/WHILE/...]` in
|
|
// std.ch.
|
|
func rtlDbTotal(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
srcArea := wam.Current()
|
|
if srcArea == nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
|
|
if nParams < 1 || t.Local(1).IsNil() {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
cFile := t.Local(1).AsString()
|
|
if cFile == "" {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
|
|
bKey := t.Local(2)
|
|
if !bKey.IsBlock() {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
|
|
// Build dst struct — drop memos like Harbour does.
|
|
nFields := srcArea.FieldCount()
|
|
var dstFields []hbrdd.FieldInfo
|
|
var keptIdx []int
|
|
for i := 0; i < nFields; i++ {
|
|
fi := srcArea.GetFieldInfo(i)
|
|
if fi.Type == 'M' {
|
|
continue
|
|
}
|
|
dstFields = append(dstFields, fi)
|
|
keptIdx = append(keptIdx, i)
|
|
}
|
|
if len(dstFields) == 0 {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
|
|
// Resolve sum-fields → list of (dstIdx, srcIdx) pairs preserving
|
|
// declaration order.
|
|
type sumPair struct{ dst, src int }
|
|
var sums []sumPair
|
|
if nParams >= 3 && t.Local(3).IsArray() {
|
|
arr := t.Local(3).AsArray()
|
|
if arr != nil {
|
|
wanted := map[string]struct{}{}
|
|
for _, it := range arr.Items {
|
|
s := strings.ToUpper(strings.TrimSpace(it.AsString()))
|
|
if s != "" {
|
|
wanted[s] = struct{}{}
|
|
}
|
|
}
|
|
for di, si := range keptIdx {
|
|
if _, ok := wanted[strings.ToUpper(dstFields[di].Name)]; ok {
|
|
sums = append(sums, sumPair{dst: di, src: si})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Loop bounds.
|
|
var bFor, bWhile hbrt.Value
|
|
if nParams >= 4 {
|
|
bFor = t.Local(4)
|
|
}
|
|
if nParams >= 5 {
|
|
bWhile = t.Local(5)
|
|
}
|
|
nCount := -1
|
|
if nParams >= 6 && !t.Local(6).IsNil() {
|
|
nCount = t.Local(6).AsInt()
|
|
}
|
|
if nParams >= 7 && !t.Local(7).IsNil() {
|
|
srcArea.GoTo(uint32(t.Local(7).AsInt()))
|
|
}
|
|
lRest := false
|
|
if nParams >= 8 && !t.Local(8).IsNil() {
|
|
lRest = t.Local(8).AsBool()
|
|
}
|
|
if !lRest && (nParams < 7 || t.Local(7).IsNil()) {
|
|
srcArea.GoTop()
|
|
}
|
|
|
|
// Create + open destination.
|
|
drv, err := hbrdd.GetDriver("DBFNTX")
|
|
if err != nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
if _, err := drv.Create(hbrdd.CreateParams{Path: cFile, Fields: dstFields}); err != nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
srcSel := wam.CurrentNum()
|
|
dstSel, err := wam.Open("DBFNTX", cFile, "__totaltmp", false, false)
|
|
if err != nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
dstArea := wam.AreaAt(dstSel)
|
|
wam.SelectByNum(srcSel)
|
|
|
|
// Group walk.
|
|
var prevKey hbrt.Value
|
|
haveGroup := false
|
|
running := make([]float64, len(sums))
|
|
|
|
flush := func() {
|
|
if !haveGroup {
|
|
return
|
|
}
|
|
wam.SelectByNum(dstSel)
|
|
// The dst row for this group is the most recent append.
|
|
// Overwrite the sum-field positions with the accumulated total,
|
|
// preserving the field's declared length/decimals.
|
|
for i, sp := range sums {
|
|
fi := dstFields[sp.dst]
|
|
dstArea.PutValue(sp.dst, hbrt.MakeDouble(running[i], uint16(fi.Len), uint16(fi.Dec)))
|
|
}
|
|
wam.SelectByNum(srcSel)
|
|
for i := range running {
|
|
running[i] = 0
|
|
}
|
|
haveGroup = false
|
|
}
|
|
|
|
scanned := 0
|
|
for !srcArea.EOF() {
|
|
if nCount >= 0 && scanned >= nCount {
|
|
break
|
|
}
|
|
if bWhile.IsBlock() {
|
|
t.PendingParams2(0)
|
|
bWhile.AsBlock().Fn(t)
|
|
if !t.GetRetValue().AsBool() {
|
|
break
|
|
}
|
|
}
|
|
match := true
|
|
if bFor.IsBlock() {
|
|
t.PendingParams2(0)
|
|
bFor.AsBlock().Fn(t)
|
|
match = t.GetRetValue().AsBool()
|
|
}
|
|
if match {
|
|
t.PendingParams2(0)
|
|
bKey.AsBlock().Fn(t)
|
|
curKey := t.GetRetValue()
|
|
|
|
if !haveGroup || compareValues(prevKey, curKey) != 0 {
|
|
flush()
|
|
// Append a fresh dst row and copy this first-of-group
|
|
// record's non-memo fields into it.
|
|
wam.SelectByNum(dstSel)
|
|
dstArea.Append()
|
|
wam.SelectByNum(srcSel)
|
|
for di, si := range keptIdx {
|
|
v, _ := srcArea.GetValue(si)
|
|
wam.SelectByNum(dstSel)
|
|
dstArea.PutValue(di, v)
|
|
wam.SelectByNum(srcSel)
|
|
}
|
|
prevKey = curKey
|
|
haveGroup = true
|
|
}
|
|
|
|
// Sum this record's contribution.
|
|
for i, sp := range sums {
|
|
v, _ := srcArea.GetValue(sp.src)
|
|
running[i] += v.AsNumDouble()
|
|
}
|
|
}
|
|
srcArea.Skip(1)
|
|
scanned++
|
|
}
|
|
flush()
|
|
|
|
wam.SelectByNum(dstSel)
|
|
wam.Close()
|
|
wam.SelectByNum(srcSel)
|
|
t.RetBool(true)
|
|
}
|
|
|
|
// stableSort is a tiny insertion sort for small N (typical DBF SORT
|
|
// targets are interactive datasets). Avoids a sort import dependency.
|
|
func stableSort(rows [][]hbrt.Value, less func(i, j int) bool) {
|
|
for i := 1; i < len(rows); i++ {
|
|
for j := i; j > 0 && less(j, j-1); j-- {
|
|
rows[j], rows[j-1] = rows[j-1], rows[j]
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- DBSETFILTER / DBCLEARFILTER / DBFILTER ---
|
|
|
|
// DBSETFILTER(bCondition [, cCondition])
|
|
func rtlDbSetFilter(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
|
|
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.EndProcFast()
|
|
|
|
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.EndProcFast()
|
|
|
|
// TODO: return stored filter expression from area
|
|
t.RetString("")
|
|
}
|