马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
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", ...}
复制代码
做日期维度的事实表、日志打点、给前端一次性返回所有日期字段时,这一行顶几十行手工拼串。
十一、坑清单(五条,全部实机踩出)
| # | 坑 | 现象 | 正确做法 | | 1 | format 格式串没包 {} | 原样返回 %F 这类字面量 | 一律写 format('{:%F}');Set 第二参同理 | | 2 | yday() 是 0 基 | 5 月 18 日返回 137,比直觉少 1 | 要 1 基用 format('{:%j}') | | 3 | timestamp(true) 毫秒位 | msecond()=0 但毫秒时间戳末三位非 0 且每次运行都变 | 只用 timestamp(false),毫秒自行构造 | | 4 | SetGMT 全局生效 | 切完忘记改回,后续所有日期偏 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。未实测项:本文无(文中每个代码块的输出均取自实机运行结果)。 |