Skip to content

Commit d72671d

Browse files
authored
chore: deduplication helper time parsing function
1 parent 433f380 commit d72671d

4 files changed

Lines changed: 54 additions & 56 deletions

File tree

src/sync.rs

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use crate::experiments::api::create_experiment;
2626
use crate::http::ApiClient;
2727
use crate::projects::api::{create_project, list_projects, Project};
2828
use crate::ui::{animations_enabled, fuzzy_select, is_quiet};
29+
use crate::utils::parse_duration_to_seconds;
2930

3031
const STATE_SCHEMA_VERSION: u32 = 1;
3132
const DEFAULT_PULL_LIMIT: usize = 100;
@@ -2606,30 +2607,6 @@ fn build_root_spans_query(
26062607
parts.join(" | ")
26072608
}
26082609

2609-
fn parse_duration_to_seconds(input: &str) -> Result<u64> {
2610-
let trimmed = input.trim();
2611-
if trimmed.is_empty() {
2612-
bail!("duration cannot be empty");
2613-
}
2614-
if let Ok(seconds) = trimmed.parse::<u64>() {
2615-
return Ok(seconds);
2616-
}
2617-
2618-
let (num_str, unit) = trimmed.split_at(trimmed.len().saturating_sub(1));
2619-
let value: u64 = num_str
2620-
.trim()
2621-
.parse()
2622-
.with_context(|| format!("invalid duration '{input}'"))?;
2623-
let multiplier = match unit.to_ascii_lowercase().as_str() {
2624-
"s" => 1,
2625-
"m" => 60,
2626-
"h" => 60 * 60,
2627-
"d" => 60 * 60 * 24,
2628-
_ => bail!("invalid duration '{input}'. expected suffix s/m/h/d"),
2629-
};
2630-
Ok(value.saturating_mul(multiplier))
2631-
}
2632-
26332610
fn build_time_filter_clause(window: &str, extra_filter: Option<&str>) -> Result<String> {
26342611
let seconds = parse_duration_to_seconds(window)?;
26352612
let time_clause = format!("created >= NOW() - INTERVAL {seconds} SECOND");

src/traces.rs

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ use crate::args::BaseArgs;
3636
use crate::auth::{self, login};
3737
use crate::http::ApiClient;
3838
use crate::ui::{fuzzy_select, is_interactive, with_spinner};
39+
use crate::utils::parse_duration_to_seconds;
3940

4041
const MAX_TRACE_SPANS: usize = 5000;
4142
const MAX_BTQL_PAGE_LIMIT: usize = 1000;
@@ -5030,30 +5031,6 @@ fn print_span_text(item: Option<&Map<String, Value>>) {
50305031
}
50315032
}
50325033

5033-
fn parse_duration_to_seconds(input: &str) -> Result<u64> {
5034-
let trimmed = input.trim();
5035-
if trimmed.is_empty() {
5036-
bail!("duration cannot be empty");
5037-
}
5038-
if let Ok(seconds) = trimmed.parse::<u64>() {
5039-
return Ok(seconds);
5040-
}
5041-
5042-
let (num_str, unit) = trimmed.split_at(trimmed.len().saturating_sub(1));
5043-
let value: u64 = num_str
5044-
.trim()
5045-
.parse()
5046-
.with_context(|| format!("invalid duration '{input}'"))?;
5047-
let multiplier = match unit.to_ascii_lowercase().as_str() {
5048-
"s" => 1,
5049-
"m" => 60,
5050-
"h" => 60 * 60,
5051-
"d" => 60 * 60 * 24,
5052-
_ => bail!("invalid duration '{input}'. expected suffix s/m/h/d"),
5053-
};
5054-
Ok(value.saturating_mul(multiplier))
5055-
}
5056-
50575034
fn build_base_filter_clause(
50585035
since: Option<&str>,
50595036
window: &str,
@@ -6179,14 +6156,6 @@ mod tests {
61796156
}
61806157
}
61816158

6182-
#[test]
6183-
fn parse_duration_to_seconds_supports_units() {
6184-
assert_eq!(parse_duration_to_seconds("90").expect("seconds"), 90);
6185-
assert_eq!(parse_duration_to_seconds("15m").expect("minutes"), 900);
6186-
assert_eq!(parse_duration_to_seconds("2h").expect("hours"), 7_200);
6187-
assert_eq!(parse_duration_to_seconds("1d").expect("days"), 86_400);
6188-
}
6189-
61906159
#[test]
61916160
fn build_base_filter_clause_uses_window_or_since() {
61926161
let from_window = build_base_filter_clause(None, "1h", Some("metadata.model IS NOT NULL"))

src/utils/duration.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
use anyhow::{bail, Context, Result};
2+
3+
pub fn parse_duration_to_seconds(input: &str) -> Result<u64> {
4+
let trimmed = input.trim();
5+
if trimmed.is_empty() {
6+
bail!("duration cannot be empty");
7+
}
8+
if let Ok(seconds) = trimmed.parse::<u64>() {
9+
return Ok(seconds);
10+
}
11+
12+
let suffix = trimmed.chars().last().filter(|ch| ch.is_ascii_alphabetic());
13+
let (num_str, unit) = match suffix {
14+
Some(unit) => (&trimmed[..trimmed.len() - unit.len_utf8()], unit),
15+
None => (trimmed, 's'),
16+
};
17+
let value: u64 = num_str
18+
.trim()
19+
.parse()
20+
.with_context(|| format!("invalid duration '{input}'"))?;
21+
let multiplier = match unit.to_ascii_lowercase() {
22+
's' => 1,
23+
'm' => 60,
24+
'h' => 60 * 60,
25+
'd' => 60 * 60 * 24,
26+
_ => bail!("invalid duration '{input}'. expected suffix s/m/h/d"),
27+
};
28+
Ok(value.saturating_mul(multiplier))
29+
}
30+
31+
#[cfg(test)]
32+
mod tests {
33+
use super::*;
34+
35+
#[test]
36+
fn supports_units() {
37+
assert_eq!(parse_duration_to_seconds("90").expect("seconds"), 90);
38+
assert_eq!(parse_duration_to_seconds("15m").expect("minutes"), 900);
39+
assert_eq!(parse_duration_to_seconds("2h").expect("hours"), 7_200);
40+
assert_eq!(parse_duration_to_seconds("1d").expect("days"), 86_400);
41+
}
42+
43+
#[test]
44+
fn rejects_non_ascii_suffix_without_panicking() {
45+
for input in ["1–", "1é", "1🙂"] {
46+
let err = parse_duration_to_seconds(input).expect_err("invalid unicode suffix");
47+
assert!(err.to_string().contains("invalid duration"));
48+
}
49+
}
50+
}

src/utils/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
mod duration;
12
mod fs_atomic;
23
mod git;
34
mod ids;
45
mod json_object;
56
mod plurals;
67

8+
pub use duration::parse_duration_to_seconds;
79
pub use fs_atomic::write_text_atomic;
810
pub use git::GitRepo;
911
pub(crate) use ids::new_uuid_id;

0 commit comments

Comments
 (0)