initial table
This commit is contained in:
parent
8e74197df9
commit
53d8b23659
6 changed files with 539 additions and 0 deletions
25
Cargo.toml
Normal file
25
Cargo.toml
Normal file
|
@ -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
|
8
src/assets/style.css
Normal file
8
src/assets/style.css
Normal file
|
@ -0,0 +1,8 @@
|
|||
entry{
|
||||
min-width: 5em;
|
||||
}
|
||||
|
||||
entry#entry-2-0{
|
||||
color: red;
|
||||
}
|
||||
|
200
src/main.bk
Normal file
200
src/main.bk
Normal file
|
@ -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<Vec<String>> {
|
||||
// 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<_>>(),
|
||||
_ => 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();
|
||||
}
|
190
src/main.rs
Normal file
190
src/main.rs
Normal file
|
@ -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("/<stock_id>")]
|
||||
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<_>>(),
|
||||
_ => 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<String> = vec![];
|
||||
let mut close_prices : Vec<String> = vec![];
|
||||
let mut high_prices : Vec<String> = vec![];
|
||||
let mut low_prices : Vec<String> = vec![];
|
||||
let mut volumes : Vec<String> = 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<String>>) ->
|
||||
Vec<HashMap<String, String>>{
|
||||
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<Build> {
|
||||
// rocket::ignite().mount("/", routes![index]).launch();
|
||||
rocket::build().attach(Template::fairing())
|
||||
.mount("/tw", routes![get_tw_stock])
|
||||
|
||||
|
||||
}
|
71
src/tests.rs
Normal file
71
src/tests.rs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
45
templates/tw_stock.html.hbs
Normal file
45
templates/tw_stock.html.hbs
Normal file
|
@ -0,0 +1,45 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<title>台股通</title>
|
||||
<style type="text/css">
|
||||
#stock-table{
|
||||
border-collapse: collapse;
|
||||
|
||||
}
|
||||
#stock-table th {
|
||||
border: 1px solid red;
|
||||
}
|
||||
#stock-table td {
|
||||
text-align: right;
|
||||
padding: 1em 0.6em;
|
||||
border: 1px solid red;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
|
||||
<html>
|
||||
<body>
|
||||
<table id="stock-table">
|
||||
<tr id="title-of-table">
|
||||
<th>日期</th>
|
||||
<th>開盤</th>
|
||||
<th>收盤</th>
|
||||
<th>最高</th>
|
||||
<th>最低</th>
|
||||
<th>成交量</th>
|
||||
</tr>
|
||||
{{#each date}}
|
||||
<tr id={{this}}>
|
||||
<td id="date">{{this}}</td>
|
||||
<td class="open">{{lookup ../open @index}}</td>
|
||||
<td class="close">{{lookup ../close @index}}</td>
|
||||
<td class="high">{{lookup ../high @index}}</td>
|
||||
<td class="low">{{lookup ../low @index}}</td>
|
||||
<td class="volume">{{lookup ../volume @index}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in a new issue