add timezone engine
This commit is contained in:
parent
b9ace5a34f
commit
c1d982b67a
58
Cargo.lock
generated
58
Cargo.lock
generated
@ -289,6 +289,30 @@ dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono-tz"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91d7b79e99bfaa0d47da0687c43aa3b7381938a62ad3a6498599039321f660b7"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"chrono-tz-build",
|
||||
"phf 0.11.2",
|
||||
"uncased",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono-tz-build"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "433e39f13c9a060046954e0592a8d0a4bcb1040125cbf91cb8ee58964cfb350f"
|
||||
dependencies = [
|
||||
"parse-zoneinfo",
|
||||
"phf 0.11.2",
|
||||
"phf_codegen 0.11.2",
|
||||
"uncased",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
@ -881,7 +905,7 @@ checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016"
|
||||
dependencies = [
|
||||
"log",
|
||||
"phf 0.10.1",
|
||||
"phf_codegen",
|
||||
"phf_codegen 0.10.0",
|
||||
"string_cache",
|
||||
"string_cache_codegen",
|
||||
"tendril",
|
||||
@ -909,6 +933,7 @@ dependencies = [
|
||||
"base64",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
"eyre",
|
||||
"fend-core",
|
||||
"futures",
|
||||
@ -1004,6 +1029,15 @@ dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parse-zoneinfo"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41"
|
||||
dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.1"
|
||||
@ -1039,6 +1073,16 @@ dependencies = [
|
||||
"phf_shared 0.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_codegen"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a"
|
||||
dependencies = [
|
||||
"phf_generator 0.11.2",
|
||||
"phf_shared 0.11.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.10.0"
|
||||
@ -1088,6 +1132,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
|
||||
dependencies = [
|
||||
"siphasher",
|
||||
"uncased",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1370,7 +1415,7 @@ dependencies = [
|
||||
"log",
|
||||
"new_debug_unreachable",
|
||||
"phf 0.10.1",
|
||||
"phf_codegen",
|
||||
"phf_codegen 0.10.0",
|
||||
"precomputed-hash",
|
||||
"servo_arc",
|
||||
"smallvec",
|
||||
@ -1696,6 +1741,15 @@ version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "uncased"
|
||||
version = "0.9.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b9bc53168a4be7402ab86c3aad243a84dd7381d09be0eddc81280c1da95ca68"
|
||||
dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.14"
|
||||
|
@ -18,6 +18,7 @@ axum = { version = "0.7.2", default-features = false, features = [
|
||||
base64 = "0.21.5"
|
||||
bytes = "1.5.0"
|
||||
chrono = "0.4.31"
|
||||
chrono-tz = { version = "0.8.5", features = ["case-insensitive"] }
|
||||
eyre = "0.6.11"
|
||||
fend-core = "1.3.3"
|
||||
futures = "0.3.29"
|
||||
|
@ -1,6 +1,7 @@
|
||||
pub mod calc;
|
||||
pub mod dictionary;
|
||||
pub mod ip;
|
||||
pub mod timezone;
|
||||
pub mod useragent;
|
||||
pub mod wikipedia;
|
||||
|
||||
|
@ -14,7 +14,7 @@ pub fn request(query: &str) -> EngineResponse {
|
||||
};
|
||||
|
||||
EngineResponse::answer_html(format!(
|
||||
r#"<p class="answer-calc-query">{query} =</p>
|
||||
r#"<p class="answer-query">{query} =</p>
|
||||
<h3><b>{result_html}</b></h3>"#,
|
||||
query = html_escape::encode_text(&query),
|
||||
))
|
||||
@ -87,7 +87,7 @@ fn evaluate(query: &str, html: bool) -> Option<String> {
|
||||
let hex = spans[0].text.trim_start_matches("0x");
|
||||
if let Ok(num) = u64::from_str_radix(hex, 16) {
|
||||
result_html.push_str(&format!(
|
||||
r#" <span class="answer-calc-comment">= {num}</span>"#,
|
||||
r#" <span class="answer-comment">= {num}</span>"#,
|
||||
num = num
|
||||
));
|
||||
}
|
||||
|
157
src/engines/answer/timezone.rs
Normal file
157
src/engines/answer/timezone.rs
Normal file
@ -0,0 +1,157 @@
|
||||
use chrono::{DateTime, TimeZone};
|
||||
use chrono_tz::{OffsetComponents, OffsetName, Tz};
|
||||
|
||||
use crate::engines::EngineResponse;
|
||||
|
||||
use super::regex;
|
||||
|
||||
pub fn request(query: &str) -> EngineResponse {
|
||||
match evaluate(query) {
|
||||
None => EngineResponse::new(),
|
||||
Some(TimeResponse::Current { time, timezone }) => EngineResponse::answer_html(format!(
|
||||
r#"<p class="answer-query">Current time in {timezone}</p>
|
||||
<h3><b>{time}</b> <span class="answer-comment">({date})</span></h3>"#,
|
||||
time = html_escape::encode_text(&time.format("%-I:%M %P").to_string()),
|
||||
date = html_escape::encode_text(&time.format("%B %-d").to_string()),
|
||||
timezone = html_escape::encode_text(&timezone_to_string(&timezone)),
|
||||
)),
|
||||
Some(TimeResponse::Conversion {
|
||||
source_timezone,
|
||||
target_timezone,
|
||||
source_time,
|
||||
target_time,
|
||||
source_offset,
|
||||
target_offset,
|
||||
}) => EngineResponse::answer_html(format!(
|
||||
r#"<p class="answer-query">{source_time} {source_timezone} to {target_timezone}</p>
|
||||
<h3><b>{target_time}</b> <span class="answer-comment">{target_timezone} ({delta})</span></h3>"#,
|
||||
source_time = html_escape::encode_text(&source_time.format("%-I:%M %P").to_string()),
|
||||
target_time = html_escape::encode_text(&target_time.format("%-I:%M %P").to_string()),
|
||||
source_timezone = html_escape::encode_text(&timezone_to_string(&source_timezone)),
|
||||
target_timezone = html_escape::encode_text(&timezone_to_string(&target_timezone)),
|
||||
delta = html_escape::encode_text(&{
|
||||
let delta_minutes = (target_offset - source_offset).num_minutes();
|
||||
if delta_minutes % 60 == 0 {
|
||||
format!("{:+}", delta_minutes / 60)
|
||||
} else {
|
||||
format!("{:+}:{}", delta_minutes / 60, delta_minutes % 60)
|
||||
}
|
||||
})
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
enum TimeResponse {
|
||||
Current {
|
||||
time: DateTime<Tz>,
|
||||
timezone: Tz,
|
||||
},
|
||||
Conversion {
|
||||
source_timezone: Tz,
|
||||
target_timezone: Tz,
|
||||
source_time: DateTime<Tz>,
|
||||
target_time: DateTime<Tz>,
|
||||
source_offset: chrono::Duration,
|
||||
target_offset: chrono::Duration,
|
||||
},
|
||||
}
|
||||
|
||||
fn evaluate(query: &str) -> Option<TimeResponse> {
|
||||
// "4pm utc to cst"
|
||||
let re = regex!(r"(\d{1,2})(?:(\d{1,2}))?\s*(am|pm|) ([\w/+\-]+) (to|as|in) ([\w/+\-]+)");
|
||||
if let Some(captures) = re.captures(query) {
|
||||
if let Some(hour) = captures.get(1).map(|m| m.as_str().parse::<u32>().unwrap()) {
|
||||
let minute = match captures.get(2) {
|
||||
Some(m) => m.as_str().parse::<u32>().ok()?,
|
||||
None => 0,
|
||||
};
|
||||
let ampm = captures.get(3).unwrap().as_str();
|
||||
let timezone1_name = captures.get(4).unwrap().as_str();
|
||||
let timezone2_name = captures.get(6).unwrap().as_str();
|
||||
|
||||
let source_timezone = parse_timezone(timezone1_name)?;
|
||||
let target_timezone = parse_timezone(timezone2_name)?;
|
||||
|
||||
let current_date = chrono::Utc::now().date_naive();
|
||||
|
||||
let source_offset = source_timezone.offset_from_utc_date(¤t_date);
|
||||
let target_offset = target_timezone.offset_from_utc_date(¤t_date);
|
||||
|
||||
println!(
|
||||
"source_offset: {:?} {:?}",
|
||||
source_offset,
|
||||
source_offset.tz_id()
|
||||
);
|
||||
println!("target_offset: {:?}", target_offset);
|
||||
|
||||
let source_time_naive = current_date.and_hms_opt(
|
||||
if ampm == "pm" && hour != 12 {
|
||||
hour + 12
|
||||
} else if ampm == "am" && hour == 12 {
|
||||
0
|
||||
} else {
|
||||
hour
|
||||
},
|
||||
minute,
|
||||
0,
|
||||
)?;
|
||||
let source_time_utc = chrono::Utc
|
||||
.from_local_datetime(&source_time_naive)
|
||||
.latest()?;
|
||||
|
||||
let source_time = source_time_utc.with_timezone(&source_timezone);
|
||||
let target_time = source_time_utc.with_timezone(&target_timezone);
|
||||
|
||||
return Some(TimeResponse::Conversion {
|
||||
source_timezone,
|
||||
target_timezone,
|
||||
source_time,
|
||||
target_time,
|
||||
// the offsets are wrong for some reason so we have to negate them
|
||||
source_offset: -source_offset.base_utc_offset(),
|
||||
target_offset: -target_offset.base_utc_offset(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// "utc time"
|
||||
let re = regex!(r"([\w/+\-]+)(?: current)? time");
|
||||
// "time in utc"
|
||||
let re2 = regex!(r"time (?:in|as) ([\w/+\-]+)");
|
||||
if let Some(timezone_name) = re
|
||||
.captures(query)
|
||||
.and_then(|m| m.get(1))
|
||||
.or_else(|| re2.captures(query).and_then(|m| m.get(1)))
|
||||
{
|
||||
if let Some(timezone) = parse_timezone(timezone_name.as_str()) {
|
||||
let time = chrono::Utc::now().with_timezone(&timezone);
|
||||
return Some(TimeResponse::Current { time, timezone });
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn parse_timezone(timezone_name: &str) -> Option<Tz> {
|
||||
match timezone_name.to_lowercase().as_str() {
|
||||
"cst" => Some(Tz::CST6CDT),
|
||||
"cdt" => Some(Tz::CST6CDT),
|
||||
_ => Tz::from_str_insensitive(timezone_name)
|
||||
.ok()
|
||||
.or_else(|| Tz::from_str_insensitive(&format!("etc/{timezone_name}")).ok()),
|
||||
}
|
||||
}
|
||||
|
||||
fn timezone_to_string(tz: &Tz) -> String {
|
||||
match tz {
|
||||
Tz::CST6CDT => "CST".to_string(),
|
||||
_ => {
|
||||
let tz_string = tz.name();
|
||||
if let Some(tz_string) = tz_string.strip_prefix("Etc/") {
|
||||
tz_string.to_string()
|
||||
} else {
|
||||
tz_string.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -34,6 +34,7 @@ engines! {
|
||||
Calc = "calc",
|
||||
Wikipedia = "wikipedia",
|
||||
Dictionary = "dictionary",
|
||||
Timezone = "timezone",
|
||||
// post-search
|
||||
StackExchange = "stackexchange",
|
||||
GitHub = "github",
|
||||
@ -49,15 +50,18 @@ engine_weights! {
|
||||
}
|
||||
|
||||
engine_requests! {
|
||||
// search
|
||||
Google => search::google::request, parse_response,
|
||||
Bing => search::bing::request, parse_response,
|
||||
Brave => search::brave::request, parse_response,
|
||||
Marginalia => search::marginalia::request, parse_response,
|
||||
// answer
|
||||
Useragent => answer::useragent::request, None,
|
||||
Ip => answer::ip::request, None,
|
||||
Calc => answer::calc::request, None,
|
||||
Wikipedia => answer::wikipedia::request, parse_response,
|
||||
Dictionary => answer::dictionary::request, parse_response,
|
||||
Timezone => answer::timezone::request, None,
|
||||
}
|
||||
|
||||
engine_autocomplete_requests! {
|
||||
|
@ -185,11 +185,17 @@ h1 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
/* styles for specific answers */
|
||||
.answer-calc-query {
|
||||
/* styles that are somewhat answer-specific but get reused across other styles sometimes */
|
||||
.answer-query {
|
||||
margin: 0;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.answer-comment {
|
||||
color: #acb6bf8c;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* styles for specific answers */
|
||||
.answer-calc-constant {
|
||||
color: #d2a6ff;
|
||||
white-space: pre-wrap;
|
||||
@ -200,10 +206,6 @@ h1 {
|
||||
.answer-calc-special {
|
||||
color: #e6b673;
|
||||
}
|
||||
.answer-calc-comment {
|
||||
color: #acb6bf8c;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.answer-dictionary-word {
|
||||
margin-top: 0;
|
||||
|
Loading…
Reference in New Issue
Block a user