手撕RRule是什么体验?一个字,难;四个字,我太难了~
前言
为什么会产生自己写一个rrule
的想法呢?现成的库,它不香么?
这得从一个巧合说起:某一天,我听说WebAssembly
很快,又一天,我听说Rust
写WebAssembly
很快,再一天,我发现我们日历项目里,RRule
解析重复规则很慢。于是一拍手,要不用rust
写一个rrule
然后打包成WebASsembly
吧~。然后苦逼的日子就开始了。
项目已经发布到
Npm
,@suilang/rrule-rust,基础的解析能力已经实现。后续会陆续补齐如生成字符串、设置排除时间等能力,有兴趣的同学可以看下。
标准场景下,执行效率会比rrule.js
快4到5倍,如果是加上时区,会快100倍左右。(最新的rrule.js对于时区的计算有些性能问题,不知道什么时候会修)。
RRule是什么
rrule
是一个用于解析日历循环的工具,它遵循了 iCalendar 规范(RFC 5545)。iCalendar 是一种通用的日历数据交换格式,用于描述日历事件和重复规则。
rrule
的主要用途是根据指定的重复规则生成符合规则的日期序列。它可以处理各种复杂的重复规则,例如每天、每周、每月、每年的重复,以及每隔一定时间间隔重复的规则。它还支持排除特定日期、指定重复次数等功能。
举个例子,假设我们有一个重复规则,要求每周一、周三和周五重复一次。我们可以使用rrule.js
来实现这个规则:
ini
import { RRule } from 'rrule';
const rule = new RRule({
freq: RRule.WEEKLY,
byweekday: [RRule.MO, RRule.WE, RRule.FR],
});
const eventDates = rule.all();
// 打印日期序列
eventDates.forEach((date) => {
console.log(date);
});
在上述示例中,我们使用rrule.js
创建了一个每周重复的规则,并指定重复的日期为周一、周三和周五。然后,我们使用rule.all()
生成满足该规则的日期序列,并遍历打印出每个日期。
rrule
同样支持使用字符串来描述重复规则,例如,我们想获取每年最后一个周五,可以使用
ini
RRULE:FREQ=YEARLY;BYDAY=-1FR
rrule.js
是rrule
的 JavaScript 版本,适用于在浏览器端或 Node.js 环境中使用。它提供了与 Python 版本类似的功能,可以轻松处理复杂的重复规则,适用于构建日历应用程序、定期任务调度等场景。
逻辑实现
首先是重复规则,rrule
支持7种重复参数,分别是年、月、周、日、时、分、秒。枚举定义如下所示。
rust
pub enum Frequency {
/// The recurrence occurs on a yearly basis.
Yearly = 0,
/// The recurrence occurs on a monthly basis.
Monthly = 1,
/// The recurrence occurs on a weekly basis.
Weekly = 2,
/// The recurrence occurs on a daily basis.
Daily = 3,
/// The recurrence occurs on an hourly basis.
Hourly = 4,
/// The recurrence occurs on a minutely basis.
Minutely = 5,
/// The recurrence occurs on a second basis.
Secondly = 6,
}
在此基础上,还有8种基础控制参数(还有点其他的,但我没实现)
markdown
- INTERVAL // 控制重复间隔,正整数
- COUNT // 生成日期的个数
- UNTIL // 生成日期的终止时间
- BYDAY // 控制星期几 如MO,FR,3TU,-2WE
- BYMONTH // 指定月份
- BYMONTHDAY // 指定每月几号,支持正负
- BYYEARDAY // 指定每年第几天,支持正负
- BYWEEKNO // 指定是第几周
然后有趣的就来了,即使在重复周期为DAILY
的情况下,所有的参数都是可用的,哪怕用了BYYEARDAY=1
。当我首次发现这个事情时,我的心情无比糟糕。。。
另外,例如BYDAY=-1FR
,如果是按年重复,代表着一年最后一个周五;如果在此基础上补充一个BYMONTH
,则代表着指定月份最后一个周五;如果改成按月重复,代表着每月最后一个周五;如果按周重复,则代表着每个周五。非常的无语。。但是很灵活
具体参数的解析规则就不具体介绍了,下面以按日和月解析来讲讲如何解析重复规则。虽然看着很简洁,但是这是我花了5个完整天总结出来的。另外,我没有实现时、分、秒的解析,毕竟前面4个已经很复杂了。
这里有一些通用的规则:
- 必须设置开始时间,否则会取当前时刻
- 必须设置
Freq
count
和until
至少设置一个- 截止时间不能小于开始时间
按日解析
- 初始化
rrule
里的参数,初始化当前时间索引curr
,存储列表list
, - 准备
while
循环,判断curr
是否大于until
,并且list
长度小于count
,不符合则到第9步 - 此重复规则下,
BYDAY
将忽略正负数;如果BYDAY
存在并且curr
不满足BYDAY
的需求,前进interval
天,回到第二步 - 如果
BYMONTH
存在并且curr
不满足BYMONTH
,前进,并回到第二步 - 如果
BYMONTHDAY
存在,计算该月所有符合BYMONTHDAY
的时刻,并判断curr
是否符合其一。如果不符合,前进,并回到第二步 - 如果
BYYEARDAY
存在,计算该月所有符合BYYEARDAY
的时刻,并判断curr
是否符合其一。如果不符合,前进,并回到第二步 - 如果
BYWEEKNO
存在,计算该curr
是否符合某个weekno
。如果不符合,前进,并回到第二步 - 将
curr
放入列表,返回第二步 - 结束循环,并返回列表
按月获取
按周和按日其实是差不多的,就不具体讲解了,现在说说按月。
- 初始化
rrule
里的参数,初始化当前时间索引curr
,存储列表list
, - 如果没有任何控制参数,则直接取每月中,开始时间所在的日,按月递增,并返回对应列表
- 构造按月解析的闭包
-
- 初始化临时存储数组
list
- 如果有指定月份并且当天不属于指定月份之一,直接返回空数组
- 如果指定了
BYYEARDAY
,获取所有符合条件的时间,存储到vec_by_year_day
-
- 如果
vec_by_year_day
为空,直接返回空数组,结束本月循环, - 否则将数据存储到list
- 如果
- 如果指定了
BYMONTHDAY
,获取所有符合条件的时间,存储到vec_by_month_day
-
- 如果
vec_by_month_day
为空,直接返回空数组,结束本月循环, - 如果
list
不为空,则将list
与vec_by_month_day
取交集,结果为空则直接返回 list
为空,则将vec_by_month_day
数据存储到list
- 如果
- 如果指定了
BYWEEKNO
,获取所有符合条件的时间,存储到vec_by_weekno
-
- 如果
vec_by_weekno
为空,则直接返回空数组,结束本月循环 - 如果
list
不为空,则将list
与vec_by_weekno
取交集,结果为空则直接返回 list
为空,则将vec_by_weekno
数据存储到list
- 如果
- 如果指定了
BYDAY
,获取所有符合条件的时间,存储到vec_by_day
-
- 如果
vec_by_day
为空,则直接返回空数组,结束本月循环 - 如果
list
不为空,则将list
与vec_by_day
取交集,结果为空则直接返回 list
为空,则将vec_by_day
数据存储到list
- 如果
- 对临时数组
list
进行排序,并返回
- 初始化临时存储数组
- 准备while循环,判断
curr
是否大于until
,并且list
长度小于count
- 传入当前时间
curr
到第三步的闭包中,获取返回值 - 将返回值放入
list
中,curr
步进interval
控制的月份,重新执行第4步 - 解析完成
rrule
的控制参数,本质上是获取+筛选,即先获取符合条件的值,再与其他的值做交集,最后剩下的就是需要的。在具体实现的过程中,增加了一些判断,在特定场景下,可以直接结束循环以节省性能。
关于时区
在实现解析规则的过程中,我们一直没有介绍关于时区的事情。并不是忘了,其实在解析过程中,我一直没用到时区。
解析本质上是对如20231023
这种格式的时间进行重复,已知年月日,已经够解析了。当我们按照一定规则获取到时间数组后,以年月日为参数,初始化一个带有时区的时间戳就够了。
性能
标准的速度验证还没来得及做,就简单验证了一下。标准场景下,执行效率会比rrule.js
快4到5倍,如果是加上时区,会快100倍左右。(最新的rrule.js对于时区的计算有些性能问题,不知道什么时候会修)。
WebAssembly
在现代Web开发中,WebAssembly(简称Wasm)已经成为一个备受关注的技术。WebAssembly是一种可移植、体积小、加载快速的二进制格式,旨在提供一种高效的执行环境。它是一种新的编译目标,可以将各种编程语言的代码编译成WebAssembly模块,这些模块可以在现代浏览器中直接运行。WebAssembly的设计目标是实现高性能、安全性和可移植性。
优势
- 性能优异:相比传统的JavaScript代码,WebAssembly的执行速度更快,因为它是直接在底层虚拟机中运行的。这使得Web应用程序可以更高效地处理复杂的计算任务,例如图形渲染、物理模拟等。
- 跨平台兼容:WebAssembly可以在几乎所有现代浏览器中运行,无论是桌面还是移动设备。这意味着开发者可以使用各种编程语言来编写Web应用程序,而不仅仅局限于JavaScript。
- 安全性:WebAssembly运行在沙箱环境中,提供了良好的安全性。它使用了一系列安全措施,如内存隔离和沙箱限制,以防止恶意代码对系统的攻击。
- 模块化:WebAssembly模块可以作为独立的组件进行开发和部署,这使得开发者可以更好地管理和维护代码库。此外,模块化的设计也为将来的性能优化和增量更新提供了便利。
使用场景
在Web开发中,可以使用WebAssembly来提高应用程序的性能和功能。以下是一些使用WebAssembly的常见场景:
- 高性能计算:如果应用程序需要进行大量的数值计算、图像处理或者复杂的算法运算,可以将这部分代码编译成WebAssembly模块,以提高计算性能。
- 游戏开发:WebAssembly可以用于创建高性能的HTML5游戏,通过将游戏逻辑编译成WebAssembly模块,可以实现更流畅的游戏体验。
- 跨平台应用:使用WebAssembly可以实现跨平台的应用程序,无论是桌面还是移动设备,用户都可以通过浏览器来访问和使用。
- 移植现有代码:如果你已经有用其他编程语言编写的代码,可以通过将其编译成WebAssembly模块,将其集成到现有的Web应用程序中,而无需重写整个应用程序。
用Rust编写WebAssembly
用Rust写WebAssembly还是很方便的,就好像新学语言时,在控制台打出Hello World! 那么简单。
- 准备运行环境
- 安装Rust,这是一切的基础
- 要构建包,我们需要一个额外的工具wasm-pack。这有助于将代码编译为WebAssembly,并生成在浏览器中使用的正确包。在终端输入以下命令:
perl
cargo install wasm-pack
- 启动新项目
sql
cargo new --lib hello-wasm
这将创建一个名为hello-wasm的新库,其中的目录结构为
css
├── Cargo.toml
└── src
└── lib.rs
其中Cargo.toml与npm的package.json类似,都是用来配置构建的文件。最终打出的WebAssembly的包,其中的package文件就是依据toml文件生成的。
- 配置
先安装下辅助工具
bash
cargo add wasm_bindgen
然后修改lib.rs的文件
rust
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn greet(name: &str)-> String {
format!("Hello, {}!", name)
}
再修改下toml文件,主要是补充下lib的参数
toml
[package]
name = "hello-wasm"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]
description = "A sample project with wasm-pack"
license = "MIT/Apache-2.0"
repository = "https://github.com/yourgithubusername/hello-wasm"
edition = "2018"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
- 打包
现在可以愉快的打包了
css
wasm-pack build --target web --release
使用我们最开始下载的打包工具,指定目标为web模式。就会在pkg文件夹下,生成一个简单但是齐全的npm包了。连d.ts
文件都补齐了。
sql
├── Cargo.lock
├── Cargo.toml
├── index.html
├── pkg
│ ├── hello_wasm.d.ts
│ ├── hello_wasm.js
│ ├── hello_wasm_bg.wasm
│ ├── hello_wasm_bg.wasm.d.ts
│ └── package.json
├── src
│ └── lib.rs
└── target
├── CACHEDIR.TAG
├── release
└── wasm32-unknown-unknown
- 使用
简单测试下~。在根路径新建一个index.html文件,放入以下内容。
html
<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<title>hello-wasm example</title>
</head>
<body>
<script type="module">
import init, { greet } from "./pkg/hello_wasm.js";
init().then(() => {
console.log(greet("WebAssembly"));
});
</script>
</body>
</html>
在script
中当做正常的包来引入,然后执行。记得先调用下init
。然后找个server
,或者在vs里下载个Live Server
,启动服务打开页面。就能在页面控制台上看到Hello, WebAssembly!
了。
完整的示例可以参考WebAssembly官方页面,也可以在Rust WebAssembly教程中自己实现一个简单的小游戏。
创作不易,点个赞吧~