From 53d8b2365903c057a997e56891602dffa25f4f9f Mon Sep 17 00:00:00 2001 From: Tan Kian-ting Date: Tue, 11 Jul 2023 00:49:33 +0800 Subject: [PATCH] initial table --- Cargo.toml | 25 +++++ src/assets/style.css | 8 ++ src/main.bk | 200 ++++++++++++++++++++++++++++++++++++ src/main.rs | 190 ++++++++++++++++++++++++++++++++++ src/tests.rs | 71 +++++++++++++ templates/tw_stock.html.hbs | 45 ++++++++ 6 files changed, 539 insertions(+) create mode 100644 Cargo.toml create mode 100644 src/assets/style.css create mode 100644 src/main.bk create mode 100644 src/main.rs create mode 100644 src/tests.rs create mode 100644 templates/tw_stock.html.hbs diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0ccfec4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "taikoothong" +version = "0.0.0" +workspace = "../" +edition = "2021" +publish = false + + + +[dependencies] +curl = "0.4.28" +serde = {version = "1.0.51", features = ["derive"]} +serde_json = "1.0.51" +anyhow = "1.0.28" +chrono = "0.4.11" +rocket = "0.5.0-rc.2" +round = "0.1.0" +num-format = "0.4.4" + +[dependencies.rocket_dyn_templates] +rocket_dyn_templates = "0.5.0-rc.3-2023-06-09" +features = ["handlebars"] + +[toolchain] +channel = "nightly" # nassessary to install rocket diff --git a/src/assets/style.css b/src/assets/style.css new file mode 100644 index 0000000..0bb7822 --- /dev/null +++ b/src/assets/style.css @@ -0,0 +1,8 @@ +entry{ + min-width: 5em; + } + + entry#entry-2-0{ + color: red; + } + diff --git a/src/main.bk b/src/main.bk new file mode 100644 index 0000000..55724fa --- /dev/null +++ b/src/main.bk @@ -0,0 +1,200 @@ +use gtk4::prelude::*; +use gtk4::glib; +use gtk4::*; +use gdk4::Display; +use serde_json::{Result, Value}; // JSON +use anyhow; // Exception Handling +use chrono::{Utc, TimeZone}; +use curl::easy::Easy; + +enum Date{ + Day(u8), + Week(u8), + Month(u8), + YearToDate, // days of current year + Max +} +impl Date { + fn as_str(&self) -> String { + match &self{ + Date::Day(d) => format!("{:?}d", d), + Date::Week(wk) => format!("{:?}wk", wk), + Date::Month(mo) => format!("{:?}mo", mo), + Date::YearToDate => format!("ytd"), + Date::Max => format!("max"), + } + } +} + +// Try visiting: +// http://127.0.0.1:8000/wave/Rocketeer/100 + + +fn get_tw_stock(stock_id: &str) -> Option> { + // let a = "👋 Hello, stock no: a22"; + + let response_body = get_stock_data(stock_id, Date::Day(1),Date::Week(1)); + let response_json: Value = serde_json::from_str(response_body.as_str()).ok()?; + + let days_in_unix_time = &response_json["chart"]["result"][0]["timestamp"]; + + let days_in_custom_format = match days_in_unix_time { + serde_json::Value::Array(days_vec) => days_vec + .iter() + .map(|day| + {json_unix_time_to_date(day)}) + .collect::>(), + _ => vec![format!("Not a series of date")], + }; + + let price_and_volume_infos = &response_json["chart"]["result"][0]["indicators"]["quote"][0]; + + print!("{:}", price_and_volume_infos); + + + return Some(days_in_custom_format); +} + +fn json_unix_time_to_date(json_value : &Value) -> String{ + + let unix_time = json_value.as_i64().unwrap(); + println!("{:?}", unix_time); + + + let naive_time = Utc.timestamp_opt(unix_time, 0).unwrap(); + + let date = format!("{}", naive_time.format("%Y-%m-%d")); + println!("{:?}", date); + + return date; +} + +fn get_stock_data(stock_id : &str, interval : Date, range : Date) -> + String { + + let intrval_str = interval.as_str(); + let range_str = range.as_str(); + + let url = format!("https://query1.finance.yahoo.com/v8/finance/chart/\ + {:}.TW?metrics=Date,High,Low,Open,Close,Volume&interval={:}&range={:}", stock_id, + intrval_str, range_str); + + let mut curl_easy = Easy::new(); // fetch the data with the curl binding + let mut response = String::new(); + + { + curl_easy.url(url.as_str()).unwrap(); + + + let mut curl_transfer = curl_easy.transfer(); + + curl_transfer.write_function(|data| { + response.push_str(std::str::from_utf8(data).unwrap()); + Ok(data.len()) + }).unwrap(); + + curl_transfer.perform().unwrap(); + } + + let response_returned = response.clone(); + + + + return response_returned; + + +} + +fn main() -> glib::ExitCode{ + let application = Application::builder() + .application_id("info.kianting.sns.taiuankoo") // app ê id + .build(); + + application.connect_startup(|_| load_css()); + application.connect_activate(build_ui); // 連結起做ui ê + application.run() // 紡app +} + +fn load_css() { + let provider = CssProvider::new(); + + //載css kàu style provider + provider.load_from_data(include_str!("./assets/style.css")); + + // kā載入去 + gtk4::style_context_add_provider_for_display( + &gdk4::Display::default().expect("無法連結到顯示"), + &provider, + gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION, + ); +} + + +// 起做ui +fn build_ui(application: >k4::Application) { + // Create a new window, set its title and default size + let window = gtk4::ApplicationWindow::new(application); + window.set_title(Some("臺灣股")); + window.set_default_size(1200, 1000); + + let main_grid = Grid::builder() + .margin_start(1) // 倒爿ê邊仔留空白ê闊度 + .margin_end(1) // 正爿ê邊仔留空白ê闊度 + .margin_top(1)// 頂懸ê邊仔留空白ê懸度 + .margin_bottom(1) // 下底ê邊仔留空白ê懸度 + .halign(Align::Start)//水平排列 + .valign(Align::Start)//垂直排列 + .row_spacing(1)// 逐列ê縫留空白ê懸度 + .column_spacing(1)// 逐欄ê縫留空白ê闊度 + .build(); + window.set_child(Some(&main_grid)); + + + // create and add the wrapper scrolled to the window + let data_grid_scrolled_wrapper = ScrolledWindow::new(); + data_grid_scrolled_wrapper.set_min_content_width(800); + + main_grid.attach(&data_grid_scrolled_wrapper, 1, 1, 1, 1); + + + // Here we construct the grid that is going contain our buttons. + let data_grid = Grid::builder() + .margin_start(1) // 倒爿ê邊仔留空白ê闊度 + .margin_end(1) // 正爿ê邊仔留空白ê闊度 + .margin_top(1)// 頂懸ê邊仔留空白ê懸度 + .margin_bottom(1) // 下底ê邊仔留空白ê懸度 + .halign(Align::Start)//水平排列 + .valign(Align::Start)//垂直排列 + .row_spacing(1)// 逐列ê縫留空白ê懸度 + .column_spacing(1)// 逐欄ê縫留空白ê闊度 + .build(); + + // data_grid.set_widget_name("data-grid"); // for css usage + data_grid_scrolled_wrapper.set_child(Some(&data_grid)); + + + + let default_col_titles =vec!["日期", "開盤", "收盤", "最高", "最低", "成交量"]; + let default_col_titles_len = default_col_titles.len(); + let col_no = 20; + for i in 0..(col_no-1){ + let mut entry = Entry::new(); + + if i < default_col_titles_len{ + entry.set_text (default_col_titles[i]); + entry.set_hexpand(true); + entry.set_vexpand(true); + gtk4::prelude::EntryExt::set_alignment(&entry, 0.5); // 0.5表示khǹg佇中央 + data_grid.attach(&entry, i as i32, 0, 1, 1); + data_grid.set_widget_name(format!("entry-{}-0", i).as_str()); // for css usage + } + else{ + entry.set_text (""); + data_grid.attach(&entry, i as i32, 0, 1, 1); + } + } + + let a = get_tw_stock("0050"); + + window.present(); +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..970821b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,190 @@ +#![feature(proc_macro_hygiene, decl_macro)] + + +use anyhow; // Exception Handling +use chrono::{TimeZone, Utc}; +use curl::easy::Easy; +use serde_json::{Result, Value}; // JSON +use std::collections::HashMap; +use serde_json::Value::Array; +use rocket_dyn_templates::Template; +use rocket::{Rocket, Build}; +use round::round; +use num_format::{Locale, WriteFormatted}; + +#[macro_use] extern crate rocket; + +enum Date { + Day(u8), + Week(u8), + Month(u8), + Max, + YearToDate} + + +impl Date { + fn as_str(&self) -> String { + match &self { + Date::Day(d) => format!("{:?}d", d), + Date::Week(wk) => format!("{:?}wk", wk), + Date::Month(mo) => format!("{:?}mo", mo), + Date::YearToDate => format!("ytd"), + Date::Max => format!("max"), + } + } +} + + + +#[get("/")] +fn get_tw_stock(stock_id: String) -> Template { + // let a = "👋 Hello, stock no: a22"; + + let response_body = get_stock_data(stock_id.as_str(), Date::Day(1), Date::Day(5)); + let response_json: Value = serde_json::from_str(response_body.as_str()).unwrap(); + + let days_in_unix_time = &response_json["chart"]["result"][0]["timestamp"]; + + let mut stock_total_data = HashMap::new(); + + let days_in_custom_format = match days_in_unix_time { + serde_json::Value::Array(days_vec) => days_vec + .iter() + .map(|day|json_unix_time_to_date(day)) + .collect::>(), + _ => vec![format!("Not a series of date")], + }; + + println!("{:?}", &days_in_unix_time); + + stock_total_data.insert("date", days_in_custom_format); + + let mut open_prices : Vec = vec![]; + let mut close_prices : Vec = vec![]; + let mut high_prices : Vec = vec![]; + let mut low_prices : Vec = vec![]; + let mut volumes : Vec = vec![]; + + + let price_and_volume_infos = &response_json["chart"]["result"][0]["indicators"]["quote"][0]; + + let open_prices_orig = &price_and_volume_infos["open"]; + let close_prices_orig = &price_and_volume_infos["close"]; + let high_prices_orig = &price_and_volume_infos["high"]; + let low_prices_orig = &price_and_volume_infos["low"]; + let volumes_orig = &price_and_volume_infos["volume"]; + + + match (open_prices_orig, close_prices_orig, high_prices_orig, low_prices_orig, volumes_orig){ + (Array(o), Array(c), Array(h), + Array(l), Array(v)) => { + for i in 0..(o.len()){ + open_prices.push(format!("{:0.2}", o[i].as_f64().unwrap())); + close_prices.push(format!("{:0.2}", c[i].as_f64().unwrap())); + high_prices.push(format!("{:0.2}", h[i].as_f64().unwrap())); + low_prices.push(format!("{:0.2}", l[i].as_f64().unwrap())); + let mut formatted_volume = String::new(); + formatted_volume.write_formatted(&v[i].as_i64().unwrap(), &Locale::zh); + + volumes.push(formatted_volume); + } + + }, + _ => (), + } + + stock_total_data.insert("open", open_prices); + stock_total_data.insert("close", close_prices); + stock_total_data.insert("high", high_prices); + stock_total_data.insert("low", low_prices); + stock_total_data.insert("volume", volumes); + + + let mut stock_total_data_by_date = transverse_stock_data_by_date(stock_total_data.clone()); + //let mut stock_total_data_by_date_wrapper = HashMap::new(); + + //stock_total_data_by_date_wrapper.insert("data", stock_total_data_by_date); + + //println!("{:?}", stock_total_data_by_date_wrapper); + println!("{:?}", stock_total_data); + return Template::render("tw_stock", stock_total_data); +} + +fn json_unix_time_to_date(json_value: &Value) -> String { + let unix_time = json_value.as_i64().unwrap(); + println!("{:?}", unix_time); + + let naive_time = Utc.timestamp_opt(unix_time, 0).unwrap(); + + let date = format!("{}", naive_time.format("%Y-%m-%d")); + println!("{:?}", date); + + return date; +} + +fn transverse_stock_data_by_date(orig_data : HashMap<&str, Vec>) -> + Vec>{ + let mut stock_data_by_date = vec![]; + let dates = &orig_data["date"]; + + for i in 0..dates.len()-1{ + let mut day_hash_map = HashMap::new(); + day_hash_map.insert(format!("date"), orig_data["date"][i].clone()); + day_hash_map.insert(format!("open"), orig_data["open"][i].clone()); + day_hash_map.insert(format!("close"), orig_data["close"][i].clone()); + day_hash_map.insert(format!("high"), orig_data["high"][i].clone()); + day_hash_map.insert(format!("low"), orig_data["low"][i].clone()); + day_hash_map.insert(format!("volume"), orig_data["volume"][i].clone()); + + stock_data_by_date.push(day_hash_map); + + } + + return stock_data_by_date; + + +} + + +fn get_stock_data(stock_id: &str, interval: Date, range: Date) -> String { + let intrval_str = interval.as_str(); + let range_str = range.as_str(); + + let url = format!( + "https://query1.finance.yahoo.com/v8/finance/chart/\ + {:}.TW?metrics=Date,High,Low,Open,Close,Volume&interval={:}&range={:}", + stock_id, intrval_str, range_str + ); + + let mut curl_easy = Easy::new(); // fetch the data with the curl binding + let mut response = String::new(); + + { + curl_easy.url(url.as_str()).unwrap(); + + let mut curl_transfer = curl_easy.transfer(); + + curl_transfer + .write_function(|data| { + response.push_str(std::str::from_utf8(data).unwrap()); + Ok(data.len()) + }) + .unwrap(); + + curl_transfer.perform().unwrap(); + } + + let response_returned = response.clone(); + + return response_returned; +} + + +#[launch] +fn rocket() -> Rocket { + // rocket::ignite().mount("/", routes![index]).launch(); + rocket::build().attach(Template::fairing()) + .mount("/tw", routes![get_tw_stock]) + + +} diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..fd5b628 --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,71 @@ +use rocket::local::blocking::Client; +use rocket::http::{RawStr, Status}; + +#[test] +fn hello() { + let langs = &["", "ru", "%D1%80%D1%83", "en", "unknown"]; + let ex_lang = &["Hi", "Привет", "Привет", "Hello", "Hi"]; + + let emojis = &["", "on", "true", "false", "no", "yes", "off"]; + let ex_emoji = &["", "👋 ", "👋 ", "", "", "👋 ", ""]; + + let names = &["", "Bob", "Bob+Smith"]; + let ex_name = &["!", ", Bob!", ", Bob Smith!"]; + + let client = Client::tracked(super::rocket()).unwrap(); + for n in 0..(langs.len() * emojis.len() * names.len()) { + let i = n / (emojis.len() * names.len()); + let j = n % (emojis.len() * names.len()) / names.len(); + let k = n % (emojis.len() * names.len()) % names.len(); + + let (lang, ex_lang) = (langs[i], ex_lang[i]); + let (emoji, ex_emoji) = (emojis[j], ex_emoji[j]); + let (name, ex_name) = (names[k], ex_name[k]); + let expected = format!("{}{}{}", ex_emoji, ex_lang, ex_name); + + let q = |name, s: &str| match s.is_empty() { + true => "".into(), + false => format!("&{}={}", name, s) + }; + + let uri = format!("/?{}{}{}", q("lang", lang), q("emoji", emoji), q("name", name)); + let response = client.get(uri).dispatch(); + assert_eq!(response.into_string().unwrap(), expected); + + let uri = format!("/?{}{}{}", q("emoji", emoji), q("name", name), q("lang", lang)); + let response = client.get(uri).dispatch(); + assert_eq!(response.into_string().unwrap(), expected); + } +} + +#[test] +fn hello_world() { + let client = Client::tracked(super::rocket()).unwrap(); + let response = client.get("/hello/world").dispatch(); + assert_eq!(response.into_string(), Some("Hello, world!".into())); +} + +#[test] +fn hello_mir() { + let client = Client::tracked(super::rocket()).unwrap(); + let response = client.get("/hello/%D0%BC%D0%B8%D1%80").dispatch(); + assert_eq!(response.into_string(), Some("Привет, мир!".into())); +} + +#[test] +fn wave() { + let client = Client::tracked(super::rocket()).unwrap(); + for &(name, age) in &[("Bob%20Smith", 22), ("Michael", 80), ("A", 0), ("a", 127)] { + let uri = format!("/wave/{}/{}", name, age); + let real_name = RawStr::new(name).percent_decode_lossy(); + let expected = format!("👋 Hello, {} year old named {}!", age, real_name); + let response = client.get(uri).dispatch(); + assert_eq!(response.into_string().unwrap(), expected); + + for bad_age in &["1000", "-1", "bird", "?"] { + let bad_uri = format!("/wave/{}/{}", name, bad_age); + let response = client.get(bad_uri).dispatch(); + assert_eq!(response.status(), Status::NotFound); + } + } +} diff --git a/templates/tw_stock.html.hbs b/templates/tw_stock.html.hbs new file mode 100644 index 0000000..2fcfbb5 --- /dev/null +++ b/templates/tw_stock.html.hbs @@ -0,0 +1,45 @@ + + + +台股通 + + + + + + + + + + + + + + + +{{#each date}} + + + + + + + + +{{/each}} +
日期開盤收盤最高最低成交量
{{this}}{{lookup ../open @index}}{{lookup ../close @index}}{{lookup ../high @index}}{{lookup ../low @index}}{{lookup ../volume @index}}
+ + \ No newline at end of file -- 2.39.2