76
Cargo.lock
generated
76
Cargo.lock
generated
@@ -1415,6 +1415,15 @@ dependencies = [
|
|||||||
"winreg",
|
"winreg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iso8601-duration"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "60b51dd97fa24074214b9eb14da518957573f4dec3189112610ae1ccec9ac464"
|
||||||
|
dependencies = [
|
||||||
|
"nom 5.1.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "0.4.7"
|
version = "0.4.7"
|
||||||
@@ -1776,6 +1785,17 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
|
checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nom"
|
||||||
|
version = "5.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af"
|
||||||
|
dependencies = [
|
||||||
|
"lexical-core",
|
||||||
|
"memchr",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nom"
|
name = "nom"
|
||||||
version = "6.1.2"
|
version = "6.1.2"
|
||||||
@@ -2413,6 +2433,19 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rstest"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "041bb0202c14f6a158bbbf086afb03d0c6e975c2dec7d4912f8061ed44f290af"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if 1.0.0",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rustc_version 0.3.3",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-argon2"
|
name = "rust-argon2"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
@@ -2431,7 +2464,16 @@ version = "0.2.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
|
checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"semver",
|
"semver 0.9.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc_version"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee"
|
||||||
|
dependencies = [
|
||||||
|
"semver 0.11.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2502,7 +2544,16 @@ version = "0.9.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
|
checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"semver-parser",
|
"semver-parser 0.7.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "semver"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
|
||||||
|
dependencies = [
|
||||||
|
"semver-parser 0.10.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2511,6 +2562,15 @@ version = "0.7.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
|
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "semver-parser"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7"
|
||||||
|
dependencies = [
|
||||||
|
"pest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.125"
|
version = "1.0.125"
|
||||||
@@ -2675,7 +2735,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5"
|
checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"discard",
|
"discard",
|
||||||
"rustc_version",
|
"rustc_version 0.2.3",
|
||||||
"stdweb-derive",
|
"stdweb-derive",
|
||||||
"stdweb-internal-macros",
|
"stdweb-internal-macros",
|
||||||
"stdweb-internal-runtime",
|
"stdweb-internal-runtime",
|
||||||
@@ -2768,9 +2828,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.69"
|
version = "1.0.72"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "48fe99c6bd8b1cc636890bcc071842de909d902c81ac7dab53ba33c421ab8ffb"
|
checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -2809,13 +2869,17 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"assert_cmd",
|
"assert_cmd",
|
||||||
"atty",
|
"atty",
|
||||||
|
"chrono",
|
||||||
"dirs-next",
|
"dirs-next",
|
||||||
"env_logger 0.8.3",
|
"env_logger 0.8.3",
|
||||||
|
"iso8601-duration",
|
||||||
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
"mdbook",
|
"mdbook",
|
||||||
"nom",
|
"nom 6.1.2",
|
||||||
"predicates",
|
"predicates",
|
||||||
"prettytable-rs",
|
"prettytable-rs",
|
||||||
|
"rstest",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"taskchampion",
|
"taskchampion",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ termcolor = "^1.1.2"
|
|||||||
atty = "^0.2.14"
|
atty = "^0.2.14"
|
||||||
toml = "^0.5.8"
|
toml = "^0.5.8"
|
||||||
toml_edit = "^0.2.0"
|
toml_edit = "^0.2.0"
|
||||||
|
chrono = "0.4"
|
||||||
|
lazy_static = "1"
|
||||||
|
iso8601-duration = "0.1"
|
||||||
|
|
||||||
# only needed for usage-docs
|
# only needed for usage-docs
|
||||||
mdbook = { version = "0.4", optional = true }
|
mdbook = { version = "0.4", optional = true }
|
||||||
@@ -34,6 +37,7 @@ path = "../taskchampion"
|
|||||||
assert_cmd = "^1.0.3"
|
assert_cmd = "^1.0.3"
|
||||||
predicates = "^1.0.7"
|
predicates = "^1.0.7"
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
rstest = "0.10"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
usage-docs = [ "mdbook", "serde_json" ]
|
usage-docs = [ "mdbook", "serde_json" ]
|
||||||
|
|||||||
@@ -1,309 +0,0 @@
|
|||||||
//! Parsers for argument lists -- arrays of strings
|
|
||||||
use super::ArgList;
|
|
||||||
use nom::bytes::complete::tag as nomtag;
|
|
||||||
use nom::{
|
|
||||||
branch::*,
|
|
||||||
character::complete::*,
|
|
||||||
combinator::*,
|
|
||||||
error::{Error, ErrorKind},
|
|
||||||
multi::*,
|
|
||||||
sequence::*,
|
|
||||||
Err, IResult,
|
|
||||||
};
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
use taskchampion::{Status, Tag, Uuid};
|
|
||||||
|
|
||||||
/// A task identifier, as given in a filter command-line expression
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
|
||||||
pub(crate) enum TaskId {
|
|
||||||
/// A small integer identifying a working-set task
|
|
||||||
WorkingSetId(usize),
|
|
||||||
|
|
||||||
/// A full Uuid specifically identifying a task
|
|
||||||
Uuid(Uuid),
|
|
||||||
|
|
||||||
/// A prefix of a Uuid
|
|
||||||
PartialUuid(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Recognizes any argument
|
|
||||||
pub(super) fn any(input: &str) -> IResult<&str, &str> {
|
|
||||||
rest(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Recognizes a report name
|
|
||||||
pub(super) fn report_name(input: &str) -> IResult<&str, &str> {
|
|
||||||
all_consuming(recognize(pair(alpha1, alphanumeric0)))(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Recognizes a literal string
|
|
||||||
pub(super) fn literal(literal: &'static str) -> impl Fn(&str) -> IResult<&str, &str> {
|
|
||||||
move |input: &str| all_consuming(nomtag(literal))(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Recognizes a colon-prefixed pair
|
|
||||||
pub(super) fn colon_prefixed(prefix: &'static str) -> impl Fn(&str) -> IResult<&str, &str> {
|
|
||||||
fn to_suffix<'a>(input: (&'a str, char, &'a str)) -> Result<&'a str, ()> {
|
|
||||||
Ok(input.2)
|
|
||||||
}
|
|
||||||
move |input: &str| {
|
|
||||||
map_res(
|
|
||||||
all_consuming(tuple((nomtag(prefix), char(':'), any))),
|
|
||||||
to_suffix,
|
|
||||||
)(input)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Recognizes `status:{pending,completed,deleted}`
|
|
||||||
pub(super) fn status_colon(input: &str) -> IResult<&str, Status> {
|
|
||||||
fn to_status(input: &str) -> Result<Status, ()> {
|
|
||||||
match input {
|
|
||||||
"pending" => Ok(Status::Pending),
|
|
||||||
"completed" => Ok(Status::Completed),
|
|
||||||
"deleted" => Ok(Status::Deleted),
|
|
||||||
_ => Err(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
map_res(colon_prefixed("status"), to_status)(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Recognizes a comma-separated list of TaskIds
|
|
||||||
pub(super) fn id_list(input: &str) -> IResult<&str, Vec<TaskId>> {
|
|
||||||
fn hex_n(n: usize) -> impl Fn(&str) -> IResult<&str, &str> {
|
|
||||||
move |input: &str| recognize(many_m_n(n, n, one_of(&b"0123456789abcdefABCDEF"[..])))(input)
|
|
||||||
}
|
|
||||||
fn uuid(input: &str) -> Result<TaskId, ()> {
|
|
||||||
Ok(TaskId::Uuid(Uuid::parse_str(input).map_err(|_| ())?))
|
|
||||||
}
|
|
||||||
fn partial_uuid(input: &str) -> Result<TaskId, ()> {
|
|
||||||
Ok(TaskId::PartialUuid(input.to_owned()))
|
|
||||||
}
|
|
||||||
fn working_set_id(input: &str) -> Result<TaskId, ()> {
|
|
||||||
Ok(TaskId::WorkingSetId(input.parse().map_err(|_| ())?))
|
|
||||||
}
|
|
||||||
all_consuming(separated_list1(
|
|
||||||
char(','),
|
|
||||||
alt((
|
|
||||||
map_res(
|
|
||||||
recognize(tuple((
|
|
||||||
hex_n(8),
|
|
||||||
char('-'),
|
|
||||||
hex_n(4),
|
|
||||||
char('-'),
|
|
||||||
hex_n(4),
|
|
||||||
char('-'),
|
|
||||||
hex_n(4),
|
|
||||||
char('-'),
|
|
||||||
hex_n(12),
|
|
||||||
))),
|
|
||||||
uuid,
|
|
||||||
),
|
|
||||||
map_res(
|
|
||||||
recognize(tuple((
|
|
||||||
hex_n(8),
|
|
||||||
char('-'),
|
|
||||||
hex_n(4),
|
|
||||||
char('-'),
|
|
||||||
hex_n(4),
|
|
||||||
char('-'),
|
|
||||||
hex_n(4),
|
|
||||||
))),
|
|
||||||
partial_uuid,
|
|
||||||
),
|
|
||||||
map_res(
|
|
||||||
recognize(tuple((hex_n(8), char('-'), hex_n(4), char('-'), hex_n(4)))),
|
|
||||||
partial_uuid,
|
|
||||||
),
|
|
||||||
map_res(
|
|
||||||
recognize(tuple((hex_n(8), char('-'), hex_n(4)))),
|
|
||||||
partial_uuid,
|
|
||||||
),
|
|
||||||
map_res(hex_n(8), partial_uuid),
|
|
||||||
// note that an 8-decimal-digit value will be treated as a UUID
|
|
||||||
map_res(digit1, working_set_id),
|
|
||||||
)),
|
|
||||||
))(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Recognizes a tag prefixed with `+` and returns the tag value
|
|
||||||
pub(super) fn plus_tag(input: &str) -> IResult<&str, &str> {
|
|
||||||
fn to_tag(input: (char, &str)) -> Result<&str, ()> {
|
|
||||||
Ok(input.1)
|
|
||||||
}
|
|
||||||
map_res(
|
|
||||||
all_consuming(tuple((
|
|
||||||
char('+'),
|
|
||||||
recognize(verify(rest, |s: &str| Tag::try_from(s).is_ok())),
|
|
||||||
))),
|
|
||||||
to_tag,
|
|
||||||
)(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Recognizes a tag prefixed with `-` and returns the tag value
|
|
||||||
pub(super) fn minus_tag(input: &str) -> IResult<&str, &str> {
|
|
||||||
fn to_tag(input: (char, &str)) -> Result<&str, ()> {
|
|
||||||
Ok(input.1)
|
|
||||||
}
|
|
||||||
map_res(
|
|
||||||
all_consuming(tuple((
|
|
||||||
char('-'),
|
|
||||||
recognize(verify(rest, |s: &str| Tag::try_from(s).is_ok())),
|
|
||||||
))),
|
|
||||||
to_tag,
|
|
||||||
)(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Consume a single argument from an argument list that matches the given string parser (one
|
|
||||||
/// of the other functions in this module). The given parser must consume the entire input.
|
|
||||||
pub(super) fn arg_matching<'a, O, F>(f: F) -> impl Fn(ArgList<'a>) -> IResult<ArgList, O>
|
|
||||||
where
|
|
||||||
F: Fn(&'a str) -> IResult<&'a str, O>,
|
|
||||||
{
|
|
||||||
move |input: ArgList<'a>| {
|
|
||||||
if let Some(arg) = input.get(0) {
|
|
||||||
return match f(arg) {
|
|
||||||
Ok(("", rv)) => Ok((&input[1..], rv)),
|
|
||||||
// single-arg parsers must consume the entire arg
|
|
||||||
Ok((unconsumed, _)) => panic!("unconsumed argument input {}", unconsumed),
|
|
||||||
// single-arg parsers are all complete parsers
|
|
||||||
Err(Err::Incomplete(_)) => unreachable!(),
|
|
||||||
// for error and failure, rewrite to an error at this position in the arugment list
|
|
||||||
Err(Err::Error(Error { input: _, code })) => Err(Err::Error(Error { input, code })),
|
|
||||||
Err(Err::Failure(Error { input: _, code })) => {
|
|
||||||
Err(Err::Failure(Error { input, code }))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(Err::Error(Error {
|
|
||||||
input,
|
|
||||||
// since we're using nom's built-in Error, our choices here are limited, but tihs
|
|
||||||
// occurs when there's no argument where one is expected, so Eof seems appropriate
|
|
||||||
code: ErrorKind::Eof,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_arg_matching() {
|
|
||||||
assert_eq!(
|
|
||||||
arg_matching(plus_tag)(argv!["+foo", "bar"]).unwrap(),
|
|
||||||
(argv!["bar"], "foo")
|
|
||||||
);
|
|
||||||
assert!(arg_matching(plus_tag)(argv!["foo", "bar"]).is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_colon_prefixed() {
|
|
||||||
assert_eq!(colon_prefixed("foo")("foo:abc").unwrap().1, "abc");
|
|
||||||
assert_eq!(colon_prefixed("foo")("foo:").unwrap().1, "");
|
|
||||||
assert!(colon_prefixed("foo")("foo").is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_status_colon() {
|
|
||||||
assert_eq!(status_colon("status:pending").unwrap().1, Status::Pending);
|
|
||||||
assert_eq!(
|
|
||||||
status_colon("status:completed").unwrap().1,
|
|
||||||
Status::Completed
|
|
||||||
);
|
|
||||||
assert_eq!(status_colon("status:deleted").unwrap().1, Status::Deleted);
|
|
||||||
assert!(status_colon("status:foo").is_err());
|
|
||||||
assert!(status_colon("status:complete").is_err());
|
|
||||||
assert!(status_colon("status").is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_plus_tag() {
|
|
||||||
assert_eq!(plus_tag("+abc").unwrap().1, "abc");
|
|
||||||
assert_eq!(plus_tag("+abc123").unwrap().1, "abc123");
|
|
||||||
assert!(plus_tag("-abc123").is_err());
|
|
||||||
assert!(plus_tag("+abc123 ").is_err());
|
|
||||||
assert!(plus_tag(" +abc123").is_err());
|
|
||||||
assert!(plus_tag("+1abc").is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_minus_tag() {
|
|
||||||
assert_eq!(minus_tag("-abc").unwrap().1, "abc");
|
|
||||||
assert_eq!(minus_tag("-abc123").unwrap().1, "abc123");
|
|
||||||
assert!(minus_tag("+abc123").is_err());
|
|
||||||
assert!(minus_tag("-abc123 ").is_err());
|
|
||||||
assert!(minus_tag(" -abc123").is_err());
|
|
||||||
assert!(minus_tag("-1abc").is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_literal() {
|
|
||||||
assert_eq!(literal("list")("list").unwrap().1, "list");
|
|
||||||
assert!(literal("list")("listicle").is_err());
|
|
||||||
assert!(literal("list")(" list ").is_err());
|
|
||||||
assert!(literal("list")("LiSt").is_err());
|
|
||||||
assert!(literal("list")("denylist").is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_id_list_single() {
|
|
||||||
assert_eq!(id_list("123").unwrap().1, vec![TaskId::WorkingSetId(123)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_id_list_uuids() {
|
|
||||||
assert_eq!(
|
|
||||||
id_list("12341234").unwrap().1,
|
|
||||||
vec![TaskId::PartialUuid(s!("12341234"))]
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
id_list("1234abcd").unwrap().1,
|
|
||||||
vec![TaskId::PartialUuid(s!("1234abcd"))]
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
id_list("abcd1234").unwrap().1,
|
|
||||||
vec![TaskId::PartialUuid(s!("abcd1234"))]
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
id_list("abcd1234-1234").unwrap().1,
|
|
||||||
vec![TaskId::PartialUuid(s!("abcd1234-1234"))]
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
id_list("abcd1234-1234-2345").unwrap().1,
|
|
||||||
vec![TaskId::PartialUuid(s!("abcd1234-1234-2345"))]
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
id_list("abcd1234-1234-2345-3456").unwrap().1,
|
|
||||||
vec![TaskId::PartialUuid(s!("abcd1234-1234-2345-3456"))]
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
id_list("abcd1234-1234-2345-3456-0123456789ab").unwrap().1,
|
|
||||||
vec![TaskId::Uuid(
|
|
||||||
Uuid::parse_str("abcd1234-1234-2345-3456-0123456789ab").unwrap()
|
|
||||||
)]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_id_list_invalid_partial_uuids() {
|
|
||||||
assert!(id_list("abcd123").is_err());
|
|
||||||
assert!(id_list("abcd12345").is_err());
|
|
||||||
assert!(id_list("abcd1234-").is_err());
|
|
||||||
assert!(id_list("abcd1234-123").is_err());
|
|
||||||
assert!(id_list("abcd1234-1234-").is_err());
|
|
||||||
assert!(id_list("abcd1234-12345-").is_err());
|
|
||||||
assert!(id_list("abcd1234-1234-2345-3456-0123456789ab-").is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_id_list_uuids_mixed() {
|
|
||||||
assert_eq!(id_list("abcd1234,abcd1234-1234,abcd1234-1234-2345,abcd1234-1234-2345-3456,abcd1234-1234-2345-3456-0123456789ab").unwrap().1,
|
|
||||||
vec![TaskId::PartialUuid(s!("abcd1234")),
|
|
||||||
TaskId::PartialUuid(s!("abcd1234-1234")),
|
|
||||||
TaskId::PartialUuid(s!("abcd1234-1234-2345")),
|
|
||||||
TaskId::PartialUuid(s!("abcd1234-1234-2345-3456")),
|
|
||||||
TaskId::Uuid(Uuid::parse_str("abcd1234-1234-2345-3456-0123456789ab").unwrap()),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
51
cli/src/argparse/args/arg_matching.rs
Normal file
51
cli/src/argparse/args/arg_matching.rs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
use crate::argparse::ArgList;
|
||||||
|
use nom::{
|
||||||
|
error::{Error, ErrorKind},
|
||||||
|
Err, IResult,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Consume a single argument from an argument list that matches the given string parser (one
|
||||||
|
/// of the other functions in this module). The given parser must consume the entire input.
|
||||||
|
pub(crate) fn arg_matching<'a, O, F>(f: F) -> impl Fn(ArgList<'a>) -> IResult<ArgList, O>
|
||||||
|
where
|
||||||
|
F: Fn(&'a str) -> IResult<&'a str, O>,
|
||||||
|
{
|
||||||
|
move |input: ArgList<'a>| {
|
||||||
|
if let Some(arg) = input.get(0) {
|
||||||
|
return match f(arg) {
|
||||||
|
Ok(("", rv)) => Ok((&input[1..], rv)),
|
||||||
|
// single-arg parsers must consume the entire arg
|
||||||
|
Ok((unconsumed, _)) => panic!("unconsumed argument input {}", unconsumed),
|
||||||
|
// single-arg parsers are all complete parsers
|
||||||
|
Err(Err::Incomplete(_)) => unreachable!(),
|
||||||
|
// for error and failure, rewrite to an error at this position in the arugment list
|
||||||
|
Err(Err::Error(Error { input: _, code })) => Err(Err::Error(Error { input, code })),
|
||||||
|
Err(Err::Failure(Error { input: _, code })) => {
|
||||||
|
Err(Err::Failure(Error { input, code }))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(Err::Error(Error {
|
||||||
|
input,
|
||||||
|
// since we're using nom's built-in Error, our choices here are limited, but tihs
|
||||||
|
// occurs when there's no argument where one is expected, so Eof seems appropriate
|
||||||
|
code: ErrorKind::Eof,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::super::*;
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_arg_matching() {
|
||||||
|
assert_eq!(
|
||||||
|
arg_matching(plus_tag)(argv!["+foo", "bar"]).unwrap(),
|
||||||
|
(argv!["bar"], "foo")
|
||||||
|
);
|
||||||
|
assert!(arg_matching(plus_tag)(argv!["foo", "bar"]).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
85
cli/src/argparse/args/colon.rs
Normal file
85
cli/src/argparse/args/colon.rs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
use super::{any, timestamp};
|
||||||
|
use crate::argparse::NOW;
|
||||||
|
use chrono::prelude::*;
|
||||||
|
use nom::bytes::complete::tag as nomtag;
|
||||||
|
use nom::{branch::*, character::complete::*, combinator::*, sequence::*, IResult};
|
||||||
|
use taskchampion::Status;
|
||||||
|
|
||||||
|
/// Recognizes up to the colon of the common `<prefix>:...` syntax
|
||||||
|
fn colon_prefix(prefix: &'static str) -> impl Fn(&str) -> IResult<&str, &str> {
|
||||||
|
fn to_suffix<'a>(input: (&'a str, char, &'a str)) -> Result<&'a str, ()> {
|
||||||
|
Ok(input.2)
|
||||||
|
}
|
||||||
|
move |input: &str| {
|
||||||
|
map_res(
|
||||||
|
all_consuming(tuple((nomtag(prefix), char(':'), any))),
|
||||||
|
to_suffix,
|
||||||
|
)(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recognizes `status:{pending,completed,deleted}`
|
||||||
|
pub(crate) fn status_colon(input: &str) -> IResult<&str, Status> {
|
||||||
|
fn to_status(input: &str) -> Result<Status, ()> {
|
||||||
|
match input {
|
||||||
|
"pending" => Ok(Status::Pending),
|
||||||
|
"completed" => Ok(Status::Completed),
|
||||||
|
"deleted" => Ok(Status::Deleted),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
map_res(colon_prefix("status"), to_status)(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recognizes `wait:` to None and `wait:<ts>` to `Some(ts)`
|
||||||
|
pub(crate) fn wait_colon(input: &str) -> IResult<&str, Option<DateTime<Utc>>> {
|
||||||
|
fn to_wait(input: DateTime<Utc>) -> Result<Option<DateTime<Utc>>, ()> {
|
||||||
|
Ok(Some(input))
|
||||||
|
}
|
||||||
|
fn to_none(_: &str) -> Result<Option<DateTime<Utc>>, ()> {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
preceded(
|
||||||
|
nomtag("wait:"),
|
||||||
|
alt((
|
||||||
|
map_res(timestamp(*NOW, Local), to_wait),
|
||||||
|
map_res(nomtag(""), to_none),
|
||||||
|
)),
|
||||||
|
)(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_colon_prefix() {
|
||||||
|
assert_eq!(colon_prefix("foo")("foo:abc").unwrap().1, "abc");
|
||||||
|
assert_eq!(colon_prefix("foo")("foo:").unwrap().1, "");
|
||||||
|
assert!(colon_prefix("foo")("foo").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_status_colon() {
|
||||||
|
assert_eq!(status_colon("status:pending").unwrap().1, Status::Pending);
|
||||||
|
assert_eq!(
|
||||||
|
status_colon("status:completed").unwrap().1,
|
||||||
|
Status::Completed
|
||||||
|
);
|
||||||
|
assert_eq!(status_colon("status:deleted").unwrap().1, Status::Deleted);
|
||||||
|
assert!(status_colon("status:foo").is_err());
|
||||||
|
assert!(status_colon("status:complete").is_err());
|
||||||
|
assert!(status_colon("status").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_wait() {
|
||||||
|
assert_eq!(wait_colon("wait:").unwrap(), ("", None));
|
||||||
|
|
||||||
|
let one_day = *NOW + chrono::Duration::days(1);
|
||||||
|
assert_eq!(wait_colon("wait:1d").unwrap(), ("", Some(one_day)));
|
||||||
|
|
||||||
|
let one_day = *NOW + chrono::Duration::days(1);
|
||||||
|
assert_eq!(wait_colon("wait:1d2").unwrap(), ("2", Some(one_day)));
|
||||||
|
}
|
||||||
|
}
|
||||||
139
cli/src/argparse/args/idlist.rs
Normal file
139
cli/src/argparse/args/idlist.rs
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
use nom::{branch::*, character::complete::*, combinator::*, multi::*, sequence::*, IResult};
|
||||||
|
use taskchampion::Uuid;
|
||||||
|
|
||||||
|
/// A task identifier, as given in a filter command-line expression
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
pub(crate) enum TaskId {
|
||||||
|
/// A small integer identifying a working-set task
|
||||||
|
WorkingSetId(usize),
|
||||||
|
|
||||||
|
/// A full Uuid specifically identifying a task
|
||||||
|
Uuid(Uuid),
|
||||||
|
|
||||||
|
/// A prefix of a Uuid
|
||||||
|
PartialUuid(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recognizes a comma-separated list of TaskIds
|
||||||
|
pub(crate) fn id_list(input: &str) -> IResult<&str, Vec<TaskId>> {
|
||||||
|
fn hex_n(n: usize) -> impl Fn(&str) -> IResult<&str, &str> {
|
||||||
|
move |input: &str| recognize(many_m_n(n, n, one_of(&b"0123456789abcdefABCDEF"[..])))(input)
|
||||||
|
}
|
||||||
|
fn uuid(input: &str) -> Result<TaskId, ()> {
|
||||||
|
Ok(TaskId::Uuid(Uuid::parse_str(input).map_err(|_| ())?))
|
||||||
|
}
|
||||||
|
fn partial_uuid(input: &str) -> Result<TaskId, ()> {
|
||||||
|
Ok(TaskId::PartialUuid(input.to_owned()))
|
||||||
|
}
|
||||||
|
fn working_set_id(input: &str) -> Result<TaskId, ()> {
|
||||||
|
Ok(TaskId::WorkingSetId(input.parse().map_err(|_| ())?))
|
||||||
|
}
|
||||||
|
all_consuming(separated_list1(
|
||||||
|
char(','),
|
||||||
|
alt((
|
||||||
|
map_res(
|
||||||
|
recognize(tuple((
|
||||||
|
hex_n(8),
|
||||||
|
char('-'),
|
||||||
|
hex_n(4),
|
||||||
|
char('-'),
|
||||||
|
hex_n(4),
|
||||||
|
char('-'),
|
||||||
|
hex_n(4),
|
||||||
|
char('-'),
|
||||||
|
hex_n(12),
|
||||||
|
))),
|
||||||
|
uuid,
|
||||||
|
),
|
||||||
|
map_res(
|
||||||
|
recognize(tuple((
|
||||||
|
hex_n(8),
|
||||||
|
char('-'),
|
||||||
|
hex_n(4),
|
||||||
|
char('-'),
|
||||||
|
hex_n(4),
|
||||||
|
char('-'),
|
||||||
|
hex_n(4),
|
||||||
|
))),
|
||||||
|
partial_uuid,
|
||||||
|
),
|
||||||
|
map_res(
|
||||||
|
recognize(tuple((hex_n(8), char('-'), hex_n(4), char('-'), hex_n(4)))),
|
||||||
|
partial_uuid,
|
||||||
|
),
|
||||||
|
map_res(
|
||||||
|
recognize(tuple((hex_n(8), char('-'), hex_n(4)))),
|
||||||
|
partial_uuid,
|
||||||
|
),
|
||||||
|
map_res(hex_n(8), partial_uuid),
|
||||||
|
// note that an 8-decimal-digit value will be treated as a UUID
|
||||||
|
map_res(digit1, working_set_id),
|
||||||
|
)),
|
||||||
|
))(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_id_list_single() {
|
||||||
|
assert_eq!(id_list("123").unwrap().1, vec![TaskId::WorkingSetId(123)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_id_list_uuids() {
|
||||||
|
assert_eq!(
|
||||||
|
id_list("12341234").unwrap().1,
|
||||||
|
vec![TaskId::PartialUuid(s!("12341234"))]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
id_list("1234abcd").unwrap().1,
|
||||||
|
vec![TaskId::PartialUuid(s!("1234abcd"))]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
id_list("abcd1234").unwrap().1,
|
||||||
|
vec![TaskId::PartialUuid(s!("abcd1234"))]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
id_list("abcd1234-1234").unwrap().1,
|
||||||
|
vec![TaskId::PartialUuid(s!("abcd1234-1234"))]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
id_list("abcd1234-1234-2345").unwrap().1,
|
||||||
|
vec![TaskId::PartialUuid(s!("abcd1234-1234-2345"))]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
id_list("abcd1234-1234-2345-3456").unwrap().1,
|
||||||
|
vec![TaskId::PartialUuid(s!("abcd1234-1234-2345-3456"))]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
id_list("abcd1234-1234-2345-3456-0123456789ab").unwrap().1,
|
||||||
|
vec![TaskId::Uuid(
|
||||||
|
Uuid::parse_str("abcd1234-1234-2345-3456-0123456789ab").unwrap()
|
||||||
|
)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_id_list_invalid_partial_uuids() {
|
||||||
|
assert!(id_list("abcd123").is_err());
|
||||||
|
assert!(id_list("abcd12345").is_err());
|
||||||
|
assert!(id_list("abcd1234-").is_err());
|
||||||
|
assert!(id_list("abcd1234-123").is_err());
|
||||||
|
assert!(id_list("abcd1234-1234-").is_err());
|
||||||
|
assert!(id_list("abcd1234-12345-").is_err());
|
||||||
|
assert!(id_list("abcd1234-1234-2345-3456-0123456789ab-").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_id_list_uuids_mixed() {
|
||||||
|
assert_eq!(id_list("abcd1234,abcd1234-1234,abcd1234-1234-2345,abcd1234-1234-2345-3456,abcd1234-1234-2345-3456-0123456789ab").unwrap().1,
|
||||||
|
vec![TaskId::PartialUuid(s!("abcd1234")),
|
||||||
|
TaskId::PartialUuid(s!("abcd1234-1234")),
|
||||||
|
TaskId::PartialUuid(s!("abcd1234-1234-2345")),
|
||||||
|
TaskId::PartialUuid(s!("abcd1234-1234-2345-3456")),
|
||||||
|
TaskId::Uuid(Uuid::parse_str("abcd1234-1234-2345-3456-0123456789ab").unwrap()),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
cli/src/argparse/args/misc.rs
Normal file
41
cli/src/argparse/args/misc.rs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
use nom::bytes::complete::tag as nomtag;
|
||||||
|
use nom::{character::complete::*, combinator::*, sequence::*, IResult};
|
||||||
|
|
||||||
|
/// Recognizes any argument
|
||||||
|
pub(crate) fn any(input: &str) -> IResult<&str, &str> {
|
||||||
|
rest(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recognizes a report name
|
||||||
|
pub(crate) fn report_name(input: &str) -> IResult<&str, &str> {
|
||||||
|
all_consuming(recognize(pair(alpha1, alphanumeric0)))(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recognizes a literal string
|
||||||
|
pub(crate) fn literal(literal: &'static str) -> impl Fn(&str) -> IResult<&str, &str> {
|
||||||
|
move |input: &str| all_consuming(nomtag(literal))(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::super::*;
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_arg_matching() {
|
||||||
|
assert_eq!(
|
||||||
|
arg_matching(plus_tag)(argv!["+foo", "bar"]).unwrap(),
|
||||||
|
(argv!["bar"], "foo")
|
||||||
|
);
|
||||||
|
assert!(arg_matching(plus_tag)(argv!["foo", "bar"]).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_literal() {
|
||||||
|
assert_eq!(literal("list")("list").unwrap().1, "list");
|
||||||
|
assert!(literal("list")("listicle").is_err());
|
||||||
|
assert!(literal("list")(" list ").is_err());
|
||||||
|
assert!(literal("list")("LiSt").is_err());
|
||||||
|
assert!(literal("list")("denylist").is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
16
cli/src/argparse/args/mod.rs
Normal file
16
cli/src/argparse/args/mod.rs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
//! Parsers for single arguments (strings)
|
||||||
|
|
||||||
|
mod arg_matching;
|
||||||
|
mod colon;
|
||||||
|
mod idlist;
|
||||||
|
mod misc;
|
||||||
|
mod tags;
|
||||||
|
mod time;
|
||||||
|
|
||||||
|
pub(crate) use arg_matching::arg_matching;
|
||||||
|
pub(crate) use colon::{status_colon, wait_colon};
|
||||||
|
pub(crate) use idlist::{id_list, TaskId};
|
||||||
|
pub(crate) use misc::{any, literal, report_name};
|
||||||
|
pub(crate) use tags::{minus_tag, plus_tag};
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
pub(crate) use time::{duration, timestamp};
|
||||||
56
cli/src/argparse/args/tags.rs
Normal file
56
cli/src/argparse/args/tags.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
use nom::{character::complete::*, combinator::*, sequence::*, IResult};
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use taskchampion::Tag;
|
||||||
|
|
||||||
|
/// Recognizes a tag prefixed with `+` and returns the tag value
|
||||||
|
pub(crate) fn plus_tag(input: &str) -> IResult<&str, &str> {
|
||||||
|
fn to_tag(input: (char, &str)) -> Result<&str, ()> {
|
||||||
|
Ok(input.1)
|
||||||
|
}
|
||||||
|
map_res(
|
||||||
|
all_consuming(tuple((
|
||||||
|
char('+'),
|
||||||
|
recognize(verify(rest, |s: &str| Tag::try_from(s).is_ok())),
|
||||||
|
))),
|
||||||
|
to_tag,
|
||||||
|
)(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recognizes a tag prefixed with `-` and returns the tag value
|
||||||
|
pub(crate) fn minus_tag(input: &str) -> IResult<&str, &str> {
|
||||||
|
fn to_tag(input: (char, &str)) -> Result<&str, ()> {
|
||||||
|
Ok(input.1)
|
||||||
|
}
|
||||||
|
map_res(
|
||||||
|
all_consuming(tuple((
|
||||||
|
char('-'),
|
||||||
|
recognize(verify(rest, |s: &str| Tag::try_from(s).is_ok())),
|
||||||
|
))),
|
||||||
|
to_tag,
|
||||||
|
)(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_plus_tag() {
|
||||||
|
assert_eq!(plus_tag("+abc").unwrap().1, "abc");
|
||||||
|
assert_eq!(plus_tag("+abc123").unwrap().1, "abc123");
|
||||||
|
assert!(plus_tag("-abc123").is_err());
|
||||||
|
assert!(plus_tag("+abc123 ").is_err());
|
||||||
|
assert!(plus_tag(" +abc123").is_err());
|
||||||
|
assert!(plus_tag("+1abc").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_minus_tag() {
|
||||||
|
assert_eq!(minus_tag("-abc").unwrap().1, "abc");
|
||||||
|
assert_eq!(minus_tag("-abc123").unwrap().1, "abc123");
|
||||||
|
assert!(minus_tag("+abc123").is_err());
|
||||||
|
assert!(minus_tag("-abc123 ").is_err());
|
||||||
|
assert!(minus_tag(" -abc123").is_err());
|
||||||
|
assert!(minus_tag("-1abc").is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
466
cli/src/argparse/args/time.rs
Normal file
466
cli/src/argparse/args/time.rs
Normal file
@@ -0,0 +1,466 @@
|
|||||||
|
use chrono::{prelude::*, Duration};
|
||||||
|
use iso8601_duration::Duration as IsoDuration;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use nom::{
|
||||||
|
branch::*,
|
||||||
|
bytes::complete::*,
|
||||||
|
character::complete::*,
|
||||||
|
character::*,
|
||||||
|
combinator::*,
|
||||||
|
error::{Error, ErrorKind},
|
||||||
|
multi::*,
|
||||||
|
sequence::*,
|
||||||
|
Err, IResult,
|
||||||
|
};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
// https://taskwarrior.org/docs/dates.html
|
||||||
|
// https://taskwarrior.org/docs/named_dates.html
|
||||||
|
// https://taskwarrior.org/docs/durations.html
|
||||||
|
|
||||||
|
/// A case for matching durations. If `.3` is true, then the value can be used
|
||||||
|
/// without a prefix, e.g., `minute`. If false, it cannot, e.g., `minutes`
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct DurationCase(&'static str, Duration, bool);
|
||||||
|
|
||||||
|
// https://github.com/GothenburgBitFactory/libshared/blob/9a5f24e2acb38d05afb8f8e316a966dee196a42a/src/Duration.cpp#L50
|
||||||
|
// TODO: use const when chrono supports it
|
||||||
|
lazy_static! {
|
||||||
|
static ref DURATION_CASES: Vec<DurationCase> = vec![
|
||||||
|
DurationCase("days", Duration::days(1), false),
|
||||||
|
DurationCase("day", Duration::days(1), true),
|
||||||
|
DurationCase("d", Duration::days(1), false),
|
||||||
|
DurationCase("hours", Duration::hours(1), false),
|
||||||
|
DurationCase("hour", Duration::hours(1), true),
|
||||||
|
DurationCase("h", Duration::hours(1), false),
|
||||||
|
DurationCase("minutes", Duration::minutes(1), false),
|
||||||
|
DurationCase("minute", Duration::minutes(1), true),
|
||||||
|
DurationCase("mins", Duration::minutes(1), false),
|
||||||
|
DurationCase("min", Duration::minutes(1), true),
|
||||||
|
DurationCase("months", Duration::days(30), false),
|
||||||
|
DurationCase("month", Duration::days(30), true),
|
||||||
|
DurationCase("mo", Duration::days(30), true),
|
||||||
|
DurationCase("seconds", Duration::seconds(1), false),
|
||||||
|
DurationCase("second", Duration::seconds(1), true),
|
||||||
|
DurationCase("s", Duration::seconds(1), false),
|
||||||
|
DurationCase("weeks", Duration::days(7), false),
|
||||||
|
DurationCase("week", Duration::days(7), true),
|
||||||
|
DurationCase("w", Duration::days(7), false),
|
||||||
|
DurationCase("years", Duration::days(365), false),
|
||||||
|
DurationCase("year", Duration::days(365), true),
|
||||||
|
DurationCase("y", Duration::days(365), false),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses suffixes like 'min', and 'd'; standalone is true if there is no numeric prefix, in which
|
||||||
|
/// case plurals (like `days`) are not matched.
|
||||||
|
fn duration_suffix(has_prefix: bool) -> impl Fn(&str) -> IResult<&str, Duration> {
|
||||||
|
move |input: &str| {
|
||||||
|
// Rust wants this to have a default value, but it is not actually used
|
||||||
|
// because DURATION_CASES has at least one case with case.2 == `true`
|
||||||
|
let mut res = Err(Err::Failure(Error::new(input, ErrorKind::Tag)));
|
||||||
|
for case in DURATION_CASES.iter() {
|
||||||
|
if !case.2 && !has_prefix {
|
||||||
|
// this case requires a prefix, and input does not have one
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
res = tag(case.0)(input);
|
||||||
|
match res {
|
||||||
|
Ok((i, _)) => {
|
||||||
|
return Ok((i, case.1));
|
||||||
|
}
|
||||||
|
Err(Err::Error(_)) => {
|
||||||
|
// recoverable error
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// irrecoverable error
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the last error
|
||||||
|
Err(res.unwrap_err())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Calculate the multiplier for a decimal prefix; this uses integer math
|
||||||
|
/// where possible, falling back to floating-point math on seconds
|
||||||
|
fn decimal_prefix_multiplier(input: &str) -> IResult<&str, f64> {
|
||||||
|
map_res(
|
||||||
|
// recognize NN or NN.NN
|
||||||
|
alt((recognize(tuple((digit1, char('.'), digit1))), digit1)),
|
||||||
|
|input: &str| -> Result<f64, <f64 as FromStr>::Err> {
|
||||||
|
let mul = input.parse::<f64>()?;
|
||||||
|
Ok(mul)
|
||||||
|
},
|
||||||
|
)(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse an iso8601 duration, converting it to a [`chrono::Duration`] on the assumption
|
||||||
|
/// that a year is 365 days and a month is 30 days.
|
||||||
|
fn iso8601_dur(input: &str) -> IResult<&str, Duration> {
|
||||||
|
if let Ok(iso_dur) = IsoDuration::parse(input) {
|
||||||
|
// iso8601_duration uses f32, but f32 underflows seconds for values as small as
|
||||||
|
// a year. So we upgrade to f64 immediately. f64 has a 53-bit mantissa which can
|
||||||
|
// represent almost 300 million years without underflow, so it should be adequate.
|
||||||
|
let days = iso_dur.year as f64 * 365.0 + iso_dur.month as f64 * 30.0 + iso_dur.day as f64;
|
||||||
|
let hours = days * 24.0 + iso_dur.hour as f64;
|
||||||
|
let mins = hours * 60.0 + iso_dur.minute as f64;
|
||||||
|
let secs = mins * 60.0 + iso_dur.second as f64;
|
||||||
|
let dur = Duration::seconds(secs as i64);
|
||||||
|
Ok((&input[input.len()..], dur))
|
||||||
|
} else {
|
||||||
|
Err(Err::Error(Error::new(input, ErrorKind::Tag)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recognizes durations
|
||||||
|
pub(crate) fn duration(input: &str) -> IResult<&str, Duration> {
|
||||||
|
alt((
|
||||||
|
map_res(
|
||||||
|
tuple((
|
||||||
|
decimal_prefix_multiplier,
|
||||||
|
multispace0,
|
||||||
|
duration_suffix(true),
|
||||||
|
)),
|
||||||
|
|input: (f64, &str, Duration)| -> Result<Duration, ()> {
|
||||||
|
// `as i64` is saturating, so for large offsets this will
|
||||||
|
// just pick an imprecise very-futuristic date
|
||||||
|
let secs = (input.0 * input.2.num_seconds() as f64) as i64;
|
||||||
|
Ok(Duration::seconds(secs))
|
||||||
|
},
|
||||||
|
),
|
||||||
|
duration_suffix(false),
|
||||||
|
iso8601_dur,
|
||||||
|
))(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a rfc3339 datestamp
|
||||||
|
fn rfc3339_timestamp(input: &str) -> IResult<&str, DateTime<Utc>> {
|
||||||
|
if let Ok(dt) = DateTime::parse_from_rfc3339(input) {
|
||||||
|
// convert to UTC and truncate seconds
|
||||||
|
let dt = dt.with_timezone(&Utc).trunc_subsecs(0);
|
||||||
|
Ok((&input[input.len()..], dt))
|
||||||
|
} else {
|
||||||
|
Err(Err::Error(Error::new(input, ErrorKind::Tag)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn named_date<Tz: TimeZone>(
|
||||||
|
now: DateTime<Utc>,
|
||||||
|
local: Tz,
|
||||||
|
) -> impl Fn(&str) -> IResult<&str, DateTime<Utc>> {
|
||||||
|
move |input: &str| {
|
||||||
|
let local_today = now.with_timezone(&local).date();
|
||||||
|
let remaining = &input[input.len()..];
|
||||||
|
match input {
|
||||||
|
"yesterday" => Ok((remaining, local_today - Duration::days(1))),
|
||||||
|
"today" => Ok((remaining, local_today)),
|
||||||
|
"tomorrow" => Ok((remaining, local_today + Duration::days(1))),
|
||||||
|
// TODO: lots more!
|
||||||
|
_ => Err(Err::Error(Error::new(input, ErrorKind::Tag))),
|
||||||
|
}
|
||||||
|
.map(|(rem, dt)| (rem, dt.and_hms(0, 0, 0).with_timezone(&Utc)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// recognize a digit
|
||||||
|
fn digit(input: &str) -> IResult<&str, char> {
|
||||||
|
satisfy(|c| is_digit(c as u8))(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse yyyy-mm-dd as the given date, at the local midnight
|
||||||
|
fn yyyy_mm_dd<Tz: TimeZone>(local: Tz) -> impl Fn(&str) -> IResult<&str, DateTime<Utc>> {
|
||||||
|
move |input: &str| {
|
||||||
|
fn parse_int<T: FromStr>(input: &str) -> Result<T, <T as FromStr>::Err> {
|
||||||
|
input.parse::<T>()
|
||||||
|
}
|
||||||
|
map_res(
|
||||||
|
tuple((
|
||||||
|
map_res(recognize(count(digit, 4)), parse_int::<i32>),
|
||||||
|
char('-'),
|
||||||
|
map_res(recognize(many_m_n(1, 2, digit)), parse_int::<u32>),
|
||||||
|
char('-'),
|
||||||
|
map_res(recognize(many_m_n(1, 2, digit)), parse_int::<u32>),
|
||||||
|
)),
|
||||||
|
|input: (i32, char, u32, char, u32)| -> Result<DateTime<Utc>, ()> {
|
||||||
|
// try to convert, handling out-of-bounds months or days as an error
|
||||||
|
let ymd = match local.ymd_opt(input.0, input.2, input.4) {
|
||||||
|
chrono::LocalResult::Single(ymd) => Ok(ymd),
|
||||||
|
_ => Err(()),
|
||||||
|
}?;
|
||||||
|
Ok(ymd.and_hms(0, 0, 0).with_timezone(&Utc))
|
||||||
|
},
|
||||||
|
)(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recognizes timestamps
|
||||||
|
pub(crate) fn timestamp<Tz: TimeZone + Copy>(
|
||||||
|
now: DateTime<Utc>,
|
||||||
|
local: Tz,
|
||||||
|
) -> impl Fn(&str) -> IResult<&str, DateTime<Utc>> {
|
||||||
|
move |input: &str| {
|
||||||
|
alt((
|
||||||
|
// relative time
|
||||||
|
map_res(
|
||||||
|
duration,
|
||||||
|
|duration: Duration| -> Result<DateTime<Utc>, ()> { Ok(now + duration) },
|
||||||
|
),
|
||||||
|
rfc3339_timestamp,
|
||||||
|
yyyy_mm_dd(local),
|
||||||
|
value(now, tag("now")),
|
||||||
|
named_date(now, local),
|
||||||
|
))(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use crate::argparse::NOW;
|
||||||
|
use rstest::rstest;
|
||||||
|
|
||||||
|
const M: i64 = 60;
|
||||||
|
const H: i64 = M * 60;
|
||||||
|
const DAY: i64 = H * 24;
|
||||||
|
const MONTH: i64 = DAY * 30;
|
||||||
|
const YEAR: i64 = DAY * 365;
|
||||||
|
|
||||||
|
// TODO: use const when chrono supports it
|
||||||
|
lazy_static! {
|
||||||
|
// India standard time (not an even multiple of hours)
|
||||||
|
static ref IST: FixedOffset = FixedOffset::east(5 * 3600 + 30 * 60);
|
||||||
|
// Utc, but as a FixedOffset TimeZone impl
|
||||||
|
static ref UTC_FO: FixedOffset = FixedOffset::east(0);
|
||||||
|
// Hawaii
|
||||||
|
static ref HST: FixedOffset = FixedOffset::west(10 * 3600);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// test helper to ensure that the entire input is consumed
|
||||||
|
fn complete_duration(input: &str) -> IResult<&str, Duration> {
|
||||||
|
all_consuming(duration)(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// test helper to ensure that the entire input is consumed
|
||||||
|
fn complete_timestamp<Tz: TimeZone + Copy>(
|
||||||
|
now: DateTime<Utc>,
|
||||||
|
local: Tz,
|
||||||
|
) -> impl Fn(&str) -> IResult<&str, DateTime<Utc>> {
|
||||||
|
move |input: &str| all_consuming(timestamp(now, local))(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shorthand day and time
|
||||||
|
fn dt(y: i32, m: u32, d: u32, hh: u32, mm: u32, ss: u32) -> DateTime<Utc> {
|
||||||
|
Utc.ymd(y, m, d).and_hms(hh, mm, ss)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Local day and time, parameterized on the timezone
|
||||||
|
fn ldt(
|
||||||
|
y: i32,
|
||||||
|
m: u32,
|
||||||
|
d: u32,
|
||||||
|
hh: u32,
|
||||||
|
mm: u32,
|
||||||
|
ss: u32,
|
||||||
|
) -> Box<dyn Fn(FixedOffset) -> DateTime<Utc>> {
|
||||||
|
Box::new(move |tz| tz.ymd(y, m, d).and_hms(hh, mm, ss).with_timezone(&Utc))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ld(y: i32, m: u32, d: u32) -> Box<dyn Fn(FixedOffset) -> DateTime<Utc>> {
|
||||||
|
ldt(y, m, d, 0, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case::rel_hours_0(dt(2021, 5, 29, 1, 30, 0), "0h", dt(2021, 5, 29, 1, 30, 0))]
|
||||||
|
#[case::rel_hours_05(dt(2021, 5, 29, 1, 30, 0), "0.5h", dt(2021, 5, 29, 2, 0, 0))]
|
||||||
|
#[case::rel_hours_no_prefix(dt(2021, 5, 29, 1, 30, 0), "hour", dt(2021, 5, 29, 2, 30, 0))]
|
||||||
|
#[case::rel_hours_5(dt(2021, 5, 29, 1, 30, 0), "5h", dt(2021, 5, 29, 6, 30, 0))]
|
||||||
|
#[case::rel_days_0(dt(2021, 5, 29, 1, 30, 0), "0d", dt(2021, 5, 29, 1, 30, 0))]
|
||||||
|
#[case::rel_days_10(dt(2021, 5, 29, 1, 30, 0), "10d", dt(2021, 6, 8, 1, 30, 0))]
|
||||||
|
#[case::rfc3339_datetime(*NOW, "2019-10-12T07:20:50.12Z", dt(2019, 10, 12, 7, 20, 50))]
|
||||||
|
#[case::now(*NOW, "now", *NOW)]
|
||||||
|
/// Cases where the `local` parameter is ignored
|
||||||
|
fn test_nonlocal_timestamp(
|
||||||
|
#[case] now: DateTime<Utc>,
|
||||||
|
#[case] input: &'static str,
|
||||||
|
#[case] output: DateTime<Utc>,
|
||||||
|
) {
|
||||||
|
let (_, res) = complete_timestamp(now, *IST)(input).unwrap();
|
||||||
|
assert_eq!(res, output, "parsing {:?}", input);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
/// Cases where the `local` parameter matters
|
||||||
|
#[case::yyyy_mm_dd(ld(2000, 1, 1), "2021-01-01", ld(2021, 1, 1))]
|
||||||
|
#[case::yyyy_m_d(ld(2000, 1, 1), "2021-1-1", ld(2021, 1, 1))]
|
||||||
|
#[case::yesterday(ld(2021, 3, 1), "yesterday", ld(2021, 2, 28))]
|
||||||
|
#[case::yesterday_from_evening(ldt(2021, 3, 1, 21, 30, 30), "yesterday", ld(2021, 2, 28))]
|
||||||
|
#[case::today(ld(2021, 3, 1), "today", ld(2021, 3, 1))]
|
||||||
|
#[case::today_from_evening(ldt(2021, 3, 1, 21, 30, 30), "today", ld(2021, 3, 1))]
|
||||||
|
#[case::tomorrow(ld(2021, 3, 1), "tomorrow", ld(2021, 3, 2))]
|
||||||
|
#[case::tomorow_from_evening(ldt(2021, 3, 1, 21, 30, 30), "tomorrow", ld(2021, 3, 2))]
|
||||||
|
fn test_local_timestamp(
|
||||||
|
#[case] now: Box<dyn Fn(FixedOffset) -> DateTime<Utc>>,
|
||||||
|
#[values(*IST, *UTC_FO, *HST)] tz: FixedOffset,
|
||||||
|
#[case] input: &str,
|
||||||
|
#[case] output: Box<dyn Fn(FixedOffset) -> DateTime<Utc>>,
|
||||||
|
) {
|
||||||
|
let now = now(tz);
|
||||||
|
let output = output(tz);
|
||||||
|
let (_, res) = complete_timestamp(now, tz)(input).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
res, output,
|
||||||
|
"parsing {:?} relative to {:?} in timezone {:?}",
|
||||||
|
input, now, tz
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case::rfc3339_datetime_bad_month(*NOW, "2019-10-99T07:20:50.12Z")]
|
||||||
|
#[case::yyyy_mm_dd_bad_month(*NOW, "2019-10-99")]
|
||||||
|
fn test_timestamp_err(#[case] now: DateTime<Utc>, #[case] input: &'static str) {
|
||||||
|
let res = complete_timestamp(now, Utc)(input);
|
||||||
|
assert!(
|
||||||
|
res.is_err(),
|
||||||
|
"expected error parsing {:?}, got {:?}",
|
||||||
|
input,
|
||||||
|
res.unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// All test cases from
|
||||||
|
// https://github.com/GothenburgBitFactory/libshared/blob/9a5f24e2acb38d05afb8f8e316a966dee196a42a/test/duration.t.cpp#L136
|
||||||
|
#[rstest]
|
||||||
|
#[case("0seconds", 0)]
|
||||||
|
#[case("2 seconds", 2)]
|
||||||
|
#[case("10seconds", 10)]
|
||||||
|
#[case("1.5seconds", 1)]
|
||||||
|
#[case("0second", 0)]
|
||||||
|
#[case("2 second", 2)]
|
||||||
|
#[case("10second", 10)]
|
||||||
|
#[case("1.5second", 1)]
|
||||||
|
#[case("0s", 0)]
|
||||||
|
#[case("2 s", 2)]
|
||||||
|
#[case("10s", 10)]
|
||||||
|
#[case("1.5s", 1)]
|
||||||
|
#[case("0minutes", 0)]
|
||||||
|
#[case("2 minutes", 2 * M)]
|
||||||
|
#[case("10minutes", 10 * M)]
|
||||||
|
#[case("1.5minutes", M + 30)]
|
||||||
|
#[case("0minute", 0)]
|
||||||
|
#[case("2 minute", 2 * M)]
|
||||||
|
#[case("10minute", 10 * M)]
|
||||||
|
#[case("1.5minute", M + 30)]
|
||||||
|
#[case("0min", 0)]
|
||||||
|
#[case("2 min", 2 * M)]
|
||||||
|
#[case("10min", 10 * M)]
|
||||||
|
#[case("1.5min", M + 30)]
|
||||||
|
#[case("0hours", 0)]
|
||||||
|
#[case("2 hours", 2 * H)]
|
||||||
|
#[case("10hours", 10 * H)]
|
||||||
|
#[case("1.5hours", H + 30 * M)]
|
||||||
|
#[case("0hour", 0)]
|
||||||
|
#[case("2 hour", 2 * H)]
|
||||||
|
#[case("10hour", 10 * H)]
|
||||||
|
#[case("1.5hour", H + 30 * M)]
|
||||||
|
#[case("0h", 0)]
|
||||||
|
#[case("2 h", 2 * H)]
|
||||||
|
#[case("10h", 10 * H)]
|
||||||
|
#[case("1.5h", H + 30 * M)]
|
||||||
|
#[case("0days", 0)]
|
||||||
|
#[case("2 days", 2 * DAY)]
|
||||||
|
#[case("10days", 10 * DAY)]
|
||||||
|
#[case("1.5days", DAY + 12 * H)]
|
||||||
|
#[case("0day", 0)]
|
||||||
|
#[case("2 day", 2 * DAY)]
|
||||||
|
#[case("10day", 10 * DAY)]
|
||||||
|
#[case("1.5day", DAY + 12 * H)]
|
||||||
|
#[case("0d", 0)]
|
||||||
|
#[case("2 d", 2 * DAY)]
|
||||||
|
#[case("10d", 10 * DAY)]
|
||||||
|
#[case("1.5d", DAY + 12 * H)]
|
||||||
|
#[case("0weeks", 0)]
|
||||||
|
#[case("2 weeks", 14 * DAY)]
|
||||||
|
#[case("10weeks", 70 * DAY)]
|
||||||
|
#[case("1.5weeks", 10 * DAY + 12 * H)]
|
||||||
|
#[case("0week", 0)]
|
||||||
|
#[case("2 week", 14 * DAY)]
|
||||||
|
#[case("10week", 70 * DAY)]
|
||||||
|
#[case("1.5week", 10 * DAY + 12 * H)]
|
||||||
|
#[case("0w", 0)]
|
||||||
|
#[case("2 w", 14 * DAY)]
|
||||||
|
#[case("10w", 70 * DAY)]
|
||||||
|
#[case("1.5w", 10 * DAY + 12 * H)]
|
||||||
|
#[case("0months", 0)]
|
||||||
|
#[case("2 months", 60 * DAY)]
|
||||||
|
#[case("10months", 300 * DAY)]
|
||||||
|
#[case("1.5months", 45 * DAY)]
|
||||||
|
#[case("0month", 0)]
|
||||||
|
#[case("2 month", 60 * DAY)]
|
||||||
|
#[case("10month", 300 * DAY)]
|
||||||
|
#[case("1.5month", 45 * DAY)]
|
||||||
|
#[case("0mo", 0)]
|
||||||
|
#[case("2 mo", 60 * DAY)]
|
||||||
|
#[case("10mo", 300 * DAY)]
|
||||||
|
#[case("1.5mo", 45 * DAY)]
|
||||||
|
#[case("0years", 0)]
|
||||||
|
#[case("2 years", 2 * YEAR)]
|
||||||
|
#[case("10years", 10 * YEAR)]
|
||||||
|
#[case("1.5years", 547 * DAY + 12 * H)]
|
||||||
|
#[case("0year", 0)]
|
||||||
|
#[case("2 year", 2 * YEAR)]
|
||||||
|
#[case("10year", 10 * YEAR)]
|
||||||
|
#[case("1.5year", 547 * DAY + 12 * H)]
|
||||||
|
#[case("0y", 0)]
|
||||||
|
#[case("2 y", 2 * YEAR)]
|
||||||
|
#[case("10y", 10 * YEAR)]
|
||||||
|
#[case("1.5y", 547 * DAY + 12 * H)]
|
||||||
|
fn test_duration_units(#[case] input: &'static str, #[case] seconds: i64) {
|
||||||
|
let (_, res) = complete_duration(input).expect(input);
|
||||||
|
assert_eq!(res.num_seconds(), seconds, "parsing {}", input);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case("years")]
|
||||||
|
#[case("minutes")]
|
||||||
|
#[case("eons")]
|
||||||
|
#[case("P1S")] // missing T
|
||||||
|
#[case("p1y")] // lower-case
|
||||||
|
fn test_duration_errors(#[case] input: &'static str) {
|
||||||
|
let res = complete_duration(input);
|
||||||
|
assert!(
|
||||||
|
res.is_err(),
|
||||||
|
"did not get expected error parsing duration {:?}; got {:?}",
|
||||||
|
input,
|
||||||
|
res.unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/GothenburgBitFactory/libshared/blob/9a5f24e2acb38d05afb8f8e316a966dee196a42a/test/duration.t.cpp#L115
|
||||||
|
#[rstest]
|
||||||
|
#[case("P1Y", YEAR)]
|
||||||
|
#[case("P1M", MONTH)]
|
||||||
|
#[case("P1D", DAY)]
|
||||||
|
#[case("P1Y1M", YEAR + MONTH)]
|
||||||
|
#[case("P1Y1D", YEAR + DAY)]
|
||||||
|
#[case("P1M1D", MONTH + DAY)]
|
||||||
|
#[case("P1Y1M1D", YEAR + MONTH + DAY)]
|
||||||
|
#[case("PT1H", H)]
|
||||||
|
#[case("PT1M", M)]
|
||||||
|
#[case("PT1S", 1)]
|
||||||
|
#[case("PT1H1M", H + M)]
|
||||||
|
#[case("PT1H1S", H + 1)]
|
||||||
|
#[case("PT1M1S", M + 1)]
|
||||||
|
#[case("PT1H1M1S", H + M + 1)]
|
||||||
|
#[case("P1Y1M1DT1H1M1S", YEAR + MONTH + DAY + H + M + 1)]
|
||||||
|
#[case("PT24H", DAY)]
|
||||||
|
#[case("PT40000000S", 40000000)]
|
||||||
|
#[case("PT3600S", H)]
|
||||||
|
#[case("PT60M", H)]
|
||||||
|
fn test_duration_8601(#[case] input: &'static str, #[case] seconds: i64) {
|
||||||
|
let (_, res) = complete_duration(input).expect(input);
|
||||||
|
assert_eq!(res.num_seconds(), seconds, "parsing {}", input);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,6 +31,13 @@ pub(crate) use modification::{DescriptionMod, Modification};
|
|||||||
pub(crate) use subcommand::Subcommand;
|
pub(crate) use subcommand::Subcommand;
|
||||||
|
|
||||||
use crate::usage::Usage;
|
use crate::usage::Usage;
|
||||||
|
use chrono::prelude::*;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
// A static value of NOW to make tests easier
|
||||||
|
pub(crate) static ref NOW: DateTime<Utc> = Utc::now();
|
||||||
|
}
|
||||||
|
|
||||||
type ArgList<'a> = &'a [&'a str];
|
type ArgList<'a> = &'a [&'a str];
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use super::args::{any, arg_matching, minus_tag, plus_tag};
|
use super::args::{any, arg_matching, minus_tag, plus_tag, wait_colon};
|
||||||
use super::ArgList;
|
use super::ArgList;
|
||||||
use crate::usage;
|
use crate::usage;
|
||||||
|
use chrono::prelude::*;
|
||||||
use nom::{branch::alt, combinator::*, multi::fold_many0, IResult};
|
use nom::{branch::alt, combinator::*, multi::fold_many0, IResult};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use taskchampion::Status;
|
use taskchampion::Status;
|
||||||
@@ -36,6 +37,9 @@ pub struct Modification {
|
|||||||
/// Set the status
|
/// Set the status
|
||||||
pub status: Option<Status>,
|
pub status: Option<Status>,
|
||||||
|
|
||||||
|
/// Set (or, with `Some(None)`, clear) the wait timestamp
|
||||||
|
pub wait: Option<Option<DateTime<Utc>>>,
|
||||||
|
|
||||||
/// Set the "active" state, that is, start (true) or stop (false) the task.
|
/// Set the "active" state, that is, start (true) or stop (false) the task.
|
||||||
pub active: Option<bool>,
|
pub active: Option<bool>,
|
||||||
|
|
||||||
@@ -51,6 +55,7 @@ enum ModArg<'a> {
|
|||||||
Description(&'a str),
|
Description(&'a str),
|
||||||
PlusTag(&'a str),
|
PlusTag(&'a str),
|
||||||
MinusTag(&'a str),
|
MinusTag(&'a str),
|
||||||
|
Wait(Option<DateTime<Utc>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Modification {
|
impl Modification {
|
||||||
@@ -71,6 +76,9 @@ impl Modification {
|
|||||||
ModArg::MinusTag(tag) => {
|
ModArg::MinusTag(tag) => {
|
||||||
acc.remove_tags.insert(tag.to_owned());
|
acc.remove_tags.insert(tag.to_owned());
|
||||||
}
|
}
|
||||||
|
ModArg::Wait(wait) => {
|
||||||
|
acc.wait = Some(wait);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
acc
|
acc
|
||||||
}
|
}
|
||||||
@@ -78,6 +86,7 @@ impl Modification {
|
|||||||
alt((
|
alt((
|
||||||
Self::plus_tag,
|
Self::plus_tag,
|
||||||
Self::minus_tag,
|
Self::minus_tag,
|
||||||
|
Self::wait,
|
||||||
// this must come last
|
// this must come last
|
||||||
Self::description,
|
Self::description,
|
||||||
)),
|
)),
|
||||||
@@ -109,6 +118,13 @@ impl Modification {
|
|||||||
map_res(arg_matching(minus_tag), to_modarg)(input)
|
map_res(arg_matching(minus_tag), to_modarg)(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn wait(input: ArgList) -> IResult<ArgList, ModArg> {
|
||||||
|
fn to_modarg(input: Option<DateTime<Utc>>) -> Result<ModArg<'static>, ()> {
|
||||||
|
Ok(ModArg::Wait(input))
|
||||||
|
}
|
||||||
|
map_res(arg_matching(wait_colon), to_modarg)(input)
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn get_usage(u: &mut usage::Usage) {
|
pub(super) fn get_usage(u: &mut usage::Usage) {
|
||||||
u.modifications.push(usage::Modification {
|
u.modifications.push(usage::Modification {
|
||||||
syntax: "DESCRIPTION",
|
syntax: "DESCRIPTION",
|
||||||
@@ -122,14 +138,25 @@ impl Modification {
|
|||||||
u.modifications.push(usage::Modification {
|
u.modifications.push(usage::Modification {
|
||||||
syntax: "+TAG",
|
syntax: "+TAG",
|
||||||
summary: "Tag task",
|
summary: "Tag task",
|
||||||
description: "
|
description: "Add the given tag to the task.",
|
||||||
Add the given tag to the task.",
|
|
||||||
});
|
});
|
||||||
u.modifications.push(usage::Modification {
|
u.modifications.push(usage::Modification {
|
||||||
syntax: "-TAG",
|
syntax: "-TAG",
|
||||||
summary: "Un-tag task",
|
summary: "Un-tag task",
|
||||||
|
description: "Remove the given tag from the task.",
|
||||||
|
});
|
||||||
|
u.modifications.push(usage::Modification {
|
||||||
|
syntax: "status:{pending,completed,deleted}",
|
||||||
|
summary: "Set the task's status",
|
||||||
|
description: "Set the status of the task explicitly.",
|
||||||
|
});
|
||||||
|
u.modifications.push(usage::Modification {
|
||||||
|
syntax: "wait:<timestamp>",
|
||||||
|
summary: "Set or unset the task's wait time",
|
||||||
description: "
|
description: "
|
||||||
Remove the given tag from the task.",
|
Set the time before which the task is not actionable and should not be shown in
|
||||||
|
reports, e.g., `wait:3day` to wait for three days. With `wait:`, the time is
|
||||||
|
un-set. See the documentation for the timestamp syntax.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,6 +164,7 @@ impl Modification {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::argparse::NOW;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_empty() {
|
fn test_empty() {
|
||||||
@@ -176,6 +204,32 @@ mod test {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_set_wait() {
|
||||||
|
let (input, modification) = Modification::parse(argv!["wait:2d"]).unwrap();
|
||||||
|
assert_eq!(input.len(), 0);
|
||||||
|
assert_eq!(
|
||||||
|
modification,
|
||||||
|
Modification {
|
||||||
|
wait: Some(Some(*NOW + chrono::Duration::days(2))),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unset_wait() {
|
||||||
|
let (input, modification) = Modification::parse(argv!["wait:"]).unwrap();
|
||||||
|
assert_eq!(input.len(), 0);
|
||||||
|
assert_eq!(
|
||||||
|
modification,
|
||||||
|
Modification {
|
||||||
|
wait: Some(None),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_multi_arg_description() {
|
fn test_multi_arg_description() {
|
||||||
let (input, modification) = Modification::parse(argv!["new", "desc", "fun"]).unwrap();
|
let (input, modification) = Modification::parse(argv!["new", "desc", "fun"]).unwrap();
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ pub(crate) fn execute<W: WriteColor>(
|
|||||||
tags.sort();
|
tags.sort();
|
||||||
t.add_row(row![b->"Tags", tags.join(" ")]);
|
t.add_row(row![b->"Tags", tags.join(" ")]);
|
||||||
}
|
}
|
||||||
|
if let Some(wait) = task.get_wait() {
|
||||||
|
t.add_row(row![b->"Wait", wait]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
t.print(w)?;
|
t.print(w)?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,5 +40,9 @@ pub(super) fn apply_modification(
|
|||||||
task.remove_tag(&tag)?;
|
task.remove_tag(&tag)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(wait) = modification.wait {
|
||||||
|
task.set_wait(wait)?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ fn sort_tasks(tasks: &mut Vec<Task>, report: &Report, working_set: &WorkingSet)
|
|||||||
}
|
}
|
||||||
SortBy::Uuid => a.get_uuid().cmp(&b.get_uuid()),
|
SortBy::Uuid => a.get_uuid().cmp(&b.get_uuid()),
|
||||||
SortBy::Description => a.get_description().cmp(b.get_description()),
|
SortBy::Description => a.get_description().cmp(b.get_description()),
|
||||||
|
SortBy::Wait => a.get_wait().cmp(&b.get_wait()),
|
||||||
};
|
};
|
||||||
// If this sort property is equal, go on to the next..
|
// If this sort property is equal, go on to the next..
|
||||||
if ord == Ordering::Equal {
|
if ord == Ordering::Equal {
|
||||||
@@ -71,6 +72,13 @@ fn task_column(task: &Task, column: &Column, working_set: &WorkingSet) -> String
|
|||||||
tags.sort();
|
tags.sort();
|
||||||
tags.join(" ")
|
tags.join(" ")
|
||||||
}
|
}
|
||||||
|
Property::Wait => {
|
||||||
|
if task.is_waiting() {
|
||||||
|
task.get_wait().unwrap().format("%Y-%m-%d").to_string()
|
||||||
|
} else {
|
||||||
|
"".to_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +132,7 @@ mod test {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::invocation::test::*;
|
use crate::invocation::test::*;
|
||||||
use crate::settings::Sort;
|
use crate::settings::Sort;
|
||||||
|
use chrono::prelude::*;
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
use taskchampion::{Status, Uuid};
|
use taskchampion::{Status, Uuid};
|
||||||
|
|
||||||
@@ -217,6 +226,50 @@ mod test {
|
|||||||
assert_eq!(got_uuids, exp_uuids);
|
assert_eq!(got_uuids, exp_uuids);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sorting_by_wait() {
|
||||||
|
let mut replica = test_replica();
|
||||||
|
let uuids = create_tasks(&mut replica);
|
||||||
|
|
||||||
|
replica
|
||||||
|
.get_task(uuids[0])
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
.into_mut(&mut replica)
|
||||||
|
.set_wait(Some(Utc::now() + chrono::Duration::days(2)))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
replica
|
||||||
|
.get_task(uuids[1])
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
.into_mut(&mut replica)
|
||||||
|
.set_wait(Some(Utc::now() + chrono::Duration::days(3)))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let working_set = replica.working_set().unwrap();
|
||||||
|
|
||||||
|
let report = Report {
|
||||||
|
sort: vec![Sort {
|
||||||
|
ascending: true,
|
||||||
|
sort_by: SortBy::Wait,
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut tasks: Vec<_> = replica.all_tasks().unwrap().values().cloned().collect();
|
||||||
|
sort_tasks(&mut tasks, &report, &working_set);
|
||||||
|
let got_uuids: Vec<_> = tasks.iter().map(|t| t.get_uuid()).collect();
|
||||||
|
|
||||||
|
let exp_uuids = vec![
|
||||||
|
uuids[2], // no wait
|
||||||
|
uuids[0], // wait:2d
|
||||||
|
uuids[1], // wait:3d
|
||||||
|
];
|
||||||
|
|
||||||
|
assert_eq!(got_uuids, exp_uuids);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sorting_by_multiple() {
|
fn sorting_by_multiple() {
|
||||||
let mut replica = test_replica();
|
let mut replica = test_replica();
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ pub(crate) enum Property {
|
|||||||
|
|
||||||
/// The task's tags
|
/// The task's tags
|
||||||
Tags,
|
Tags,
|
||||||
|
|
||||||
|
/// The task's wait date
|
||||||
|
Wait,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A sorting criterion for a sort operation.
|
/// A sorting criterion for a sort operation.
|
||||||
@@ -71,6 +74,9 @@ pub(crate) enum SortBy {
|
|||||||
|
|
||||||
/// The task's description
|
/// The task's description
|
||||||
Description,
|
Description,
|
||||||
|
|
||||||
|
/// The task's wait date
|
||||||
|
Wait,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Conversions from settings::Settings.
|
// Conversions from settings::Settings.
|
||||||
@@ -174,6 +180,7 @@ impl TryFrom<&toml::Value> for Property {
|
|||||||
"active" => Property::Active,
|
"active" => Property::Active,
|
||||||
"description" => Property::Description,
|
"description" => Property::Description,
|
||||||
"tags" => Property::Tags,
|
"tags" => Property::Tags,
|
||||||
|
"wait" => Property::Wait,
|
||||||
_ => bail!(": unknown property {}", s),
|
_ => bail!(": unknown property {}", s),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -210,6 +217,7 @@ impl TryFrom<&toml::Value> for SortBy {
|
|||||||
"id" => SortBy::Id,
|
"id" => SortBy::Id,
|
||||||
"uuid" => SortBy::Uuid,
|
"uuid" => SortBy::Uuid,
|
||||||
"description" => SortBy::Description,
|
"description" => SortBy::Description,
|
||||||
|
"wait" => SortBy::Wait,
|
||||||
_ => bail!(": unknown sort_by value `{}`", s),
|
_ => bail!(": unknown sort_by value `{}`", s),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -231,6 +239,11 @@ pub(crate) fn get_usage(u: &mut Usage) {
|
|||||||
as_sort_by: None,
|
as_sort_by: None,
|
||||||
as_column: Some("`*` if the task is active (started)"),
|
as_column: Some("`*` if the task is active (started)"),
|
||||||
});
|
});
|
||||||
|
u.report_properties.push(usage::ReportProperty {
|
||||||
|
name: "wait",
|
||||||
|
as_sort_by: Some("Sort by the task's wait date, with non-waiting tasks first"),
|
||||||
|
as_column: Some("Wait date of the task"),
|
||||||
|
});
|
||||||
u.report_properties.push(usage::ReportProperty {
|
u.report_properties.push(usage::ReportProperty {
|
||||||
name: "description",
|
name: "description",
|
||||||
as_sort_by: Some("Sort by the task's description"),
|
as_sort_by: Some("Sort by the task's description"),
|
||||||
|
|||||||
@@ -218,6 +218,10 @@ impl Default for Settings {
|
|||||||
label: "tags".to_owned(),
|
label: "tags".to_owned(),
|
||||||
property: Property::Tags,
|
property: Property::Tags,
|
||||||
},
|
},
|
||||||
|
Column {
|
||||||
|
label: "wait".to_owned(),
|
||||||
|
property: Property::Wait,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
filter: Default::default(),
|
filter: Default::default(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
* [Tags](./tags.md)
|
* [Tags](./tags.md)
|
||||||
* [Filters](./filters.md)
|
* [Filters](./filters.md)
|
||||||
* [Modifications](./modifications.md)
|
* [Modifications](./modifications.md)
|
||||||
|
* [Dates and Durations](./time.md)
|
||||||
* [Configuration](./config-file.md)
|
* [Configuration](./config-file.md)
|
||||||
* [Environment](./environment.md)
|
* [Environment](./environment.md)
|
||||||
* [Synchronization](./task-sync.md)
|
* [Synchronization](./task-sync.md)
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ The following keys, and key formats, are defined:
|
|||||||
* `modified` - the time of the last modification of this task
|
* `modified` - the time of the last modification of this task
|
||||||
* `start.<timestamp>` - either an empty string (representing work on the task to the task that has not been stopped) or a timestamp (representing the time that work stopped)
|
* `start.<timestamp>` - either an empty string (representing work on the task to the task that has not been stopped) or a timestamp (representing the time that work stopped)
|
||||||
* `tag.<tag>` - indicates this task has tag `<tag>` (value is an empty string)
|
* `tag.<tag>` - indicates this task has tag `<tag>` (value is an empty string)
|
||||||
|
* `wait` - indicates the time before which this task should be hidden, as it is not actionable
|
||||||
|
|
||||||
The following are not yet implemented:
|
The following are not yet implemented:
|
||||||
|
|
||||||
|
|||||||
30
docs/src/time.md
Normal file
30
docs/src/time.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
## Timestamps
|
||||||
|
|
||||||
|
Times may be specified in a wide variety of convenient formats.
|
||||||
|
|
||||||
|
* [RFC3339](https://datatracker.ietf.org/doc/html/rfc3339) timestamps, such as `2019-10-12 07:20:50.12Z`
|
||||||
|
* A date of the format `YYYY-MM-DD` is interpreted as the _local_ midnight at the beginning of the given date.
|
||||||
|
Single-digit month and day are accepted, but the year must contain four digits.
|
||||||
|
* `now` refers to the exact current time
|
||||||
|
* `yesterday`, `today`, and `tomorrow` refer to the _local_ midnight at the beginning of the given day
|
||||||
|
* Any duration (described below) may be used as a timestamp, and is considered relative to the current time.
|
||||||
|
|
||||||
|
Times are stored internally as UTC.
|
||||||
|
|
||||||
|
## Durations
|
||||||
|
|
||||||
|
Durations can be given in a dizzying array of units.
|
||||||
|
Each can be preceded by a whole number or a decimal multiplier, e.g., `3days`.
|
||||||
|
The multiplier is optional with the singular forms of the units; for example `day` is allowed.
|
||||||
|
Some of the units allow an adjectival form, such as `daily` or `annually`; this form is more readable in some cases, but otherwise has the same meaning.
|
||||||
|
|
||||||
|
* `s`, `second`, or `seconds`
|
||||||
|
* `min`, `mins`, `minute`, or `minutes` (note that `m` not allowed, as it might also mean `month`)
|
||||||
|
* `h`, `hour`, or `hours`
|
||||||
|
* `d`, `day`, or `days`
|
||||||
|
* `w`, `week`, or `weeks`
|
||||||
|
* `mo`, or `months` (always 30 days, regardless of calendar month)
|
||||||
|
* `y`, `year`, or `years` (365 days, regardless of leap days)
|
||||||
|
|
||||||
|
[ISO 8601 standard durations](https://en.wikipedia.org/wiki/ISO_8601#Durations) are also allowed.
|
||||||
|
While the standard does not specify the length of "P1Y" or "P1M", Taskchampion treats those as 365 and 30 days, respectively.
|
||||||
@@ -211,6 +211,20 @@ impl Task {
|
|||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the wait time. If this value is set, it will be returned, even
|
||||||
|
/// if it is in the past.
|
||||||
|
pub fn get_wait(&self) -> Option<DateTime<Utc>> {
|
||||||
|
self.get_timestamp("wait")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determine whether this task is waiting now.
|
||||||
|
pub fn is_waiting(&self) -> bool {
|
||||||
|
if let Some(ts) = self.get_wait() {
|
||||||
|
return ts > Utc::now();
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
/// Determine whether this task is active -- that is, that it has been started
|
/// Determine whether this task is active -- that is, that it has been started
|
||||||
/// and not stopped.
|
/// and not stopped.
|
||||||
pub fn is_active(&self) -> bool {
|
pub fn is_active(&self) -> bool {
|
||||||
@@ -275,6 +289,10 @@ impl<'r> TaskMut<'r> {
|
|||||||
self.set_string("description", Some(description))
|
self.set_string("description", Some(description))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_wait(&mut self, wait: Option<DateTime<Utc>>) -> anyhow::Result<()> {
|
||||||
|
self.set_timestamp("wait", wait)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_modified(&mut self, modified: DateTime<Utc>) -> anyhow::Result<()> {
|
pub fn set_modified(&mut self, modified: DateTime<Utc>) -> anyhow::Result<()> {
|
||||||
self.set_timestamp("modified", Some(modified))
|
self.set_timestamp("modified", Some(modified))
|
||||||
}
|
}
|
||||||
@@ -452,6 +470,43 @@ mod test {
|
|||||||
assert!(!task.is_active());
|
assert!(!task.is_active());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_wait_not_set() {
|
||||||
|
let task = Task::new(Uuid::new_v4(), TaskMap::new());
|
||||||
|
|
||||||
|
assert!(!task.is_waiting());
|
||||||
|
assert_eq!(task.get_wait(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_wait_in_past() {
|
||||||
|
let ts = Utc.ymd(1970, 1, 1).and_hms(0, 0, 0);
|
||||||
|
let task = Task::new(
|
||||||
|
Uuid::new_v4(),
|
||||||
|
vec![(String::from("wait"), format!("{}", ts.timestamp()))]
|
||||||
|
.drain(..)
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
dbg!(&task);
|
||||||
|
|
||||||
|
assert!(!task.is_waiting());
|
||||||
|
assert_eq!(task.get_wait(), Some(ts));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_wait_in_future() {
|
||||||
|
let ts = Utc.ymd(3000, 1, 1).and_hms(0, 0, 0);
|
||||||
|
let task = Task::new(
|
||||||
|
Uuid::new_v4(),
|
||||||
|
vec![(String::from("wait"), format!("{}", ts.timestamp()))]
|
||||||
|
.drain(..)
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(task.is_waiting());
|
||||||
|
assert_eq!(task.get_wait(), Some(ts));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_has_tag() {
|
fn test_has_tag() {
|
||||||
let task = Task::new(
|
let task = Task::new(
|
||||||
|
|||||||
Reference in New Issue
Block a user