use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::OnceLock;
const TERMS_JSON: &str = include_str!("plot_terms.json");
const CITY: &str = "cyber valley";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LandUse {
Lease,
Venture,
Commons,
Hgb,
Dual,
Special,
Unclassified,
}
impl LandUse {
pub const ALL: [LandUse; 7] = [
LandUse::Lease,
LandUse::Dual,
LandUse::Hgb,
LandUse::Venture,
LandUse::Commons,
LandUse::Special,
LandUse::Unclassified,
];
pub fn from_key(k: &str) -> Self {
match k {
"lease" => LandUse::Lease,
"venture" => LandUse::Venture,
"commons" => LandUse::Commons,
"hgb" => LandUse::Hgb,
"dual" => LandUse::Dual,
"special" => LandUse::Special,
_ => LandUse::Unclassified,
}
}
pub fn label(self) -> &'static str {
match self {
LandUse::Lease => "RESIDENCE",
LandUse::Venture => "BUSINESS",
LandUse::Commons => "CITY LAND",
LandUse::Hgb => "HGB SALE · WHOLE DISTRICT",
LandUse::Dual => "DUAL USE",
LandUse::Special => "SPECIAL PLACE",
LandUse::Unclassified => "UNCLASSIFIED",
}
}
pub fn short(self) -> &'static str {
match self {
LandUse::Lease => "RESIDENCE",
LandUse::Venture => "BUSINESS",
LandUse::Commons => "CITY LAND",
LandUse::Hgb => "FOR SALE",
LandUse::Dual => "DUAL USE",
LandUse::Special => "SPECIAL",
LandUse::Unclassified => "UNSET",
}
}
pub fn line(self) -> &'static str {
match self {
LandUse::Lease => "a home on a 25-year leasehold",
LandUse::Venture => "ground for a business",
LandUse::Commons => "belongs to the city · shared by everyone",
LandUse::Hgb => "the whole district sold as one HGB title",
LandUse::Dual => "business and housing on one flat",
LandUse::Special => "high-energy ground · held by the valley",
LandUse::Unclassified => "land use not set on the map yet",
}
}
pub fn color(self) -> &'static str {
match self {
LandUse::Lease => "#00fe00",
LandUse::Venture => "#ff5b00",
LandUse::Commons => "#d500f9",
LandUse::Hgb => "#00acff",
LandUse::Dual => "#fcf000",
LandUse::Special => "#ff0000",
LandUse::Unclassified => "#4b4b4d",
}
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct Terms {
#[serde(default)]
pub fill: String,
#[serde(default)]
pub land_use: String,
#[serde(default)]
pub address: String,
#[serde(default)]
pub kind: String,
#[serde(default)]
pub ares: Option<f64>,
#[serde(default)]
pub owner: String,
#[serde(default)]
pub purpose: String,
#[serde(default)]
pub are_price_usd: Option<f64>,
#[serde(default)]
pub plot_price_usd: Option<f64>,
#[serde(default)]
pub map_use: String,
#[serde(default)]
pub buyer_mark: String,
#[serde(default)]
pub note: String,
#[serde(default)]
pub hold: String,
#[serde(default)]
pub niche: String,
#[serde(default)]
pub measured_m2: Option<f64>,
#[serde(default)]
pub measured_on: String,
}
impl Terms {
pub fn land_use(&self) -> LandUse {
LandUse::from_key(&self.land_use)
}
pub fn holder(&self) -> Option<&str> {
let o = self.owner.trim();
(!o.is_empty() && !o.eq_ignore_ascii_case(CITY)).then_some(o)
}
pub fn is_city_land(&self) -> bool {
self.kind == "commons" || self.owner.trim().eq_ignore_ascii_case(CITY)
}
pub fn kind_word(&self) -> &'static str {
match self.kind.as_str() {
"private" => "residence",
"community" | "commerce" => "business",
"commons" => "city land",
_ => "",
}
}
pub fn price(&self) -> Option<f64> {
self.plot_price_usd.filter(|p| *p > 0.0)
}
pub fn in_reregistration(&self) -> bool {
self.hold == "re-registration"
}
pub fn is_construction(&self) -> bool {
self.note == "construction business"
}
pub fn kind_line(&self) -> String {
if !self.note.is_empty() {
return if self.is_city_land() {
format!("{} of the city · {}", self.note, self.purpose)
} else {
self.note.clone()
};
}
let base = match (self.kind_word(), self.purpose.is_empty()) {
("", _) => "not in the plot sheet".to_string(),
(w, true) => w.to_string(),
(w, false) => format!("{w} · {}", self.purpose),
};
if self.niche.is_empty() {
base
} else {
format!("{base} · {} niche", self.niche)
}
}
}
fn book() -> &'static HashMap<String, Terms> {
static BOOK: OnceLock<HashMap<String, Terms>> = OnceLock::new();
BOOK.get_or_init(|| serde_json::from_str(TERMS_JSON).unwrap_or_default())
}
pub fn terms(id: &str) -> Option<&'static Terms> {
book().get(id)
}
pub fn land_use(id: &str) -> LandUse {
terms(id).map(|t| t.land_use()).unwrap_or(LandUse::Unclassified)
}
pub struct DistrictSale {
pub flats: usize,
pub ares: f64,
pub are_price: f64,
}
impl DistrictSale {
pub fn total(&self) -> f64 {
self.ares * self.are_price
}
}
pub fn district_sale(zone: &str, areas: &[(String, String, f64)]) -> Option<DistrictSale> {
let mut flats = 0;
let mut ares = 0.0;
let mut are_price = None;
for (id, z, m2) in areas {
if z != zone {
continue;
}
let Some(t) = terms(id) else { continue };
if t.land_use() != LandUse::Hgb {
continue;
}
flats += 1;
ares += t.ares.unwrap_or(m2 / 100.0);
are_price = are_price.or(t.are_price_usd);
}
Some(DistrictSale {
flats,
ares,
are_price: are_price?,
})
}
pub fn fmt_usd(v: f64) -> String {
let n = v.round() as i64;
let s = n.abs().to_string();
let mut out = String::new();
for (i, ch) in s.chars().enumerate() {
if i > 0 && (s.len() - i) % 3 == 0 {
out.push(',');
}
out.push(ch);
}
format!("{}${out}", if n < 0 { "-" } else { "" })
}
pub fn price_line(t: &Terms) -> Option<String> {
let p = t.price()?;
Some(match t.are_price_usd {
Some(a) if a > 0.0 => format!("{} · {} / are", fmt_usd(p), fmt_usd(a)),
_ => fmt_usd(p),
})
}
fn fmt_m2(v: f64) -> String {
let whole = fmt_usd(v.trunc()).trim_start_matches('$').to_string();
let frac = ((v - v.trunc()) * 100.0).round() as i64;
if frac == 0 {
whole
} else {
format!("{whole}.{frac:02}")
}
}
fn fmt_ares(a: f64) -> String {
let s = format!("{a:.2}");
let s = s.trim_end_matches('0').trim_end_matches('.');
format!("{s} ares")
}
pub fn size_line(t: Option<&Terms>, drawn_m2: f64) -> String {
match (t.and_then(|t| t.measured_m2), t.and_then(|t| t.ares)) {
(Some(m), _) => fmt_ares(m / 100.0),
(None, Some(a)) => fmt_ares(a),
(None, None) => fmt_ares((drawn_m2 / 10.0).round() / 10.0),
}
}
pub fn size_detail(t: Option<&Terms>, drawn_m2: f64) -> String {
match (t.and_then(|t| t.measured_m2), t.and_then(|t| t.ares)) {
(Some(m), _) => format!(
"{} · {} m² surveyed on the slope · {drawn_m2:.0} m² in plan on the map",
fmt_ares(m / 100.0),
fmt_m2(m)
),
(None, Some(a)) => format!("{} · {:.0} m²", fmt_ares(a), a * 100.0),
(None, None) => format!("{} · {drawn_m2:.0} m² drawn on the map", fmt_ares((drawn_m2 / 10.0).round() / 10.0)),
}
}
pub fn bare_name(name: &str) -> String {
let head = name.split(":@").next().unwrap_or(name);
let joined: String = head.split_whitespace().collect::<Vec<_>>().join(" ");
joined
.replace(" -", "-")
.replace("- ", "-")
.replace(':', " · ")
.to_uppercase()
}
pub const AVALON_TRACKS: &[&str] = &[
"education · from age 0 to maturity",
"health resort",
"reservoir cascade · aquatics",
];