PBIDEA:用 uo_datetime 做日期时间处理(格式化、加减、差值、时间戳与时区)
PBIDEA:用 uo_datetime 做日期时间处理(格式化、加减、差值、时间戳与时区)阅读说明
1. 适用版本:PB 10 / PB 12.5 + PBIDEA 1.x(本文全部示例在 PB 12.5 实机跑通;uo_datetime 本体只是一层 PbIdea.dll 外部函数声明,PB10 工程直接 import websuite.pbl 即可用,两版无 API 差异)
2. 支持数据库:本文不涉及数据库,纯内存计算
3. 操作系统与环境要求:Windows 7+;PbIdea.dll 需与可执行文件同目录或处于可搜索路径;工程需 import websuite.pbl
4. 难度系数:★★☆☆☆(会声明变量、会 create 对象即可跟做)
5. 其它阅读说明:uo_datetime 不是 autoinstantiate 对象,global uo_datetime uo_datetime 那句只是声明,必须自己 create / destroy;本文出现的所有输出值均为实机运行真实结果,不是推演
一、这是什么
PowerBuilder 原生的日期时间函数只有散落的几个:Today()、Now()、RelativeDate()、DaysAfter()、SecondsAfter()、String(dt,'yyyy-mm-dd')。真到业务里要算"上个月最后一天""两个时间差几小时""把 2023-05-18T12:02:04Z 这种串解析进来""转 Unix 时间戳给接口用",就得自己拼一堆相对日期和字符串截取,又啰嗦又容易错。
PBIDEA 的 uo_datetime 就是一个把 C++ std::chrono 包装成 PowerScript 对象的日期时间处理器:一个对象持有一个时间点,格式解析/输出、加减、截断、差值、时间戳、时区、月末、星期、年内天数全都有,还能一行 info() 把几十个字段灌进 uo_json。本文把它讲透——API 全貌、每个方法的真实行为、以及五个实机才暴露出来的坑。
二、先认清它的形态
uo_datetime 是 nonvisualobject,本体极薄:构造函数调 dtCreate(),析构函数调 dtDestroy(),其余全是 PbIdea.dll 的外部函数声明。这决定了三件事:
[*]必须 create。global uo_datetime uo_datetime 只是声明了一个全局同名变量,不做实例化。直接拿类名调用方法,编译能过、运行时必报 Null object reference。
[*]用完 destroy。add() / truncate() / MLastDay() 返回的是新对象,不是同一个引用,每个都要销毁。
[*]不依赖界面。纯计算,后台线程、DataStore 批处理、服务程序里都能跑,不需要窗口。
三、API 全貌(速查表)
以下方法名逐一对照 websuite.pbl 导出的 uo_datetime.sru 源码核实,无自编。
方法说明返回
ok()对象是否有效boolean
now()取系统当前时间datetime
Set(longlong ts, boolean millsecond)用 Unix 时间戳初始化(false=秒,true=毫秒)datetime
Set(date) / Set(time) / Set(datetime)用 PB 原生类型初始化datetime
Set(string)按 yyyy/mm/dd hh:mm:ss 风格解析datetime
Set(string, string fmt)按自定义格式解析datetime
format(string fmt)格式化输出string
add(string unit, long value)加/减(负数即减)uo_datetime
truncate(string unit)按单位把低位清零uo_datetime
diff(string unit, uo_datetime other)差值 = 本对象 − 参数对象long
timestamp(boolean millsecond)Unix 时间戳longlong
datetime() / date() / time()取 PB 原生值datetime / date / time
year() month() day() hour() minute() second() msecond()各时间分量int
yday()一年中的第几天(0 基)int
week()星期(1=周一 … 7=周日)int
MLastDay()所在月份的最后一天uo_datetime
SetGMT(string)设置时区,全局生效boolean
info(uo_json)把全部字段与常用格式结果导入 JSON—
unit 取值(大小写敏感,已逐个实机验证):y=年、m=月、d=日、h=时、M=分。注意小写 m 是月份、大写 M 才是分钟,写反了不会报错,只会得到一个莫名其妙的日期。
四、坑一:format 的格式串必须包在 {} 里
这是最容易踩的一条。看源码注释里写的是 %F、%T 这种 strftime 写法,很多人会直接:
ls = ldt.format('%Y-%m-%d %H:%M:%S') // 返回原样的 %Y-%m-%d %H:%M:%S
实机结果:原样返回,一个字都不展开。 正确写法是 fmt 库的 {:...} 语法:
ls = ldt.format('{:%Y-%m-%d %H:%M:%S}')
Set(字符串, 格式串) 的第二参数同样要带 {}(实测 Set('2023-05-18 12:02:04', '{:%Y-%m-%d %H:%M:%S}') 解析成功,返回 ok=true)。
常用格式符(实测输出,基准 2023/05/18 12:02:04):
格式输出格式输出
{:%F}2023-05-18{:%T}12:02:04
{:%Y}2023{:%m}05
{:%d}18{:%H}12
{:%M}02{:%S}04
{:%A}Thursday{:%a}Thu
{:%B}May{:%j}138
{:%p}PM{:%z}+0800
{:%F %T}2023-05-18 12:02:04{:%c}Thu May 18 12:02:04 2023
五、取当前时间
前置:工程已 import websuite.pbl,运行目录有 PbIdea.dll。把下面代码贴进任意按钮的 clicked 事件即可运行。
// 示例输入:无外部输入,取系统当前时间
// 输出格式:先拼成多行字符串,最后一次弹窗
uo_datetime ldt_now
string ls_out
// uo_datetime 不是自动实例化对象,必须显式 create
ldt_now = create uo_datetime
// now() 返回 PB 原生 datetime,再用 Set 灌回对象内部
ldt_now.Set(ldt_now.now())
ls_out = '对象有效:' + string(ldt_now.ok()) + '~r~n'
// 注意:格式串必须写成 {:%F} 的 fmt 语法,裸写 %F 会原样返回
ls_out = ls_out + '日期:' + ldt_now.format('{:%F}') + '~r~n'
ls_out = ls_out + '时间:' + ldt_now.format('{:%T}') + '~r~n'
ls_out = ls_out + '完整:' + ldt_now.format('{:%Y-%m-%d %H:%M:%S}')
destroy ldt_now
MessageBox('当前时间', ls_out)
实机输出:对象有效:true / 日期:2026-09-13 / 时间:06:29:38 / 完整:2026-09-13 06:29:38。
六、解析字符串与取时间分量
// 示例输入:待解析的日期时间串(默认格式 yyyy/mm/dd hh:mm:ss)
string ls_input
ls_input = '2023/05/18 12:02:04'
uo_datetime ldt
string ls_out
ldt = create uo_datetime
ldt.Set(ls_input)
ls_out = '年=' + string(ldt.year()) + ' 月=' + string(ldt.month()) + ' 日=' + string(ldt.day()) + '~r~n'
ls_out = ls_out + '时=' + string(ldt.hour()) + ' 分=' + string(ldt.minute()) + ' 秒=' + string(ldt.second()) + '~r~n'
ls_out = ls_out + '毫秒=' + string(ldt.msecond()) + ' 星期=' + string(ldt.week()) + '~r~n'
// 坑二:yday() 是 0 基的,{:%j} 才是 1 基的年内天数
ls_out = ls_out + '年内天数(0基)=' + string(ldt.yday()) + ' 年内天数(1基)=' + ldt.format('{:%j}') + '~r~n'
ls_out = ls_out + '星期名=' + ldt.format('{:%A}') + '~r~n'
ls_out = ls_out + 'date()=' + string(ldt.date()) + ' time()=' + string(ldt.time())
destroy ldt
MessageBox('时间分量', ls_out)
实机输出:年=2023 月=5 日=18 / 时=12 分=2 秒=4 / 毫秒=0 星期=4 / 年内天数(0基)=137 年内天数(1基)=138 / 星期名=Thursday / date()=2023-05-18 time()=12:02:04。
注意 month()、hour() 这些返回的是不带前导零的整数,要 "05" 这种定宽字符串请用 format('{:%m}')。date() / time() / datetime() 直接给出 PB 原生类型,可以直接塞进 DataWindow 的 SetItem。
七、加减、截断、月末、差值
// 示例输入:基准时间
string ls_base
ls_base = '2023/05/18 12:02:04'
// 示例输入:要加的天数
long ll_days
ll_days = 10
uo_datetime ldt_a
uo_datetime ldt_b
uo_datetime ldt_c
uo_datetime ldt_d
long ll_diff
string ls_out
ldt_a = create uo_datetime
ldt_a.Set(ls_base)
// add / truncate / MLastDay 返回的都是「新的」uo_datetime,原对象 ldt_a 不变
ldt_b = ldt_a.add('d', ll_days)
ldt_c = ldt_a.truncate('m') // 小写 m = 月
ldt_d = ldt_a.MLastDay()
// diff = 本对象 − 参数对象,单位由第一个参数指定
ll_diff = ldt_b.diff('d', ldt_a)
ls_out = '基准:' + ldt_a.format('{:%F %T}') + '~r~n'
ls_out = ls_out + '加 10 天:' + ldt_b.format('{:%F}') + '~r~n'
ls_out = ls_out + '加 2 月:' + ldt_a.add('m', 2).format('{:%F}') + '~r~n'
ls_out = ls_out + '加 30 分:' + ldt_a.add('M', 30).format('{:%F %T}') + '~r~n'
ls_out = ls_out + '截断到月:' + ldt_c.format('{:%Y-%m-%d %H:%M:%S}') + '~r~n'
ls_out = ls_out + '本月最后一天:' + ldt_d.format('{:%F}') + '~r~n'
ls_out = ls_out + '相差天数:' + string(ll_diff)
destroy ldt_d
destroy ldt_c
destroy ldt_b
destroy ldt_a
MessageBox('日期运算', ls_out)
实机输出:基准:2023-05-18 12:02:04 / 加 10 天:2023-05-28 / 加 2 月:2023-07-18 / 加 30 分:2023-05-18 12:32:04 / 截断到月:2023-05-01 00:00:00 / 本月最后一天:2023-05-31 / 相差天数:10。
truncate 各 unit 的实机结果(同一基准):
调用结果
truncate('y')2023-01-01 00:00:00
truncate('m')2023-05-01 00:00:00
truncate('d')2023-05-18 00:00:00
truncate('h')2023-05-18 12:00:00
truncate('M')2023-05-18 12:02:00
做"本月账单区间""今日零点到现在""按小时聚合"这类需求,truncate 一行就完事,不需要自己拼日期字符串。
八、时间戳互转(含坑三)
// 示例输入:待转换的日期时间
string ls_base
ls_base = '2023/05/18 12:02:04'
uo_datetime ldt
uo_datetime ldt_back
longlong ll_ts
longlong ll_ms
string ls_out
ldt = create uo_datetime
ldt.Set(ls_base)
// false = 秒级时间戳,true = 毫秒级时间戳
ll_ts = ldt.timestamp(false)
ll_ms = ldt.timestamp(true)
ldt_back = create uo_datetime
ldt_back.Set(ll_ts, false)
ls_out = '秒级时间戳:' + string(ll_ts) + '~r~n'
ls_out = ls_out + '毫秒级时间戳:' + string(ll_ms) + '~r~n'
ls_out = ls_out + '回转日期:' + ldt_back.format('{:%Y-%m-%d %H:%M:%S}')
destroy ldt_back
destroy ldt
MessageBox('时间戳', ls_out)
实机输出:秒级时间戳:1684382524(准确,2023-05-18 12:02:04 东八区 = 1684382524 UTC 秒);回转 :2023-05-18 12:02:04,秒级往返无损。
坑三(重要):timestamp(true) 的毫秒位不可信。实测同一段代码跑三次,毫秒级时间戳分别是 1684382524892、1684382524030、1684382524995——而 msecond() 始终是 0。也就是说毫秒部分并非来自你 Set 进去的值。更直接的证据:先 Set(1684382524000, true)(末三位是 000),再 timestamp(true) 得到 1684382524368。**结论:需要毫秒精度时不要依赖 timestamp(true),用 timestamp(false) * 1000 自行构造,或改用其它高精度来源。**
九、时区:SetGMT(坑四:全局生效)
// 示例输入:待观察的日期时间
string ls_base
ls_base = '2023/05/18 12:02:04'
uo_datetime ldt
string ls_out
ldt = create uo_datetime
ldt.Set(ls_base)
// SetGMT 是全局设置,用完必须改回来,否则同进程后续所有日期都按新时区
ldt.SetGMT('GMT-0')
ls_out = 'UTC 显示:' + ldt.format('{:%F %T}') + '时间戳=' + string(ldt.timestamp(false)) + '~r~n'
ldt.SetGMT('GMT-8')
ls_out = ls_out + '东八区显示:' + ldt.format('{:%F %T}') + '时间戳=' + string(ldt.timestamp(false))
destroy ldt
MessageBox('时区', ls_out)
实机输出:UTC 显示:2023-05-18 04:02:04 时间戳=1684382524 / 东八区显示:2023-05-18 12:02:04 时间戳=1684382524。
两点结论:SetGMT 只影响显示(以及按字符串解析时的基准),不改变底层时间戳;它返回 boolean 表示设置是否成功。用法上,对接海外接口时先切到 GMT-0 读出 UTC 串,处理完立刻切回 GMT-8,否则后面所有日期都偏 8 小时。
十、info():一行导出全部字段到 uo_json
// 示例输入:待导出的日期时间
string ls_base
ls_base = '2023/05/18 12:02:04'
uo_datetime ldt
uo_json lj
string ls_out
ldt = create uo_datetime
ldt.Set(ls_base)
lj = create uo_json
// info 一次性把全部时间分量与 40 多种常用格式结果灌进 uo_json
ldt.info(lj)
ls_out = lj.ToString()
destroy lj
destroy ldt
MessageBox('info 导出', ls_out)
实机导出的 JSON(节选)形如:
{"year":2023,"month":5,"day":18,"hour":12,"minute":2,"second":4,
"millsecond":0,"week":4,"yday":137,"datetime":"2023-05-18 12:02:04",
"format('{:%F}')":"2023-05-18","format('{:%T}')":"12:02:04",
"format('{:%A}')":"Thursday","format('{:%z}')":"+0800", ...}
做日期维度的事实表、日志打点、给前端一次性返回所有日期字段时,这一行顶几十行手工拼串。
十一、坑清单(五条,全部实机踩出)
#坑现象正确做法
1format 格式串没包 {}原样返回 %F 这类字面量一律写 format('{:%F}');Set 第二参同理
2yday() 是 0 基5 月 18 日返回 137,比直觉少 1要 1 基用 format('{:%j}')
3timestamp(true) 毫秒位msecond()=0 但毫秒时间戳末三位非 0 且每次运行都变只用 timestamp(false),毫秒自行构造
4SetGMT 全局生效切完忘记改回,后续所有日期偏 8 小时成对切换,用完立刻改回 GMT-8
5对象没 create编译通过、运行报 Null object reference显式 create / destroy,add/truncate/MLastDay 的返回值也要销毁
另有一条使用约定:add / truncate / MLastDay 返回新对象、原对象不变(实测 ldt_a 在调用前后 format 结果一致),所以别指望 ldt.add('d',10) 之后 ldt 自己变了。
十二、与原生 PowerScript 日期函数对比
需求原生做法uo_datetime
加减天数RelativeDate(d, n)(只能到天)add('d', n),且支持月/时/分
相差天数DaysAfter(d1, d2)(只到天)diff('d', other),可扩展到月/年
相差秒SecondsAfter(t1, t2)(限 32 位、有溢出风险)diff('h', other) 或时间戳相减
月末自己拼下月 1 号减一天MLastDay() 一行
格式化String(dt, 'yyyy-mm-dd')format('{:%F}'),支持 40+ 格式符
解析任意格式手工 Mid 截取Set(s, fmt)
时间戳自己算基准差timestamp() / Set(ts) 双向
时区无SetGMT()
原生函数在"加几天""差几天"这种简单场景够用且零依赖;一旦涉及月末、时/分级运算、任意格式解析、时间戳、时区,uo_datetime 的价值立刻体现。两者可以混用:date() / datetime() 把结果转回 PB 原生类型,无缝接回 DataWindow。
十三、扩展点
[*]批量日期维度生成:truncate('d') 定位到零点后循环 add('d',1),配合 DataStore 一次性灌进日期维表。
[*]报表区间:月初 truncate('m')、月末 MLastDay()、上月同日 add('m',-1),比手写 SQL 的 DATEADD 更可读也更易跨库。
[*]与 uo_json / uo_database 联动:info() 导出的 JSON 可以直接 uo_database 参数化入库,或作为接口响应返回。
[*]跨线程:uo_datetime 无界面依赖,可在 uo_thread 后台任务里做时间片计算,但注意 SetGMT 是全局的,多线程下不要并发切换。
实机验证情况:本文全部代码在 PB 12.5 + PBIDEA 环境下完成四项验证——pypower pbl import 导入 0 错误、build rebuild --type full 全量编译 0 错误、PBVM 运行 .pbtest PASS(9 项断言全部命中:加 10 天得 2023-05-28、截断到月得 2023-05-01 00:00:00、月末得 2023-05-31、差值得 10、分量与 2023/05/18 12:02:04 一致、秒/毫秒时间戳往返一致、info 导出非空、星期名为 Thursday)、check_pb125.py 0 错 0 提醒 + check_enc.py 编码全部 OK。未实测项:本文无(文中每个代码块的输出均取自实机运行结果)。 PB10 兼容版示例 PBL 已上传(附件 uo_datetime_pb10.zip)
环境要求:PB 10 及以上(12.5 同样可用);Windows 7+;PbIdea.dll 必须与程序同目录或可搜索;不需要数据库。
操作步骤:
1) 解压 uo_datetime_pb10.zip,得到 uo_datetime_pb10.pbl;
2) 把 PbIdea.dll 复制到应用程序目录(PB 调试时放工程目录或 PB 根目录);
3) 工程 Library List 追加 uo_datetime_pb10.pbl,Full Rebuild 一次(应 0 错误);
4) 在按钮 clicked 里写:
nvo_dt_demo lnv
lnv = create nvo_dt_demo
lnv.of_show() // 一键弹窗看全部 7 组演示结果
destroy lnv
包内对象:uo_datetime(主对象)、uo_json 及其依赖链(uo_map / uo_xml / uo_xml_node / uo_xml_attribute,供 info() 用)、nvo_dt_demo(本文全部演示封装)。
数据库:本示例不依赖数据库,无建表 SQL,纯内存日期时间计算。
几个必看的坑(都是实机踩出来的):
[*]format() 格式串必须包在 {} 里:ldt.format('{:%F}') 才出 2023-05-18,裸写 '%F' 原样返回;
[*]unit 大小写敏感:y=年、m=月、d=日、h=时、M=分钟(小写 m 是月份);
[*]yday() 是 0 基的(5/18 得 137),1 基请用 format('{:%j}');
[*]timestamp(true) 的毫秒位不可信(msecond() 恒为 0 但末三位每次运行都变),毫秒精度请用 timestamp(false)*1000;
[*]SetGMT 只改显示、不改时间戳,且全局生效,用完必须切回 GMT-8。
实机验证情况:PB12.5 下 import 0 错 + Full Rebuild 0 错 + PBVM 运行 PASS(9 项断言全中)+ check_pb125 0 错 0 提醒 + check_enc 编码全 OK;本 PB10 版 PBL 用 pypower --pb-version 100 全量编译 0 错误,并已用 pbl list 核对对象齐全(防循环依赖静默丢失)。文中所有输出值均为真实运行结果。
页:
[1]