Add support for date & time handling (#435)
This commit is contained in:
parent
f4fd6855e7
commit
752817ae74
160
Cargo.lock
generated
160
Cargo.lock
generated
@ -29,15 +29,6 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.3.2"
|
||||
@ -193,12 +184,6 @@ dependencies = [
|
||||
"wyz",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c6ed94e98ecff0c12dd1b04c15ec0d7d9458ca8fe806cea6f12954efe74c63b"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.13.1"
|
||||
@ -247,10 +232,8 @@ version = "0.4.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -381,12 +364,6 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.3.2"
|
||||
@ -780,29 +757,6 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.56"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.3.0"
|
||||
@ -995,15 +949,6 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.63"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue"
|
||||
version = "1.0.7"
|
||||
@ -1239,6 +1184,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_threads"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numerals"
|
||||
version = "0.1.4"
|
||||
@ -1996,6 +1950,35 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"libc",
|
||||
"num_threads",
|
||||
"serde",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b"
|
||||
dependencies = [
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-skia"
|
||||
version = "0.9.1"
|
||||
@ -2198,6 +2181,7 @@ dependencies = [
|
||||
"stacker",
|
||||
"subsetter",
|
||||
"svg2pdf",
|
||||
"time",
|
||||
"tiny-skia",
|
||||
"tracing",
|
||||
"ttf-parser",
|
||||
@ -2216,7 +2200,6 @@ name = "typst-cli"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"chrono",
|
||||
"clap 4.2.7",
|
||||
"clap_complete",
|
||||
"clap_mangen",
|
||||
@ -2232,6 +2215,7 @@ dependencies = [
|
||||
"same-file",
|
||||
"siphasher",
|
||||
"tempfile",
|
||||
"time",
|
||||
"tracing",
|
||||
"tracing-error",
|
||||
"tracing-flame",
|
||||
@ -2281,6 +2265,7 @@ dependencies = [
|
||||
"serde_yaml",
|
||||
"smallvec",
|
||||
"syntect",
|
||||
"time",
|
||||
"toml",
|
||||
"tracing",
|
||||
"ttf-parser",
|
||||
@ -2535,60 +2520,6 @@ version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"wasm-bindgen-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.16",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.16",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93"
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.7"
|
||||
@ -2635,15 +2566,6 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
|
||||
dependencies = [
|
||||
"windows-targets 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.45.0"
|
||||
|
@ -63,6 +63,7 @@ unicode-segmentation = "1"
|
||||
unscanny = "0.1"
|
||||
usvg = { version = "0.32", default-features = false, features = ["text"] }
|
||||
xmp-writer = "0.1"
|
||||
time = { version = "0.3.20", features = ["std", "formatting"] }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
stacker = "0.1.15"
|
||||
|
@ -23,7 +23,6 @@ doc = false
|
||||
typst = { path = ".." }
|
||||
typst-library = { path = "../library" }
|
||||
atty = "0.2"
|
||||
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
|
||||
clap = { version = "4.2.4", features = ["derive", "env"] }
|
||||
codespan-reporting = "0.11"
|
||||
comemo = "0.3"
|
||||
@ -37,6 +36,7 @@ open = "4.0.2"
|
||||
same-file = "1"
|
||||
siphasher = "0.3"
|
||||
tempfile = "3.5.0"
|
||||
time = { version = "0.3.20", features = ["formatting", "local-offset", "macros"] }
|
||||
tracing = "0.1.37"
|
||||
tracing-error = "0.2"
|
||||
tracing-flame = "0.2.0"
|
||||
|
@ -6,6 +6,7 @@ use std::collections::HashMap;
|
||||
use std::fs::{self, File};
|
||||
use std::hash::Hash;
|
||||
use std::io::{self, Write};
|
||||
use std::ops::Add;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::ExitCode;
|
||||
|
||||
@ -21,8 +22,10 @@ use once_cell::unsync::OnceCell;
|
||||
use same_file::{is_same_file, Handle};
|
||||
use siphasher::sip128::{Hasher128, SipHasher13};
|
||||
use termcolor::{ColorChoice, StandardStream, WriteColor};
|
||||
use time::macros::format_description;
|
||||
use time::Duration;
|
||||
use typst::diag::{FileError, FileResult, SourceError, StrResult};
|
||||
use typst::eval::Library;
|
||||
use typst::eval::{Datetime, Library};
|
||||
use typst::font::{Font, FontBook, FontInfo, FontVariant};
|
||||
use typst::syntax::{Source, SourceId};
|
||||
use typst::util::{Buffer, PathExt};
|
||||
@ -291,8 +294,8 @@ fn status(command: &CompileSettings, status: Status) -> io::Result<()> {
|
||||
let esc = 27 as char;
|
||||
let input = command.input.display();
|
||||
let output = command.output.display();
|
||||
let time = chrono::offset::Local::now();
|
||||
let timestamp = time.format("%H:%M:%S");
|
||||
let time = time::OffsetDateTime::now_local().unwrap();
|
||||
let timestamp = time.format(format_description!("[hour]:[minute]:[second]")).unwrap();
|
||||
let message = status.message();
|
||||
let color = status.color();
|
||||
|
||||
@ -427,6 +430,7 @@ struct SystemWorld {
|
||||
hashes: RefCell<HashMap<PathBuf, FileResult<PathHash>>>,
|
||||
paths: RefCell<HashMap<PathHash, PathSlot>>,
|
||||
sources: FrozenVec<Box<Source>>,
|
||||
current_date: Cell<Option<Datetime>>,
|
||||
main: SourceId,
|
||||
}
|
||||
|
||||
@ -457,6 +461,7 @@ impl SystemWorld {
|
||||
hashes: RefCell::default(),
|
||||
paths: RefCell::default(),
|
||||
sources: FrozenVec::new(),
|
||||
current_date: Cell::new(None),
|
||||
main: SourceId::detached(),
|
||||
}
|
||||
}
|
||||
@ -511,6 +516,23 @@ impl World for SystemWorld {
|
||||
.get_or_init(|| read(path).map(Buffer::from))
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn today(&self, offset: Option<i64>) -> Option<Datetime> {
|
||||
if self.current_date.get().is_none() {
|
||||
let datetime = match offset {
|
||||
None => time::OffsetDateTime::now_local().ok()?,
|
||||
Some(o) => time::OffsetDateTime::now_utc().add(Duration::hours(o)),
|
||||
};
|
||||
|
||||
self.current_date.set(Some(Datetime::from_ymd(
|
||||
datetime.year(),
|
||||
datetime.month().try_into().ok()?,
|
||||
datetime.day(),
|
||||
)?))
|
||||
}
|
||||
|
||||
self.current_date.get()
|
||||
}
|
||||
}
|
||||
|
||||
impl SystemWorld {
|
||||
@ -572,6 +594,7 @@ impl SystemWorld {
|
||||
self.sources.as_mut().clear();
|
||||
self.hashes.borrow_mut().clear();
|
||||
self.paths.borrow_mut().clear();
|
||||
self.current_date.set(None);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ use md::escape::escape_html;
|
||||
use pulldown_cmark as md;
|
||||
use typed_arena::Arena;
|
||||
use typst::diag::FileResult;
|
||||
use typst::eval::Datetime;
|
||||
use typst::font::{Font, FontBook};
|
||||
use typst::geom::{Point, Size};
|
||||
use typst::syntax::{Source, SourceId};
|
||||
@ -489,4 +490,8 @@ impl World for DocWorld {
|
||||
.contents()
|
||||
.into())
|
||||
}
|
||||
|
||||
fn today(&self, _: Option<i64>) -> Option<Datetime> {
|
||||
Some(Datetime::from_ymd(1970, 1, 1).unwrap())
|
||||
}
|
||||
}
|
||||
|
@ -931,6 +931,7 @@ const TYPE_ORDER: &[&str] = &[
|
||||
"relative length",
|
||||
"fraction",
|
||||
"color",
|
||||
"datetime",
|
||||
"string",
|
||||
"regex",
|
||||
"label",
|
||||
|
@ -174,6 +174,147 @@ Produces the negative of the color.
|
||||
|
||||
- returns: color
|
||||
|
||||
# Datetime
|
||||
Represents a date, a time, or a combination of both. Can be created by either
|
||||
specifying a custom datetime using the [`datetime`]($func/datetime) function or
|
||||
getting the current date with [`datetime.today`]($func/datetime.today).
|
||||
|
||||
## Example
|
||||
```example
|
||||
#let date = datetime(
|
||||
year: 2020,
|
||||
month: 10,
|
||||
day: 4,
|
||||
)
|
||||
|
||||
#date.display() \
|
||||
#date.display("y:[year repr:last_two]")
|
||||
|
||||
#let time = datetime(
|
||||
hour: 18,
|
||||
minute: 2,
|
||||
second: 23,
|
||||
)
|
||||
|
||||
#time.display() \
|
||||
#time.display("h:[hour repr:12][period]")
|
||||
```
|
||||
|
||||
## Format
|
||||
You can specify a customized formatting using the `display` method.
|
||||
The format of a datetime is specified by providing
|
||||
_components_ with a specified number of _modifiers_. A component represents a
|
||||
certain part of the datetime that you want to display, and with the help of
|
||||
modifiers you can define how you want to display that component. In order to
|
||||
display a component, you wrap the name of the component in square brackets
|
||||
(e.g. `[year]` will display the year). In order to add modifiers,
|
||||
you add a space after the component name followed by the name of the modifier,
|
||||
a colon and the value of the modifier (e.g. `[month repr:short]` will display
|
||||
the short representation of the month).
|
||||
|
||||
The possible combination of components and their respective modifiers is as
|
||||
follows:
|
||||
|
||||
* `year`: Displays the year of the datetime.
|
||||
* `padding`: Can be either `zero`, `space` or `none`. Specifies how the year
|
||||
is padded.
|
||||
* `repr` Can be either `full` in which case the full year is displayed or
|
||||
`last_two` in which case only the last two digits are displayed.
|
||||
* `sign`: Can be either `automatic` or `mandatory`. Specifies when the sign
|
||||
should be displayed.
|
||||
* `month`: Displays the month of the datetime.
|
||||
* `padding`: Can be either `zero`, `space` or `none`. Specifies how the month
|
||||
is padded.
|
||||
* `repr`: Can be either `numerical`, `long` or `short`. Specifies if the month
|
||||
should be displayed as a number or a word. Unfortunately, when choosing the
|
||||
word representation, it can currently only display the English version. In
|
||||
the future, it is planned to support localization.
|
||||
* `day`: Displays the day of the datetime.
|
||||
* `padding`: Can be either `zero`, `space` or `none`. Specifies how the day
|
||||
is padded.
|
||||
* `week_number`: Displays the week number of the datetime.
|
||||
* `padding`: Can be either `zero`, `space` or `none`. Specifies how the week
|
||||
number is padded.
|
||||
* `repr`: Can be either `ISO`, `sunday` or `monday`. In the case of `ISO`,
|
||||
week numbers are between 1 and 53, while the other ones are between 0
|
||||
and 53.
|
||||
* `weekday`: Displays the weekday of the date.
|
||||
* `repr` Can be either `long`, `short`, `sunday` or `monday`. In the case of
|
||||
`long` and `short`, the corresponding English name will be displayed (same
|
||||
as for the month, other languages are currently not supported). In the case
|
||||
of `sunday` and `monday`, the numerical value will be displayed (assuming
|
||||
Sunday and Monday as the first day of the week, respectively).
|
||||
* `one_indexed`: Can be either `true` or `false`. Defines whether the
|
||||
numerical representation of the week starts with 0 or 1.
|
||||
* `hour`: Displays the hour of the date.
|
||||
* `padding`: Can be either `zero`, `space` or `none`. Specifies how the hour
|
||||
is padded.
|
||||
* `repr`: Can be either `24` or `12`. Changes whether the hour is displayed in
|
||||
the 24-hour or 12-hour format.
|
||||
* `period`: The AM/PM part of the hour
|
||||
* `case`: Can be `lower` to display it in lower case and `upper` to display it
|
||||
in upper case.
|
||||
* `minute`: Displays the minute of the date.
|
||||
* `padding`: Can be either `zero`, `space` or `none`. Specifies how the minute
|
||||
is padded.
|
||||
* `second`: Displays the second of the date.
|
||||
* `padding`: Can be either `zero`, `space` or `none`. Specifies how the second
|
||||
is padded.
|
||||
|
||||
Keep in mind that not always all components can be used. For example, if
|
||||
you create a new datetime with `#datetime(year: 2023, month: 10, day: 13)`, it
|
||||
will be stored as a plain date internally, meaning that you cannot use
|
||||
components such as `hour` or `minute`, which would only work on datetimes
|
||||
that have a specified time.
|
||||
|
||||
## Methods
|
||||
### display()
|
||||
Displays the datetime in a certain way. Depending on whether you have defined
|
||||
just a date, a time or both, the default format will be different.
|
||||
If you specified a date, it will be `[year]-[month]-[day]`. If you specified a
|
||||
time, it will be `[hour]:[minute]:[second]`. In the case of a datetime, it will
|
||||
be `[year]-[month]-[day] [hour]:[minute]:[second]`.
|
||||
|
||||
- pattern: string (positional)
|
||||
The format used to display the datetime.
|
||||
- returns: string
|
||||
|
||||
### year()
|
||||
Returns the year of the datetime, if it exists. Otherwise, it returns `none`.
|
||||
|
||||
- returns: integer or none
|
||||
|
||||
### month()
|
||||
Returns the month of the datetime, if it exists. Otherwise, it returns `none`.
|
||||
|
||||
- returns: integer or none
|
||||
|
||||
### weekday()
|
||||
Returns the weekday of the datetime as a number starting with 1 from Monday, if
|
||||
it exists. Otherwise, it returns `none`.
|
||||
|
||||
- returns: integer or none
|
||||
|
||||
### day()
|
||||
Returns the day of the datetime, if it exists. Otherwise, it returns `none`.
|
||||
|
||||
- returns: integer or none
|
||||
|
||||
### hour()
|
||||
Returns the hour of the datetime, if it exists. Otherwise, it returns `none`.
|
||||
|
||||
- returns: integer or none
|
||||
|
||||
### minute()
|
||||
Returns the minute of the datetime, if it exists. Otherwise, it returns `none`.
|
||||
|
||||
- returns: integer or none
|
||||
|
||||
### second()
|
||||
Returns the second of the datetime, if it exists. Otherwise, it returns `none`.
|
||||
|
||||
- returns: integer or none
|
||||
|
||||
# Symbol
|
||||
A Unicode symbol.
|
||||
|
||||
|
@ -35,6 +35,7 @@ serde_json = "1"
|
||||
serde_yaml = "0.8"
|
||||
smallvec = "1.10"
|
||||
syntect = { version = "5", default-features = false, features = ["default-syntaxes", "regex-fancy"] }
|
||||
time = { version = "0.3.20", features = ["formatting"] }
|
||||
toml = { version = "0.7.3", default-features = false, features = ["parse"] }
|
||||
tracing = "0.1.37"
|
||||
ttf-parser = "0.18.1"
|
||||
|
@ -1,7 +1,9 @@
|
||||
use std::num::NonZeroI64;
|
||||
use std::str::FromStr;
|
||||
|
||||
use typst::eval::Regex;
|
||||
use time::{Month, PrimitiveDateTime};
|
||||
|
||||
use typst::eval::{Datetime, Dynamic, Regex};
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
@ -179,6 +181,175 @@ cast_from_value! {
|
||||
},
|
||||
}
|
||||
|
||||
/// Create a new datetime.
|
||||
///
|
||||
/// You can specify the [datetime]($type/datetime) using a year, month, day,
|
||||
/// hour, minute, and second.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```example
|
||||
/// #datetime(
|
||||
/// year: 2012,
|
||||
/// month: 8,
|
||||
/// day: 3,
|
||||
/// ).display()
|
||||
/// ```
|
||||
///
|
||||
/// ## Format
|
||||
/// _Note_: Depending on which components of the datetime you specify, Typst
|
||||
/// will store it in one of the following three ways:
|
||||
/// * If you specify year, month and day, Typst will store just a date.
|
||||
/// * If you specify hour, minute and second, Typst will store just a time.
|
||||
/// * If you specify all of year, month, day, hour, minute and second, Typst
|
||||
/// will store a full datetime.
|
||||
///
|
||||
/// Depending on how it is stored, the [`display`]($type/datetime.display)
|
||||
/// method will choose a different formatting by default.
|
||||
///
|
||||
/// Display: Datetime
|
||||
/// Category: construct
|
||||
/// Returns: datetime
|
||||
#[func]
|
||||
#[scope(
|
||||
scope.define("today", datetime_today);
|
||||
scope
|
||||
)]
|
||||
pub fn datetime(
|
||||
/// The year of the datetime.
|
||||
#[named]
|
||||
year: Option<YearComponent>,
|
||||
/// The month of the datetime.
|
||||
#[named]
|
||||
month: Option<MonthComponent>,
|
||||
/// The day of the datetime.
|
||||
#[named]
|
||||
day: Option<DayComponent>,
|
||||
/// The hour of the datetime.
|
||||
#[named]
|
||||
hour: Option<HourComponent>,
|
||||
/// The minute of the datetime.
|
||||
#[named]
|
||||
minute: Option<MinuteComponent>,
|
||||
/// The second of the datetime.
|
||||
#[named]
|
||||
second: Option<SecondComponent>,
|
||||
) -> Value {
|
||||
let time = match (hour, minute, second) {
|
||||
(Some(hour), Some(minute), Some(second)) => {
|
||||
match time::Time::from_hms(hour.0, minute.0, second.0) {
|
||||
Ok(time) => Some(time),
|
||||
Err(_) => bail!(args.span, "time is invalid"),
|
||||
}
|
||||
}
|
||||
(None, None, None) => None,
|
||||
_ => bail!(args.span, "time is incomplete"),
|
||||
};
|
||||
|
||||
let date = match (year, month, day) {
|
||||
(Some(year), Some(month), Some(day)) => {
|
||||
match time::Date::from_calendar_date(year.0, month.0, day.0) {
|
||||
Ok(date) => Some(date),
|
||||
Err(_) => bail!(args.span, "date is invalid"),
|
||||
}
|
||||
}
|
||||
(None, None, None) => None,
|
||||
_ => bail!(args.span, "date is incomplete"),
|
||||
};
|
||||
|
||||
match (date, time) {
|
||||
(Some(date), Some(time)) => Value::Dyn(Dynamic::new(Datetime::Datetime(
|
||||
PrimitiveDateTime::new(date, time),
|
||||
))),
|
||||
(Some(date), None) => Value::Dyn(Dynamic::new(Datetime::Date(date))),
|
||||
(None, Some(time)) => Value::Dyn(Dynamic::new(Datetime::Time(time))),
|
||||
(None, None) => {
|
||||
bail!(args.span, "at least one of date or time must be fully specified")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct YearComponent(i32);
|
||||
struct MonthComponent(Month);
|
||||
struct DayComponent(u8);
|
||||
struct HourComponent(u8);
|
||||
struct MinuteComponent(u8);
|
||||
struct SecondComponent(u8);
|
||||
|
||||
cast_from_value!(
|
||||
YearComponent,
|
||||
v: i64 => match i32::try_from(v) {
|
||||
Ok(n) => Self(n),
|
||||
_ => Err("year is invalid")?
|
||||
}
|
||||
);
|
||||
|
||||
cast_from_value!(
|
||||
MonthComponent,
|
||||
v: i64 => match u8::try_from(v).ok().and_then(|n1| Month::try_from(n1).ok()).map(Self) {
|
||||
Some(m) => m,
|
||||
_ => Err("month is invalid")?
|
||||
}
|
||||
);
|
||||
|
||||
cast_from_value!(
|
||||
DayComponent,
|
||||
v: i64 => match u8::try_from(v) {
|
||||
Ok(n) => Self(n),
|
||||
_ => Err("day is invalid")?
|
||||
}
|
||||
);
|
||||
|
||||
cast_from_value!(
|
||||
HourComponent,
|
||||
v: i64 => match u8::try_from(v) {
|
||||
Ok(n) => Self(n),
|
||||
_ => Err("hour is invalid")?
|
||||
}
|
||||
);
|
||||
|
||||
cast_from_value!(
|
||||
MinuteComponent,
|
||||
v: i64 => match u8::try_from(v) {
|
||||
Ok(n) => Self(n),
|
||||
_ => Err("minute is invalid")?
|
||||
}
|
||||
);
|
||||
|
||||
cast_from_value!(
|
||||
SecondComponent,
|
||||
v: i64 => match u8::try_from(v) {
|
||||
Ok(n) => Self(n),
|
||||
_ => Err("second is invalid")?
|
||||
}
|
||||
);
|
||||
|
||||
/// Returns the current date.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```example
|
||||
/// Today's date is
|
||||
/// #datetime.today().display().
|
||||
/// ```
|
||||
///
|
||||
/// Display: Today
|
||||
/// Category: construct
|
||||
/// Returns: datetime
|
||||
#[func]
|
||||
pub fn datetime_today(
|
||||
/// An offset to apply to the current UTC date. If set to `{auto}`, the
|
||||
/// offset will be the local offset.
|
||||
#[named]
|
||||
#[default]
|
||||
offset: Smart<i64>,
|
||||
) -> Value {
|
||||
let current_date = match vm.vt.world.today(offset.as_custom()) {
|
||||
Some(d) => d,
|
||||
None => bail!(args.span, "unable to get the current date"),
|
||||
};
|
||||
|
||||
Value::Dyn(Dynamic::new(current_date))
|
||||
}
|
||||
|
||||
/// Create a CMYK color.
|
||||
///
|
||||
/// This is useful if you want to target a specific printer. The conversion
|
||||
|
@ -23,6 +23,7 @@ pub(super) fn define(global: &mut Scope) {
|
||||
global.define("luma", luma);
|
||||
global.define("rgb", rgb);
|
||||
global.define("cmyk", cmyk);
|
||||
global.define("datetime", datetime);
|
||||
global.define("symbol", symbol);
|
||||
global.define("str", str);
|
||||
global.define("label", label);
|
||||
|
201
src/eval/datetime.rs
Normal file
201
src/eval/datetime.rs
Normal file
@ -0,0 +1,201 @@
|
||||
use std::fmt;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::hash::Hash;
|
||||
|
||||
use ecow::{eco_format, EcoString, EcoVec};
|
||||
use time::error::{Format, InvalidFormatDescription};
|
||||
use time::{format_description, PrimitiveDateTime};
|
||||
|
||||
use crate::eval::cast_from_value;
|
||||
use crate::util::pretty_array_like;
|
||||
|
||||
/// A datetime object that represents either a date, a time or a combination of
|
||||
/// both.
|
||||
#[derive(Clone, Copy, PartialEq, Hash)]
|
||||
pub enum Datetime {
|
||||
/// Representation as a date.
|
||||
Date(time::Date),
|
||||
/// Representation as a time.
|
||||
Datetime(time::PrimitiveDateTime),
|
||||
/// Representation as a combination of date and time.
|
||||
Time(time::Time),
|
||||
}
|
||||
|
||||
impl Datetime {
|
||||
/// Display the date and/or time in a certain format.
|
||||
pub fn display(&self, pattern: Option<EcoString>) -> Result<EcoString, EcoString> {
|
||||
let pattern = pattern.as_ref().map(EcoString::as_str).unwrap_or(match self {
|
||||
Datetime::Date(_) => "[year]-[month]-[day]",
|
||||
Datetime::Time(_) => "[hour]:[minute]:[second]",
|
||||
Datetime::Datetime(_) => "[year]-[month]-[day] [hour]:[minute]:[second]",
|
||||
});
|
||||
|
||||
let format = format_description::parse(pattern)
|
||||
.map_err(format_time_invalid_format_description_error)?;
|
||||
|
||||
let formatted_result = match self {
|
||||
Datetime::Date(date) => date.format(&format),
|
||||
Datetime::Time(time) => time.format(&format),
|
||||
Datetime::Datetime(datetime) => datetime.format(&format),
|
||||
}
|
||||
.map(EcoString::from);
|
||||
|
||||
formatted_result.map_err(format_time_format_error)
|
||||
}
|
||||
|
||||
/// Return the year of the datetime, if existing.
|
||||
pub fn year(&self) -> Option<i32> {
|
||||
match self {
|
||||
Datetime::Date(date) => Some(date.year()),
|
||||
Datetime::Time(_) => None,
|
||||
Datetime::Datetime(datetime) => Some(datetime.year()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the month of the datetime, if existing.
|
||||
pub fn month(&self) -> Option<u8> {
|
||||
match self {
|
||||
Datetime::Date(date) => Some(date.month().into()),
|
||||
Datetime::Time(_) => None,
|
||||
Datetime::Datetime(datetime) => Some(datetime.month().into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the weekday of the datetime, if existing.
|
||||
pub fn weekday(&self) -> Option<u8> {
|
||||
match self {
|
||||
Datetime::Date(date) => Some(date.weekday().number_from_monday()),
|
||||
Datetime::Time(_) => None,
|
||||
Datetime::Datetime(datetime) => Some(datetime.weekday().number_from_monday()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the day of the datetime, if existing.
|
||||
pub fn day(&self) -> Option<u8> {
|
||||
match self {
|
||||
Datetime::Date(date) => Some(date.day()),
|
||||
Datetime::Time(_) => None,
|
||||
Datetime::Datetime(datetime) => Some(datetime.day()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the hour of the datetime, if existing.
|
||||
pub fn hour(&self) -> Option<u8> {
|
||||
match self {
|
||||
Datetime::Date(_) => None,
|
||||
Datetime::Time(time) => Some(time.hour()),
|
||||
Datetime::Datetime(datetime) => Some(datetime.hour()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the minute of the datetime, if existing.
|
||||
pub fn minute(&self) -> Option<u8> {
|
||||
match self {
|
||||
Datetime::Date(_) => None,
|
||||
Datetime::Time(time) => Some(time.minute()),
|
||||
Datetime::Datetime(datetime) => Some(datetime.minute()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the second of the datetime, if existing.
|
||||
pub fn second(&self) -> Option<u8> {
|
||||
match self {
|
||||
Datetime::Date(_) => None,
|
||||
Datetime::Time(time) => Some(time.second()),
|
||||
Datetime::Datetime(datetime) => Some(datetime.second()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a datetime from year, month, and day.
|
||||
pub fn from_ymd(year: i32, month: u8, day: u8) -> Option<Self> {
|
||||
Some(Datetime::Date(
|
||||
time::Date::from_calendar_date(year, time::Month::try_from(month).ok()?, day)
|
||||
.ok()?,
|
||||
))
|
||||
}
|
||||
|
||||
/// Create a datetime from hour, minute, and second.
|
||||
pub fn from_hms(hour: u8, minute: u8, second: u8) -> Option<Self> {
|
||||
Some(Datetime::Time(time::Time::from_hms(hour, minute, second).ok()?))
|
||||
}
|
||||
|
||||
/// Create a datetime from day and time.
|
||||
pub fn from_ymd_hms(
|
||||
year: i32,
|
||||
month: u8,
|
||||
day: u8,
|
||||
hour: u8,
|
||||
minute: u8,
|
||||
second: u8,
|
||||
) -> Option<Self> {
|
||||
let date =
|
||||
time::Date::from_calendar_date(year, time::Month::try_from(month).ok()?, day)
|
||||
.ok()?;
|
||||
let time = time::Time::from_hms(hour, minute, second).ok()?;
|
||||
Some(Datetime::Datetime(PrimitiveDateTime::new(date, time)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Datetime {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
let year = self.year().map(|y| eco_format!("year: {y}"));
|
||||
let month = self.month().map(|m| eco_format!("month: {m}"));
|
||||
let day = self.day().map(|d| eco_format!("day: {d}"));
|
||||
let hour = self.hour().map(|h| eco_format!("hour: {h}"));
|
||||
let minute = self.minute().map(|m| eco_format!("minute: {m}"));
|
||||
let second = self.second().map(|s| eco_format!("second: {s}"));
|
||||
let filtered = [year, month, day, hour, minute, second]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<EcoVec<_>>();
|
||||
|
||||
write!(f, "datetime{}", &pretty_array_like(&filtered, false))
|
||||
}
|
||||
}
|
||||
|
||||
cast_from_value! {
|
||||
Datetime: "datetime",
|
||||
}
|
||||
|
||||
/// Format the `Format` error of the time crate in an appropriate way.
|
||||
fn format_time_format_error(error: Format) -> EcoString {
|
||||
match error {
|
||||
Format::InvalidComponent(name) => eco_format!("invalid component '{}'", name),
|
||||
_ => "failed to format datetime in the requested format".into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Format the `InvalidFormatDescription` error of the time crate in an
|
||||
/// appropriate way.
|
||||
fn format_time_invalid_format_description_error(
|
||||
error: InvalidFormatDescription,
|
||||
) -> EcoString {
|
||||
match error {
|
||||
InvalidFormatDescription::UnclosedOpeningBracket { index, .. } => {
|
||||
eco_format!("missing closing bracket for bracket at index {}", index)
|
||||
}
|
||||
InvalidFormatDescription::InvalidComponentName { name, index, .. } => {
|
||||
eco_format!("invalid component name '{}' at index {}", name, index)
|
||||
}
|
||||
InvalidFormatDescription::InvalidModifier { value, index, .. } => {
|
||||
eco_format!("invalid modifier '{}' at index {}", value, index)
|
||||
}
|
||||
InvalidFormatDescription::Expected { what, index, .. } => {
|
||||
eco_format!("expected {} at index {}", what, index)
|
||||
}
|
||||
InvalidFormatDescription::MissingComponentName { index, .. } => {
|
||||
eco_format!("expected component name at index {}", index)
|
||||
}
|
||||
InvalidFormatDescription::MissingRequiredModifier { name, index, .. } => {
|
||||
eco_format!(
|
||||
"missing required modifier {} for component at index {}",
|
||||
name,
|
||||
index
|
||||
)
|
||||
}
|
||||
InvalidFormatDescription::NotSupported { context, what, index, .. } => {
|
||||
eco_format!("{} is not supported in {} at index {}", what, context, index)
|
||||
}
|
||||
_ => "failed to parse datetime format".into(),
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ use ecow::EcoString;
|
||||
|
||||
use super::{Args, Str, Value, Vm};
|
||||
use crate::diag::{At, SourceResult};
|
||||
use crate::eval::Datetime;
|
||||
use crate::model::{Location, Selector};
|
||||
use crate::syntax::Span;
|
||||
|
||||
@ -185,6 +186,30 @@ pub fn call(
|
||||
}
|
||||
_ => return missing(),
|
||||
}
|
||||
} else if let Some(&datetime) = dynamic.downcast::<Datetime>() {
|
||||
match method {
|
||||
"display" => datetime.display(args.eat()?).at(args.span)?.into(),
|
||||
"year" => {
|
||||
datetime.year().map_or(Value::None, |y| Value::Int(y.into()))
|
||||
}
|
||||
"month" => {
|
||||
datetime.month().map_or(Value::None, |m| Value::Int(m.into()))
|
||||
}
|
||||
"weekday" => {
|
||||
datetime.weekday().map_or(Value::None, |w| Value::Int(w.into()))
|
||||
}
|
||||
"day" => datetime.day().map_or(Value::None, |d| Value::Int(d.into())),
|
||||
"hour" => {
|
||||
datetime.hour().map_or(Value::None, |h| Value::Int(h.into()))
|
||||
}
|
||||
"minute" => {
|
||||
datetime.minute().map_or(Value::None, |m| Value::Int(m.into()))
|
||||
}
|
||||
"second" => {
|
||||
datetime.second().map_or(Value::None, |s| Value::Int(s.into()))
|
||||
}
|
||||
_ => return missing(),
|
||||
}
|
||||
} else {
|
||||
return (vm.items.library_method)(vm, &dynamic, method, args, span);
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ mod str;
|
||||
#[macro_use]
|
||||
mod value;
|
||||
mod args;
|
||||
mod datetime;
|
||||
mod func;
|
||||
mod methods;
|
||||
mod module;
|
||||
@ -26,6 +27,7 @@ pub use once_cell::sync::Lazy;
|
||||
pub use self::args::*;
|
||||
pub use self::array::*;
|
||||
pub use self::cast::*;
|
||||
pub use self::datetime::*;
|
||||
pub use self::dict::*;
|
||||
pub use self::func::*;
|
||||
pub use self::library::*;
|
||||
|
@ -58,7 +58,7 @@ use comemo::{Prehashed, Track, TrackedMut};
|
||||
|
||||
use crate::diag::{FileResult, SourceResult};
|
||||
use crate::doc::Document;
|
||||
use crate::eval::{Library, Route, Tracer};
|
||||
use crate::eval::{Datetime, Library, Route, Tracer};
|
||||
use crate::font::{Font, FontBook};
|
||||
use crate::syntax::{Source, SourceId};
|
||||
use crate::util::Buffer;
|
||||
@ -116,4 +116,10 @@ pub trait World {
|
||||
|
||||
/// Try to access a file at a path.
|
||||
fn file(&self, path: &Path) -> FileResult<Buffer>;
|
||||
|
||||
/// Get the current date.
|
||||
///
|
||||
/// If no offset is specified, the local date should be chosen. Otherwise,
|
||||
/// the UTC date should be chosen with the corresponding offset in hours.
|
||||
fn today(&self, offset: Option<i64>) -> Option<Datetime>;
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ use std::path::Path;
|
||||
use comemo::{Prehashed, Track, Tracked};
|
||||
use iai::{black_box, main, Iai};
|
||||
use typst::diag::{FileError, FileResult};
|
||||
use typst::eval::Library;
|
||||
use typst::eval::{Datetime, Library};
|
||||
use typst::font::{Font, FontBook};
|
||||
use typst::geom::Color;
|
||||
use typst::syntax::{Source, SourceId};
|
||||
@ -147,4 +147,8 @@ impl World for BenchWorld {
|
||||
fn file(&self, path: &Path) -> FileResult<Buffer> {
|
||||
Err(FileError::NotFound(path.into()))
|
||||
}
|
||||
|
||||
fn today(&self, _: Option<i64>) -> Option<Datetime> {
|
||||
Some(Datetime::from_ymd(1970, 1, 1).unwrap())
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ use walkdir::WalkDir;
|
||||
|
||||
use typst::diag::{bail, FileError, FileResult};
|
||||
use typst::doc::{Document, Frame, FrameItem, Meta};
|
||||
use typst::eval::{func, Library, Value};
|
||||
use typst::eval::{func, Datetime, Library, Value};
|
||||
use typst::font::{Font, FontBook};
|
||||
use typst::geom::{Abs, Color, RgbaColor, Sides, Smart};
|
||||
use typst::syntax::{Source, SourceId, Span, SyntaxNode};
|
||||
@ -296,6 +296,10 @@ impl World for TestWorld {
|
||||
.get_or_init(|| read(path).map(Buffer::from))
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn today(&self, _: Option<i64>) -> Option<Datetime> {
|
||||
Some(Datetime::from_ymd(1970, 1, 1).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl TestWorld {
|
||||
|
@ -73,3 +73,89 @@
|
||||
|
||||
---
|
||||
#assert(range(2, 5) == (2, 3, 4))
|
||||
|
||||
---
|
||||
// Test displaying of dates.
|
||||
#test(datetime(year: 2023, month: 4, day: 29).display(), "2023-04-29")
|
||||
#test(datetime(year: 2023, month: 4, day: 29).display("[year]"), "2023")
|
||||
#test(
|
||||
datetime(year: 2023, month: 4, day: 29)
|
||||
.display("[year repr:last_two]"),
|
||||
"23",
|
||||
)
|
||||
#test(
|
||||
datetime(year: 2023, month: 4, day: 29)
|
||||
.display("[year] [month repr:long] [day] [week_number] [weekday]"),
|
||||
"2023 April 29 17 Saturday",
|
||||
)
|
||||
|
||||
// Test displaying of times
|
||||
#test(datetime(hour: 14, minute: 26, second: 50).display(), "14:26:50")
|
||||
#test(datetime(hour: 14, minute: 26, second: 50).display("[hour]"), "14")
|
||||
#test(
|
||||
datetime(hour: 14, minute: 26, second: 50)
|
||||
.display("[hour repr:12 padding:none]"),
|
||||
"2",
|
||||
)
|
||||
#test(
|
||||
datetime(hour: 14, minute: 26, second: 50)
|
||||
.display("[hour], [minute], [second]"), "14, 26, 50",
|
||||
)
|
||||
|
||||
// Test displaying of datetimes
|
||||
#test(
|
||||
datetime(year: 2023, month: 4, day: 29, hour: 14, minute: 26, second: 50).display(),
|
||||
"2023-04-29 14:26:50",
|
||||
)
|
||||
|
||||
// Test getting the year/month/day etc. of a datetime
|
||||
#let d = datetime(year: 2023, month: 4, day: 29, hour: 14, minute: 26, second: 50)
|
||||
#test(d.year(), 2023)
|
||||
#test(d.month(), 4)
|
||||
#test(d.weekday(), 6)
|
||||
#test(d.day(), 29)
|
||||
#test(d.hour(), 14)
|
||||
#test(d.minute(), 26)
|
||||
#test(d.second(), 50)
|
||||
|
||||
#let e = datetime(year: 2023, month: 4, day: 29)
|
||||
#test(e.hour(), none)
|
||||
#test(e.minute(), none)
|
||||
#test(e.second(), none)
|
||||
|
||||
// Test today
|
||||
#test(datetime.today().display(), "1970-01-01")
|
||||
#test(datetime.today(offset: auto).display(), "1970-01-01")
|
||||
#test(datetime.today(offset: 2).display(), "1970-01-01")
|
||||
|
||||
---
|
||||
// Error: 10-12 at least one of date or time must be fully specified
|
||||
#datetime()
|
||||
|
||||
---
|
||||
// Error: 10-42 time is invalid
|
||||
#datetime(hour: 25, minute: 0, second: 0)
|
||||
|
||||
---
|
||||
// Error: 10-41 date is invalid
|
||||
#datetime(year: 2000, month: 2, day: 30)
|
||||
|
||||
---
|
||||
// Error: 26-35 missing closing bracket for bracket at index 0
|
||||
#datetime.today().display("[year")
|
||||
|
||||
---
|
||||
// Error: 26-39 invalid component name 'nothing' at index 1
|
||||
#datetime.today().display("[nothing]")
|
||||
|
||||
---
|
||||
// Error: 26-51 invalid modifier 'wrong' at index 6
|
||||
#datetime.today().display("[year wrong:last_two]")
|
||||
|
||||
---
|
||||
// Error: 26-34 expected component name at index 2
|
||||
#datetime.today().display(" []")
|
||||
|
||||
---
|
||||
// Error: 26-36 failed to format datetime in the requested format
|
||||
#datetime.today().display("[hour]")
|
||||
|
Loading…
Reference in New Issue
Block a user