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",
|
"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]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
@ -881,7 +905,7 @@ checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"phf 0.10.1",
|
"phf 0.10.1",
|
||||||
"phf_codegen",
|
"phf_codegen 0.10.0",
|
||||||
"string_cache",
|
"string_cache",
|
||||||
"string_cache_codegen",
|
"string_cache_codegen",
|
||||||
"tendril",
|
"tendril",
|
||||||
@ -909,6 +933,7 @@ dependencies = [
|
|||||||
"base64",
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"chrono-tz",
|
||||||
"eyre",
|
"eyre",
|
||||||
"fend-core",
|
"fend-core",
|
||||||
"futures",
|
"futures",
|
||||||
@ -1004,6 +1029,15 @@ dependencies = [
|
|||||||
"windows-targets 0.48.5",
|
"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]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.1"
|
version = "2.3.1"
|
||||||
@ -1039,6 +1073,16 @@ dependencies = [
|
|||||||
"phf_shared 0.10.0",
|
"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]]
|
[[package]]
|
||||||
name = "phf_generator"
|
name = "phf_generator"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
@ -1088,6 +1132,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
|
checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"siphasher",
|
"siphasher",
|
||||||
|
"uncased",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1370,7 +1415,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"new_debug_unreachable",
|
"new_debug_unreachable",
|
||||||
"phf 0.10.1",
|
"phf 0.10.1",
|
||||||
"phf_codegen",
|
"phf_codegen 0.10.0",
|
||||||
"precomputed-hash",
|
"precomputed-hash",
|
||||||
"servo_arc",
|
"servo_arc",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
@ -1696,6 +1741,15 @@ version = "0.2.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
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]]
|
[[package]]
|
||||||
name = "unicode-bidi"
|
name = "unicode-bidi"
|
||||||
version = "0.3.14"
|
version = "0.3.14"
|
||||||
|
@ -18,6 +18,7 @@ axum = { version = "0.7.2", default-features = false, features = [
|
|||||||
base64 = "0.21.5"
|
base64 = "0.21.5"
|
||||||
bytes = "1.5.0"
|
bytes = "1.5.0"
|
||||||
chrono = "0.4.31"
|
chrono = "0.4.31"
|
||||||
|
chrono-tz = { version = "0.8.5", features = ["case-insensitive"] }
|
||||||
eyre = "0.6.11"
|
eyre = "0.6.11"
|
||||||
fend-core = "1.3.3"
|
fend-core = "1.3.3"
|
||||||
futures = "0.3.29"
|
futures = "0.3.29"
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
pub mod calc;
|
pub mod calc;
|
||||||
pub mod dictionary;
|
pub mod dictionary;
|
||||||
pub mod ip;
|
pub mod ip;
|
||||||
|
pub mod timezone;
|
||||||
pub mod useragent;
|
pub mod useragent;
|
||||||
pub mod wikipedia;
|
pub mod wikipedia;
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ pub fn request(query: &str) -> EngineResponse {
|
|||||||
};
|
};
|
||||||
|
|
||||||
EngineResponse::answer_html(format!(
|
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>"#,
|
<h3><b>{result_html}</b></h3>"#,
|
||||||
query = html_escape::encode_text(&query),
|
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");
|
let hex = spans[0].text.trim_start_matches("0x");
|
||||||
if let Ok(num) = u64::from_str_radix(hex, 16) {
|
if let Ok(num) = u64::from_str_radix(hex, 16) {
|
||||||
result_html.push_str(&format!(
|
result_html.push_str(&format!(
|
||||||
r#" <span class="answer-calc-comment">= {num}</span>"#,
|
r#" <span class="answer-comment">= {num}</span>"#,
|
||||||
num = num
|
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",
|
Calc = "calc",
|
||||||
Wikipedia = "wikipedia",
|
Wikipedia = "wikipedia",
|
||||||
Dictionary = "dictionary",
|
Dictionary = "dictionary",
|
||||||
|
Timezone = "timezone",
|
||||||
// post-search
|
// post-search
|
||||||
StackExchange = "stackexchange",
|
StackExchange = "stackexchange",
|
||||||
GitHub = "github",
|
GitHub = "github",
|
||||||
@ -49,15 +50,18 @@ engine_weights! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
engine_requests! {
|
engine_requests! {
|
||||||
|
// search
|
||||||
Google => search::google::request, parse_response,
|
Google => search::google::request, parse_response,
|
||||||
Bing => search::bing::request, parse_response,
|
Bing => search::bing::request, parse_response,
|
||||||
Brave => search::brave::request, parse_response,
|
Brave => search::brave::request, parse_response,
|
||||||
Marginalia => search::marginalia::request, parse_response,
|
Marginalia => search::marginalia::request, parse_response,
|
||||||
|
// answer
|
||||||
Useragent => answer::useragent::request, None,
|
Useragent => answer::useragent::request, None,
|
||||||
Ip => answer::ip::request, None,
|
Ip => answer::ip::request, None,
|
||||||
Calc => answer::calc::request, None,
|
Calc => answer::calc::request, None,
|
||||||
Wikipedia => answer::wikipedia::request, parse_response,
|
Wikipedia => answer::wikipedia::request, parse_response,
|
||||||
Dictionary => answer::dictionary::request, parse_response,
|
Dictionary => answer::dictionary::request, parse_response,
|
||||||
|
Timezone => answer::timezone::request, None,
|
||||||
}
|
}
|
||||||
|
|
||||||
engine_autocomplete_requests! {
|
engine_autocomplete_requests! {
|
||||||
|
@ -185,11 +185,17 @@ h1 {
|
|||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* styles for specific answers */
|
/* styles that are somewhat answer-specific but get reused across other styles sometimes */
|
||||||
.answer-calc-query {
|
.answer-query {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
.answer-comment {
|
||||||
|
color: #acb6bf8c;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* styles for specific answers */
|
||||||
.answer-calc-constant {
|
.answer-calc-constant {
|
||||||
color: #d2a6ff;
|
color: #d2a6ff;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
@ -200,10 +206,6 @@ h1 {
|
|||||||
.answer-calc-special {
|
.answer-calc-special {
|
||||||
color: #e6b673;
|
color: #e6b673;
|
||||||
}
|
}
|
||||||
.answer-calc-comment {
|
|
||||||
color: #acb6bf8c;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.answer-dictionary-word {
|
.answer-dictionary-word {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
Loading…
Reference in New Issue
Block a user