compromise-dates 插件全解析:用自然语言解析日期、时间与时长
【免费下载链接】compromisemodest natural-language processing项目地址: https://gitcode.com/gh_mirrors/co/compromise
compromise-dates 是 compromise 生态中最具实用价值的插件之一,它把"把一段人话变成结构化时间数据"这件事做到了开箱即用:无论是the second monday of february、2 years, 4 months, and 5 days ago还是GMT+9,都能解析出明确的start/endISO 时间戳。本指南以 plugins/dates/README.md 为核心,结合插件源码与测试,完整讲解其能力边界、API、配置项与底层解析管线,读完即可在自己的前端或后端项目里接入自然语言日期解析。
快速开始:安装与加载
compromise-dates 是独立发布的 npm 包,与 compromise 主库分开安装:
npm install compromise compromise-dates在代码中先加载 compromise,再通过nlp.plugin()注册日期插件:
import nlp from 'compromise' import datePlugin from 'compromise-dates' nlp.plugin(datePlugin) let doc = nlp('the second monday of february') doc.dates().get()[0] /* { start: '2021-02-08T00:00:00.000Z', end: '2021-02-08T23:59:59.999Z'} */从 package.json 可以看到,插件声明了compromise >=14.2.0作为 peerDependency,运行时依赖spacetime(时区与夏令时计算)与spacetime-holiday(节假日推算),并提供 CJS/ESM/UMD 三种构建产物(builds 目录),浏览器可直接引入compromise-dates.min.js。
它能解析什么:能力全景
插件覆盖了从最直白的书面日期到口语化时间表达的大多数场景。下表完整列出了 README 中收录的解析能力,Start/End两列表示解析结果的时间范围:
明确日期(explicit dates)
| 输入 | 说明 | Start | End |
|---|---|---|---|
march 2nd | 月+日 | March 2, 12:00am | March 2, 11:59pm |
2 march | 日+月 | — | — |
tues march 2 | 星期+月+日 | — | — |
march the second | 自然语言数字 | — | — |
on the 2nd | 隐含月份 | — | — |
tuesday the 2nd | 日期推算 | — | — |
数字日期(numeric dates)
| 输入 | 说明 | Start | End |
|---|---|---|---|
2020/03/02 | ISO 格式 | — | — |
2020-03-02 | 连字符 ISO | — | — |
03-02-2020 | 英式格式 | — | — |
03/02 | 月/日 | — | — |
2020.08.13 | 替代 ISO | — | — |
命名日期(named dates)
| 输入 | 说明 | Start | End |
|---|---|---|---|
today | — | — | — |
tomorrow | — | — | — |
christmas eve | 日历节假日 | Dec 24, 12:00am | Dec 24, 11:59pm |
easter | 天文节假日 | 依年份而定 | — |
q1 | 财务季度 | Jan 1, 12:00am | Mar 31, 11:59pm |
时间(times)
| 输入 | 说明 | Start | End |
|---|---|---|---|
2pm | — | — | — |
2:12pm | — | — | — |
2:12 | — | — | — |
02:12:00 | 奇怪 ISO 时间 | — | — |
two oclock | 文字形式 | — | — |
before 1 | 时间前 | — | — |
noon | — | — | — |
at night | 非正式时段 | — | — |
in the morning | — | — | — |
tomorrow evening | — | — | — |
时区(timezones)
| 输入 | 说明 | Start | End |
|---|---|---|---|
eastern time | 非正式时区 | — | — |
est | 时区缩写 | — | — |
peru time | 地区时区 | — | — |
GMT+9 | UTC/GMT 偏移 | — | — |
-4h | 小时偏移 | — | — |
Canada/Eastern | IANA 时区码 | — | — |
相对时长(relative durations)
| 输入 | 说明 | Start | End |
|---|---|---|---|
this march | — | — | — |
this week | — | — | — |
this sunday | — | — | — |
next april | — | — | — |
this past year | — | — | — |
second week of march | 第 N 周 | — | — |
last weekend of march | — | — | — |
last spring | 季节 | — | — |
the saturday after next | 后推 | — | — |
推后日期(punted dates)
| 输入 | 说明 | Start | End |
|---|---|---|---|
in seven weeks | 现在+时长 | — | — |
two days after june 6th | 日期+时长 | — | — |
2 weeks from now | — | — | — |
2 weeks after june | — | — | — |
2 years, 4 months, and 5 days ago | 复杂时长 | — | — |
a week and a half before | 文字数字 | — | — |
a week friday | 习语格式 | — | — |
开始/结束(start/end)
| 输入 | 说明 | Start | End |
|---|---|---|---|
end of the week | 倾向结尾 | — | — |
start of next year | 倾向开头 | — | — |
middle of q2 last year | 粗略居中 | — | — |
日期区间(date-ranges)
| 输入 | 说明 | Start | End |
|---|---|---|---|
between june and july | 显式区间 | — | — |
from today to next halloween | — | — | — |
aug 1 - aug 31 | 破折号区间 | — | — |
22-23 February | — | — | — |
today to next friday | — | — | — |
during june | — | — | — |
aug to june 1999 | 共享区间信息 | — | — |
before [2019] | 截至某日期 | — | — |
by march | — | — | — |
after february | 日期到无限 | — | — |
重复区间(repeating-intervals)
| 输入 | 说明 | Start | End |
|---|---|---|---|
any wednesday | N 次重复日期 | — | — |
any day in June | 区间内重复日期 | June 1 ... | .. June 30 |
any wednesday this week | — | — | — |
weekends in July | 更复杂区间 | — | — |
every weekday until February | 截至某日期的区间 | — | — |
API 详解
.dates():主入口
doc.dates()用于查找所有日期短语,返回一个Dates视图(继承自 compromise 的View),核心实现在 src/api/dates.js。其查找逻辑位于 src/api/find/index.js:先用doc.match('#Date+')匹配,再做一系列反例过滤——排除纯时长(如20 minutes)、金额百分比($5 an hour)、per #Duration等,最后按规则切分日期块,避免30 minutes on tuesday这类短语被误判为日期。
支持以下子方法:
.dates().get():返回精简的 start/end JSON。get(n)可传入索引取第 n 个结果;源码中会过滤掉既无start也无repeat的结果,因此every tuesday这类重复日期也会被保留(见 dates.js)。.dates().json():在 compromise 原生 json 基础上叠加日期元数据,每个结果附带dates字段(可用{ dates: false }关闭,见 dates.js)。.dates().format(fmt):把原文中的日期短语原地替换为格式化日期。格式串是 spacetime 的格式语法,测试 format.test.js 给出了可运行示例:
let doc = nlp(`i'm going skiing two days after November 1st 2019 at 7pm`) doc.dates().format('{day} {month} {date-ordinal}, {time}') // i'm going skiing Sunday November 3rd, 7:00pm doc = nlp(`halloween`) doc.dates().format('{month} {date-ordinal}') // October 31st.dates().isBefore(iso)/.dates().isAfter(iso):仅保留早于/晚于给定 ISO 日期的结果。.dates().isSame(unit, iso):仅保留与给定日期同年、同月或同日的(如isSame('month', '2021-02-01'))。
get()返回的DateJSON结构(与 index.d.ts 一致)为:
interface DateJSON { start: string | null // ISO 时间戳 end: string | null timezone: string | null duration: { years?, months?, days?, hours?, minutes? } // 区间跨度 unit?: string // 跨度单位:'day' / 'time' / 'year' 等 repeat?: { // 重复日期,如 'every tuesday' interval: Record<string, number> filter?: { weekDays?: Record<string, boolean> } choose?: 'AND' | 'OR' | null time?: string | null } }其中unit的推断逻辑很巧妙(见 toJSON.js):如果start恰好落在某单位(year/quarter/month/week/day)的起点,且end落在下一单位起点,就推断该区间为一个完整单位——例如jan 1 to dec 31会被识别为unit: 'year'。
.durations():时长
doc.durations()匹配2 months、2mins、20min这类长度表达(见 durations/index.js),匹配模式为#Value+ #Duration (and? #Value+ #Duration)?,并排除in 20 minutes这类日期偏移表达。get()返回形如{ minute: 30, hour: 2 }的对象。
单位归一化逻辑在 durations/parse.js:m→minute、hr→hour、wk→week、qtr→quarter、yr→year等缩写映射,并支持20mins这种数字字母粘连的词内拆分。测试 durations.test.js 验证了三类行为的边界:
nlp('in 20 mins').dates().found // true —— 'in 20 mins' 是日期偏移 nlp('in 20 mins').durations().found // false nlp('for 20 mins').dates().found // false —— 'for 20 mins' 是纯时长 nlp('for 20 mins').durations().found // true.times():一天中的时刻
doc.times()匹配4:30pm、half past five、ten past three等时间表达(见 times.js),查找模式为#Time+ (am|pm)?。get()返回:
interface TimeJSON { time: string | null // 规范时间,如 '3:10pm' '24h': string | null // 24 小时制,如 '15:10' hour?: number minute?: number }format('24h')可把文本时间统一为 24 小时制。时间区间(如tuesday from 4 to 5pm、9-5 on tuesday)也能被 range 解析 拆成{ start, end }两个时间点,测试见 times.test.js。
配置选项:context 对象
.dates()接受一个可选 context 对象来设定日期解析的基准环境,这是该插件最重要的定制入口(TypeScript 类型见 index.d.ts 的DateOptions):
const context = { timezone: 'Canada/Eastern', // 默认是你的本地时区 today: '2020-02-20', // 隐含的基准日/基准年 punt: { weeks: 2 }, // 'after june 2nd' 这类表达的隐含时长 dayStart: '8:00am', // 一天的默认开始时间 dayEnd: '5:30pm', // 一天的默认结束时间 dmy: false // 设为 true 时,歧义日期按英式(日月年)解析 } nlp('in two days').dates(context).get() /* [{ start: '2020-02-22T08:00:00.000+5:00', end: '2020-02-22T17:30:00.000+5:00' }] */各选项的底层行为可以从 parse/index.js 的 context 归一化看到:
timezone:设为false时强制使用'UTC',避免本地时区干扰;解析到的时区信息(如eastern time)会覆盖该默认值。today:解析"今天/今年"的相对基准,接受 ISO 字符串、epoch 数字或 Date;默认为spacetime.now(timezone)。punt:'after june 2nd'这类表达没有明确终点时的隐含时长,默认{ weeks: 2 }。dayStart/dayEnd:无显式时刻的日期默认从dayStart开始、dayEnd结束,因此上面的示例中两天后返回8:00–17:30而非 0 点到 23:59。dmy:控制歧义数字日期的解释,见下文"英美日期歧义"。
时区还会参与日期基准的换算:当文本自带时区(如in PST)时,parse/one/index.js 会把today的墙上时钟时间搬到目标时区,保证"同一天、同一时刻"语义正确。
解析管线的源码剖析
理解插件为何能处理这么多表达,关键在于其分层的解析管线。整条链路(parse/index.js → range/index.js)可以概括为"单日期解析 + 区间组合"两层:
1. 标签与查找(compute 阶段):插件注册了 tags、words、regex 与一个计算钩子(见 plugin.js)。compute/index.js 中会执行两遍正则网络匹配(doMatches调用两次以链式处理2 years, 4 months and 5 days ago这类复合表达),随后依次运行00-year、01-time-range、02-timezone、03-fixup四步处理,最后对#DateShift表达再做两轮强化打标。
2. 单日期解析(parse/one):任何一段日期文本都要经过"分词 → 解析 → 变换"三个阶段(parse/one/index.js):
- tokenize(01-tokenize):按 7 个维度切分文本,包括 shift(偏移量)、counter(计数)、time、relative(相对词)、section、timezone、weekday;
- parse(02-parse):分别处理
today(基准日)、holidays(节假日)、next/last(相对星期/月份)、yearly(年度表达)、explicit(显式日期); - transform(03-transform):把解析出的各部件叠加到基准日期对象上,例如
addCounter实现second week of march这类第 N 周计算。
3. 区间解析(parse/range):先检测重复日期(every tuesday),再按模板依次尝试twoTimes、combos、dateRange、oneDate四类区间(range/index.js),每个模板内部先doc.match(fmt.match)命中再执行自定义parse逻辑。quarter to five这类"钟表时间"会被特殊拦截,避免误当成日期区间。最后若发现start晚于end,还会自动交换两者以保证区间方向正确。
4. JSON 输出(toJSON):toJSON.js 通过end - start计算duration字段(删除毫秒与秒),并推导unit与repeat,最终产出{ start, end, timezone, duration, unit?, repeat? }的扁平结构。
设计决策与"观点"(Opinions)
README 用专门的章节记录了插件在歧义场景下的取舍,这些决策对集成方至关重要:
一周从周一开始
默认情况下,一周从周一开始,'next week'表示周一早上到周日晚上。README 说明该配置目前没有透传给 spacetime,属于插件内固定行为。
隐含时长(Implied durations)
'after October'默认返回从Nov 1st开始、持续2 周的区间。可通过punt覆盖:
doc.dates({ punt: { month: 1 } })未来倾向(Future bias)
'May 7th'倾向返回未来最近的 5 月 7 日;但在当前月份内会回退到过去日期:
// 假设今天是 3 月 2 日 nlp('feb 30th').dates({ today: '2021-02-01' }).get()This / Next / Last 的语义
this monday:裸的monday总是指它自己或即将到来的周一——周一当天说this monday是当天,周二说则指下周一。this june同理,6 月说指当月,其他月份指最近的未来 6 月。README 也坦言,未来版本可以考虑借助句子时态(i paid on mondayvsi will pay on monday)进一步消歧。last monday:周二说last monday不是昨天,而是-1 周;a week ago monday同样有效;this past monday才指昨天。'last X'若跨过周起始点可能少于 7 天,例如周一说的last friday只有几天前。README 对比了同类库:Wit.ai 与 chronic 返回昨天,Natty 与 SugarJS 与本插件一致返回 -1 周。next wednesday:周二说next wednesday不是明天,而是+1 周;a week wednesday同样 +1 周;this coming wednesday才是明天。此处 Wit.ai、chronic、Natty 均返回明天,SugarJS 与本插件一致返回 +1 周。
第 N 周(Nth Week)
一个月的"第一周"或一年的"第一周"定义为包含周四的那一周——这是广泛采用但略显奇怪的惯例(README 猜测源自军事格式),且不易配置。因此first week of January的起始日可能是 12 月的某个周一;而first monday of January则必然落在 1 月内。
英美日期歧义
默认与 JavaScript 保持一致:01/02/2020按美式解析为1 月 2 日,但13/01/2020会按英式解析为1 月 13 日(因为 13 不可能是月份)。若想强制02/03/1999的解释,用dmy: true:
nlp('02/03/1999').dates().get() // February 3(美式) nlp('02/03/1999').dates({dmy:true}).get() // March 2(英式)ISO 日期(如1999-03-02)不受该选项影响。对应测试见 dmy.test.js。
季节与昼夜
默认'this summer'返回6 月 1 日 – 9 月 1 日(北半球 ISO 定义),半球配置未来可能支持。lunch time等词有硬编码时刻;一般情况下一天从12:00am开始、到11:59pm(当天最后一毫秒)结束。
无效日期(Invalid dates)
compromise 会先把"看起来像日期"的东西打上标签,但直到解析时才校验有效性:
'january 34th 2020'→ 返回Jan 31 2020(钳制到当月最后一天);'tomorrow at 2:62pm'→ 直接返回'tomorrow'(丢弃无效时间);'6th week of february'→ 返回 3 月的第 2 周(越界顺延);- 遇到 DST 跳变中被跳过或重复的小时,返回离 DST 变更最近的合法时间。
包含/排他区间
'between january and march'是排他的——结束于 3 月开始之前;'january to march'是包含的——结束于 3 月的最后一天。README 承认这在日常语义中通常模棱两可。
日期贪婪度(Date greediness)
插件默认对输入文本不做假设,尽力避免误报。若你能确认文本里必然包含日期,可以用 compromise 的nlp.extend提高打标强度(这是 README 提供的官方示例):
nlp.extend(function (Doc, world) { // 歧义缩写词 world.addWords({ weds: 'WeekDay', wed: 'WeekDay', sat: 'WeekDay', sun: 'WeekDay', }) world.postProcess(doc => { // 把 '2nd quarter' 标记为日期 doc.match('#Ordinal quarter').tag('#Date') // 把 '2/2' 标记为日期(而非分数) doc.match('/[0-9]{1,2}/[0-9]{1,2}/').tag('#Date') }) })杂项行为
'thursday the 16th'会强制落到 16 日,即便 16 日并非周四;'in a few hours/years'按 3 小时/年计,'a couple of'按 2 计;'jan 5th 2008 to Jan 6th the following year'支持跨年显式引用;'half past 5'默认按下午 5 点(5pm)处理。
边界与限制:诚实的清单
README 同样坦白记录了插件"做得勉强"和"做不到"的场景,集成前务必知晓:
处理得勉强的(awkward):
| 输入 | 说明 |
|---|---|
middle of 2019/June | 尝试寻找"大致中心",返回 June 15 |
good friday 2025 | 尝试推算天文设定的节假日 |
Oct 22 1975 2am in PST | 历史 DST 变更(默认按当前 DST 规则计算) |
不支持的(doesn't do):
| 输入 | 说明 |
|---|---|
not this Saturday, but the Saturday after | 自引用逻辑 |
3 years ago tomorrow | 口语化省略表达 |
2100 | 军事时间格式 |
测试与可靠性
插件在 tests 目录 下配备了 30+ 个测试文件,覆盖am big的典型场景:ambig-month、ambig-week、before-after、dmy、duration、full-iso、timezone、today、tokenizer、week等,另有 false-positive.test.js 专门防守误报。测试通过 tests/_lib.js 同时验证源码版(../src/plugin.js)与构建产物版(builds/compromise-dates.mjs),确保发布包与源码行为一致。运行方式:
cd plugins/dates npm test # 源码测试 npm run testb # 生产构建产物测试可交互演示位于 plugins/dates/demo/index.html,便于在不写代码的情况下快速验证各种输入。
依赖与生态位置
从 package.json 可以看到插件的技术底座:
- spacetime:承担时区、夏令时(DST)与所有日历算术——README 明确指出 Tokenization 与消歧由 compromise 负责,时区与 DST 推算交给 spacetime,数字解析复用 compromise-numbers,非正式时区名(
eastern time)则由 spacetime-informal 调和; - spacetime-holiday:提供
easter、christmas eve等节假日推算; - peerDependencies:要求
compromise >=14.2.0。
README 的"About"一节给出了作者的核心立场:正则表达式太脆弱、神经网络太飘忽、商业公司不该垄断通用日期解析——他们认为基于规则与简单 NLP 的开源社区库才是构建自然语言日期解析器的最优解,这也是整个 compromise 项目"modest natural-language processing"定位在日期领域的落地体现。
总结
compromise-dates 用一套清晰的"标签查找 → 单日期解析 → 区间组合 → JSON 输出"管线,把自然语言日期解析做成了三个简单方法(dates()/durations()/times())。掌握 context 配置(timezone、today、punt、dayStart/dayEnd、dmy)与 README 中记录的设计决策,就能预判它在歧义输入下的行为,从而在自己的应用中可靠地使用它。配合format()与isBefore/isAfter/isSame,它足以支撑日程提醒、内容提取、报表归档等常见的时间信息处理需求。
【免费下载链接】compromisemodest natural-language processing项目地址: https://gitcode.com/gh_mirrors/co/compromise
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考