Cumulative season's silent-bug hunting (~62 fixes) across the FiveSql2 SQL engine, the Five compiler/runtime, and the hbrdd RDD layer. Saved as a single checkpoint before refactoring the parser to delegate xBase command translation to the preprocessor. Highlights: FiveSql2 engine (_FiveSql2/src/) - prefix-glob index attach -> explicit convention (<table>_pk.ntx, <table>_uq.ntx, <table>.cdx) — fixes silent multi-row INSERT row-drop - DROP/CREATE TABLE FErase chain extended (.cdx, .fsc, .fsv, .dbt, .fpt) - COUNT(DISTINCT col) parsed + aggregated via hSeen hash - UNION column-count mismatch returns SQL_ERR_GRAMMAR (was silent) - DISTINCT + ORDER BY hidden-col leak fixed (trim before DISTINCT) - Derived table FROM (SELECT...) + JOIN right-side derived - Self-FK CASCADE depth 2+ via SqlGetSingleColPK pre-collect - LAG/LEAD default arg uses SqlEvalRowExpr (handles -N const exprs) - DATE literal round-trip validation (Feb 29 non-leap rejected) - CREATE OR REPLACE VIEW; CREATE VIEW errors on already-exists - AlterTable type dispatcher comma-wrapped (1-char type "A" no longer matches CHARACTER) Compiler / runtime - gengo: HB_ -> FV_ prefix on emitted Go function names (Five identity) - gengo split: emit_block.go, emit_stmt.go, folding.go extracted - parser/stmtreg.go nudges - hbrt: debug TUI/CLI restructure (debugcmd, debugkey, termios_*), windows debug stubs collapsed - thread/vm/value/class/pcinterp tightening from panic traces RDD layer (hbrdd/) - dbf: null bitmap support (null.go + null_test.go), mmap split (mmap_posix.go / mmap_windows.go), byte-level numeric parse - ntx/cdx: windows mmap parity - workarea + mem RDD: cross-area state-bleed fixes RTL (hbrtl/) - errorlog rewrite with platform-specific FD (errorlog_fd_unix / errorlog_fd_other) - sqlscan, sqlhelpers, indexrtl, datetime extensions Gates green at checkpoint: - go test ./... : PASS - FiveSql2 SQL:1999 : 43/43 - Harbour compat : 56/56 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
495 lines
11 KiB
Go
495 lines
11 KiB
Go
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
|
// All rights reserved.
|
|
|
|
// Date and time functions for the Five runtime library.
|
|
// Complete Harbour date system: SET DATE, SET EPOCH, SET CENTURY,
|
|
// and all date conversion/extraction functions.
|
|
//
|
|
// Harbour date defaults:
|
|
// SET DATE FORMAT "MM/DD/YY" (American)
|
|
// SET EPOCH 1900 (2-digit year base)
|
|
// SET CENTURY OFF (show 2-digit year)
|
|
//
|
|
// Reference: /mnt/d/harbour-core/src/rtl/dates.c, dateshb.c, set.c
|
|
package hbrtl
|
|
|
|
import (
|
|
"five/hbrt"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// --- Date SET system ---
|
|
|
|
var (
|
|
setDateFormat = "MM/DD/YY" // Harbour default: American
|
|
setEpoch = 1900 // Harbour default: 2-digit year → 1900-1999
|
|
setCentury = false // Harbour default: OFF (2-digit year display)
|
|
)
|
|
|
|
// SetDateFormat changes date display format.
|
|
// Harbour: SET DATE FORMAT TO "YYYY-MM-DD" etc.
|
|
// Predefined: AMERICAN="MM/DD/YY", ANSI="YY.MM.DD", BRITISH="DD/MM/YY",
|
|
// FRENCH="DD/MM/YY", GERMAN="DD.MM.YY", ITALIAN="DD-MM-YY",
|
|
// JAPANESE="YY/MM/DD", USA="MM-DD-YY"
|
|
func SetDateFormat(format string) {
|
|
setDateFormat = format
|
|
// Auto-detect century
|
|
setCentury = strings.Contains(format, "YYYY")
|
|
}
|
|
|
|
// SetDateFormatRTL is the RTL wrapper.
|
|
func rtlSetDateFormat(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProc()
|
|
SetDateFormat(t.Local(1).AsString())
|
|
t.RetNil()
|
|
}
|
|
|
|
// SetDatePreset sets date format by name.
|
|
func rtlSetDate(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProc()
|
|
name := strings.ToUpper(t.Local(1).AsString())
|
|
switch name {
|
|
case "AMERICAN", "USA":
|
|
setDateFormat = "MM/DD/YY"
|
|
case "ANSI":
|
|
setDateFormat = "YY.MM.DD"
|
|
case "BRITISH", "FRENCH":
|
|
setDateFormat = "DD/MM/YY"
|
|
case "GERMAN":
|
|
setDateFormat = "DD.MM.YY"
|
|
case "ITALIAN":
|
|
setDateFormat = "DD-MM-YY"
|
|
case "JAPANESE":
|
|
setDateFormat = "YY/MM/DD"
|
|
}
|
|
setCentury = strings.Contains(setDateFormat, "YYYY")
|
|
t.RetNil()
|
|
}
|
|
|
|
// SET EPOCH nYear
|
|
func rtlSetEpoch(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProc()
|
|
setEpoch = int(t.Local(1).AsNumInt())
|
|
t.RetNil()
|
|
}
|
|
|
|
// SET CENTURY ON/OFF
|
|
func rtlSetCentury(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProc()
|
|
if nParams > 0 {
|
|
v := t.Local(1)
|
|
if v.IsLogical() {
|
|
setCentury = v.AsBool()
|
|
} else if v.IsString() {
|
|
setCentury = strings.ToUpper(v.AsString()) == "ON"
|
|
}
|
|
}
|
|
// Update format to match century setting
|
|
if setCentury && !strings.Contains(setDateFormat, "YYYY") {
|
|
setDateFormat = strings.Replace(setDateFormat, "YY", "YYYY", 1)
|
|
} else if !setCentury && strings.Contains(setDateFormat, "YYYY") {
|
|
setDateFormat = strings.Replace(setDateFormat, "YYYY", "YY", 1)
|
|
}
|
|
t.RetNil()
|
|
}
|
|
|
|
// --- Date display ---
|
|
|
|
// julianToDateStr converts Julian day to formatted date string using current SET DATE.
|
|
func julianToDateStr(julian int64) string {
|
|
if julian <= 0 {
|
|
// Empty date: return blank in current format width
|
|
w := len(formatDate(2000, 1, 1, setDateFormat))
|
|
return strings.Repeat(" ", w)
|
|
}
|
|
y, m, d := julianToDate(julian)
|
|
return formatDate(y, m, d, setDateFormat)
|
|
}
|
|
|
|
// formatDate formats a date according to format string.
|
|
func formatDate(y, m, d int, format string) string {
|
|
result := format
|
|
if strings.Contains(result, "YYYY") {
|
|
result = strings.Replace(result, "YYYY", fmt.Sprintf("%04d", y), 1)
|
|
} else if strings.Contains(result, "YY") {
|
|
result = strings.Replace(result, "YY", fmt.Sprintf("%02d", y%100), 1)
|
|
}
|
|
if strings.Contains(result, "MM") {
|
|
result = strings.Replace(result, "MM", fmt.Sprintf("%02d", m), 1)
|
|
}
|
|
if strings.Contains(result, "DD") {
|
|
result = strings.Replace(result, "DD", fmt.Sprintf("%02d", d), 1)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// --- Date functions ---
|
|
|
|
// Date returns current system date.
|
|
func Date(t *hbrt.Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProc()
|
|
now := time.Now()
|
|
t.PushValue(hbrt.MakeDate(dateToJulian(now.Year(), int(now.Month()), now.Day())))
|
|
t.RetValue()
|
|
}
|
|
|
|
// Time returns current time as "HH:MM:SS".
|
|
func Time(t *hbrt.Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProc()
|
|
t.PushString(time.Now().Format("15:04:05"))
|
|
t.RetValue()
|
|
}
|
|
|
|
// Year extracts year from date.
|
|
func Year(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProc()
|
|
v := t.Local(1)
|
|
if v.IsDateTime() {
|
|
y, _, _ := julianToDate(v.AsJulian())
|
|
t.RetInt(int64(y))
|
|
} else {
|
|
t.RetInt(0)
|
|
}
|
|
}
|
|
|
|
// Month extracts month from date.
|
|
func Month(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProc()
|
|
v := t.Local(1)
|
|
if v.IsDateTime() {
|
|
_, m, _ := julianToDate(v.AsJulian())
|
|
t.RetInt(int64(m))
|
|
} else {
|
|
t.RetInt(0)
|
|
}
|
|
}
|
|
|
|
// Day extracts day from date.
|
|
func Day(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProc()
|
|
v := t.Local(1)
|
|
if v.IsDateTime() {
|
|
_, _, d := julianToDate(v.AsJulian())
|
|
t.RetInt(int64(d))
|
|
} else {
|
|
t.RetInt(0)
|
|
}
|
|
}
|
|
|
|
// DOW returns day of week (1=Sunday .. 7=Saturday).
|
|
func DOW(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProc()
|
|
v := t.Local(1)
|
|
if v.IsDateTime() {
|
|
dow := (v.AsJulian() + 2) % 7 // Julian day 0 = Monday
|
|
if dow <= 0 {
|
|
dow += 7
|
|
}
|
|
t.RetInt(dow)
|
|
} else {
|
|
t.RetInt(0)
|
|
}
|
|
}
|
|
|
|
// Seconds returns seconds since midnight.
|
|
func Seconds(t *hbrt.Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProc()
|
|
now := time.Now()
|
|
secs := float64(now.Hour()*3600+now.Minute()*60+now.Second()) + float64(now.Nanosecond())/1e9
|
|
t.PushValue(hbrt.MakeDoubleAuto(secs))
|
|
t.RetValue()
|
|
}
|
|
|
|
// DToC converts date to character using current SET DATE FORMAT.
|
|
func DToC(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProc()
|
|
v := t.Local(1)
|
|
if v.IsDateTime() {
|
|
t.PushString(julianToDateStr(v.AsJulian()))
|
|
} else {
|
|
t.PushString("")
|
|
}
|
|
t.RetValue()
|
|
}
|
|
|
|
// DToS converts date to YYYYMMDD string (fixed format, always 8 chars).
|
|
func DToS(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProc()
|
|
v := t.Local(1)
|
|
if v.IsDateTime() {
|
|
y, m, d := julianToDate(v.AsJulian())
|
|
t.PushString(fmt.Sprintf("%04d%02d%02d", y, m, d))
|
|
} else {
|
|
t.PushString(" ")
|
|
}
|
|
t.RetValue()
|
|
}
|
|
|
|
// SToD converts YYYYMMDD string to date.
|
|
func SToD(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProc()
|
|
s := t.Local(1).AsString()
|
|
if len(s) >= 8 {
|
|
y := parseInt(s[0:4])
|
|
m := parseInt(s[4:6])
|
|
d := parseInt(s[6:8])
|
|
if y > 0 && m >= 1 && m <= 12 && d >= 1 && d <= 31 {
|
|
t.PushValue(hbrt.MakeDate(dateToJulian(y, m, d)))
|
|
t.RetValue()
|
|
return
|
|
}
|
|
}
|
|
t.PushValue(hbrt.MakeDate(0))
|
|
t.RetValue()
|
|
}
|
|
|
|
// CToD converts character to date using current SET DATE FORMAT.
|
|
// Harbour: CToD("09/18/92") with SET DATE AMERICAN
|
|
//
|
|
// Pre-pass: unambiguous ISO forms (YYYY-MM-DD, YYYY.MM.DD, YYYY/MM/DD,
|
|
// and bare YYYYMMDD) parse directly regardless of SET DATE. Those
|
|
// formats have a fixed component order, so skipping the setDateFormat
|
|
// lookup lets code that uses ISO dates work without a SET DATE ANSI
|
|
// call up front. Non-ISO strings fall through to the original
|
|
// format-aware path so American / European users keep their semantics.
|
|
func CToD(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProc()
|
|
s := strings.TrimSpace(t.Local(1).AsString())
|
|
if len(s) == 0 {
|
|
t.PushValue(hbrt.MakeDate(0))
|
|
t.RetValue()
|
|
return
|
|
}
|
|
|
|
if y, m, d, ok := parseIsoDate(s); ok {
|
|
t.PushValue(hbrt.MakeDate(dateToJulian(y, m, d)))
|
|
t.RetValue()
|
|
return
|
|
}
|
|
|
|
// Parse according to current date format
|
|
y, m, d := parseDateByFormat(s, setDateFormat)
|
|
|
|
// Apply epoch for 2-digit years
|
|
if y >= 0 && y < 100 {
|
|
if y+setEpoch/100*100 < setEpoch {
|
|
y += (setEpoch/100 + 1) * 100
|
|
} else {
|
|
y += setEpoch / 100 * 100
|
|
}
|
|
}
|
|
|
|
if y > 0 && m >= 1 && m <= 12 && d >= 1 && d <= 31 {
|
|
t.PushValue(hbrt.MakeDate(dateToJulian(y, m, d)))
|
|
} else {
|
|
t.PushValue(hbrt.MakeDate(0))
|
|
}
|
|
t.RetValue()
|
|
}
|
|
|
|
// parseIsoDate accepts YYYY-MM-DD, YYYY/MM/DD, YYYY.MM.DD, and bare
|
|
// YYYYMMDD. Returns ok=false for anything else so the caller can fall
|
|
// back to the format-aware parse. The 4-digit year anchor disambiguates
|
|
// from 2-digit-year American / European layouts.
|
|
func parseIsoDate(s string) (y, m, d int, ok bool) {
|
|
switch len(s) {
|
|
case 10:
|
|
if s[4] != '-' && s[4] != '/' && s[4] != '.' {
|
|
return
|
|
}
|
|
if s[7] != s[4] {
|
|
return
|
|
}
|
|
if yy, e := strconv.Atoi(s[0:4]); e == nil {
|
|
y = yy
|
|
} else {
|
|
return
|
|
}
|
|
if mm, e := strconv.Atoi(s[5:7]); e == nil {
|
|
m = mm
|
|
} else {
|
|
return
|
|
}
|
|
if dd, e := strconv.Atoi(s[8:10]); e == nil {
|
|
d = dd
|
|
} else {
|
|
return
|
|
}
|
|
case 8:
|
|
if yy, e := strconv.Atoi(s[0:4]); e == nil {
|
|
y = yy
|
|
} else {
|
|
return
|
|
}
|
|
if mm, e := strconv.Atoi(s[4:6]); e == nil {
|
|
m = mm
|
|
} else {
|
|
return
|
|
}
|
|
if dd, e := strconv.Atoi(s[6:8]); e == nil {
|
|
d = dd
|
|
} else {
|
|
return
|
|
}
|
|
default:
|
|
return
|
|
}
|
|
if y > 0 && m >= 1 && m <= 12 && d >= 1 && d <= 31 {
|
|
ok = true
|
|
}
|
|
return
|
|
}
|
|
|
|
// parseDateByFormat parses a date string according to format.
|
|
func parseDateByFormat(s, format string) (y, m, d int) {
|
|
// Find positions of Y, M, D in format
|
|
yPos := strings.Index(format, "YY")
|
|
mPos := strings.Index(format, "MM")
|
|
dPos := strings.Index(format, "DD")
|
|
yLen := 2
|
|
if strings.Contains(format, "YYYY") {
|
|
yLen = 4
|
|
}
|
|
|
|
// Extract numeric parts from input (skip separators)
|
|
digits := extractDigits(s)
|
|
|
|
// Map digits to y/m/d based on format order
|
|
type part struct {
|
|
pos int
|
|
len int
|
|
val *int
|
|
}
|
|
parts := []part{
|
|
{yPos, yLen, &y},
|
|
{mPos, 2, &m},
|
|
{dPos, 2, &d},
|
|
}
|
|
// Sort by position
|
|
for i := 0; i < len(parts)-1; i++ {
|
|
for j := i + 1; j < len(parts); j++ {
|
|
if parts[j].pos < parts[i].pos {
|
|
parts[i], parts[j] = parts[j], parts[i]
|
|
}
|
|
}
|
|
}
|
|
|
|
off := 0
|
|
for _, p := range parts {
|
|
if off+p.len <= len(digits) {
|
|
*p.val = parseInt(digits[off : off+p.len])
|
|
off += p.len
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func extractDigits(s string) string {
|
|
var buf []byte
|
|
for _, c := range s {
|
|
if c >= '0' && c <= '9' {
|
|
buf = append(buf, byte(c))
|
|
}
|
|
}
|
|
return string(buf)
|
|
}
|
|
|
|
// CDoW returns day name.
|
|
func CDoW(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProc()
|
|
v := t.Local(1)
|
|
if v.IsDateTime() {
|
|
dow := (v.AsJulian() + 2) % 7
|
|
if dow <= 0 {
|
|
dow += 7
|
|
}
|
|
days := []string{"", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}
|
|
if dow >= 1 && dow <= 7 {
|
|
t.PushString(days[dow])
|
|
t.RetValue()
|
|
return
|
|
}
|
|
}
|
|
t.PushString("")
|
|
t.RetValue()
|
|
}
|
|
|
|
// CMonth returns month name.
|
|
func CMonth(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProc()
|
|
v := t.Local(1)
|
|
if v.IsDateTime() {
|
|
_, m, _ := julianToDate(v.AsJulian())
|
|
months := []string{"", "January", "February", "March", "April", "May", "June",
|
|
"July", "August", "September", "October", "November", "December"}
|
|
if m >= 1 && m <= 12 {
|
|
t.PushString(months[m])
|
|
t.RetValue()
|
|
return
|
|
}
|
|
}
|
|
t.PushString("")
|
|
t.RetValue()
|
|
}
|
|
|
|
// --- Julian date helpers ---
|
|
|
|
func dateToJulian(y, m, d int) int64 {
|
|
if m <= 2 {
|
|
y--
|
|
m += 12
|
|
}
|
|
a := y / 100
|
|
b := 2 - a + a/4
|
|
return int64(365.25*float64(y+4716)) + int64(30.6001*float64(m+1)) + int64(d+b) - 1524
|
|
}
|
|
|
|
func julianToDate(julian int64) (y, m, d int) {
|
|
if julian <= 0 {
|
|
return 0, 0, 0
|
|
}
|
|
l := julian + 68569
|
|
n := 4 * l / 146097
|
|
l = l - (146097*n+3)/4
|
|
i := 4000 * (l + 1) / 1461001
|
|
l = l - 1461*i/4 + 31
|
|
j := 80 * l / 2447
|
|
d = int(l - 2447*j/80)
|
|
l = j / 11
|
|
m = int(j + 2 - 12*l)
|
|
y = int(100*(n-49) + i + l)
|
|
return
|
|
}
|
|
|
|
func parseInt(s string) int {
|
|
n := 0
|
|
for _, c := range s {
|
|
if c >= '0' && c <= '9' {
|
|
n = n*10 + int(c-'0')
|
|
}
|
|
}
|
|
return n
|
|
}
|