diff --git a/compiler/parser/parser.go b/compiler/parser/parser.go index 27b482f..104f12c 100644 --- a/compiler/parser/parser.go +++ b/compiler/parser/parser.go @@ -1155,7 +1155,7 @@ func (p *Parser) parseIdentStmt() ast.Stmt { // rewritten by compiler/pp/std.ch into function calls before the // parser sees them. switch upper { - case "SORT", "TOTAL", "UPDATE", + case "TOTAL", "UPDATE", "LABEL", "REPORT", "ACCEPT", "INPUT", "JOIN", "RELEASE", "SAVE", "RESTORE", "DIR", "STORE", "NOTE", "TEXT", "ENDTEXT", diff --git a/compiler/pp/std.ch b/compiler/pp/std.ch index f5ccd01..d1831fe 100644 --- a/compiler/pp/std.ch +++ b/compiler/pp/std.ch @@ -73,6 +73,15 @@ __dbCopy( <(f)>, { <(fields)> }, ; <{for}>, <{while}>, , , <.rest.> ) +/* SORT TO copies the visible records into a fresh DBF in key order. + Each key in `` may carry `/D` for descending; default is + ascending. */ +#command SORT [TO <(f)>] [ON ] ; + [FOR ] [WHILE ] [NEXT ] ; + [RECORD ] [] [ALL] => ; + __dbSort( <(f)>, { <(fields)> }, ; + <{for}>, <{while}>, , , <.rest.> ) + /* --- bulk maintenance --- */ #command REINDEX => DbReindex() #command PACK => DbPack() diff --git a/hbrtl/database.go b/hbrtl/database.go index 631089a..3aaa950 100644 --- a/hbrtl/database.go +++ b/hbrtl/database.go @@ -931,6 +931,202 @@ func rtlDbCopy(t *hbrt.Thread) { 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 [ON ] [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) +} + +// 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]) diff --git a/hbrtl/register.go b/hbrtl/register.go index 8eefdf1..bc21db8 100644 --- a/hbrtl/register.go +++ b/hbrtl/register.go @@ -200,6 +200,7 @@ func RegisterRTL(vm *hbrt.VM) { hbrt.Sym("__DBCONTINUE", hbrt.FsPublic, rtlDbContinue), hbrt.Sym("__DBAVERAGE", hbrt.FsPublic, rtlDbAverage), hbrt.Sym("__DBCOPY", hbrt.FsPublic, rtlDbCopy), + hbrt.Sym("__DBSORT", hbrt.FsPublic, rtlDbSort), hbrt.Sym("DBSETFILTER", hbrt.FsPublic, rtlDbSetFilter), hbrt.Sym("DBCLEARFILTER", hbrt.FsPublic, rtlDbClearFilter), hbrt.Sym("DBFILTER", hbrt.FsPublic, rtlDbFilter),