// 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 }