/* OpenTally: Open-source election vote counting * Copyright © 2021 Lee Yingtong Li (RunasSudo) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ #![allow(rustdoc::private_intra_doc_links)] #![allow(unused_unsafe)] // Confuses cargo check use crate::constraints::Constraints; use crate::election::{CandidateState, CountState, Election, StageKind}; use crate::numbers::{Fixed, GuardedFixed, NativeFloat64, Number, Rational}; use crate::parser::blt; use crate::stv; use crate::ties; extern crate console_error_panic_hook; use itertools::Itertools; use js_sys::Array; use wasm_bindgen::prelude::wasm_bindgen; use std::cmp::max; // Error handling #[wasm_bindgen] extern "C" { fn wasm_error(message: String); } macro_rules! wasm_error { ($type:expr, $err:expr) => { { unsafe { wasm_error(format!("{}: {}", $type, $err)); } panic!("{}: {}", $type, $err); } } } // Init /// Wrapper for [Fixed::set_dps] #[wasm_bindgen] pub fn fixed_set_dps(dps: usize) { Fixed::set_dps(dps); } /// Wrapper for [GuardedFixed::set_dps] #[wasm_bindgen] pub fn gfixed_set_dps(dps: usize) { GuardedFixed::set_dps(dps); } // Helper macros for making functions macro_rules! impl_type { ($type:ident) => { paste::item! { // Counting /// Wrapper for [blt::parse_iterator] #[wasm_bindgen] #[allow(non_snake_case)] pub fn [](text: String) -> [] { // Install panic! hook console_error_panic_hook::set_once(); let election: Election<$type> = match blt::parse_iterator(text.chars().peekable()) { Ok(e) => e, Err(err) => wasm_error!("Syntax Error", err), }; return [](election); } /// Call [Constraints::from_con] and set [Election::constraints] #[wasm_bindgen] #[allow(non_snake_case)] pub fn [](election: &mut [], text: String) { election.0.constraints = Some(Constraints::from_con(text.lines().map(|s| s.to_string()).into_iter())); } /// Wrapper for [stv::preprocess_election] #[wasm_bindgen] #[allow(non_snake_case)] pub fn [](election: &mut [], opts: &STVOptions) { stv::preprocess_election(&mut election.0, &opts.0); } /// Wrapper for [stv::count_init] #[wasm_bindgen] #[allow(non_snake_case)] pub fn [](state: &mut [], opts: &STVOptions) { match stv::count_init(&mut state.0, opts.as_static()) { Ok(_) => (), Err(err) => wasm_error!("Error", err), } } /// Wrapper for [stv::count_one_stage] #[wasm_bindgen] #[allow(non_snake_case)] pub fn [](state: &mut [], opts: &STVOptions) -> bool { match stv::count_one_stage::<[<$type>]>(&mut state.0, &opts.0) { Ok(v) => v, Err(err) => wasm_error!("Error", err), } } // Reporting /// Wrapper for [init_results_table] #[wasm_bindgen] #[allow(non_snake_case)] pub fn [](election: &[], opts: &STVOptions, report_style: &str) -> String { return init_results_table(&election.0, &opts.0, report_style); } /// Wrapper for [describe_count] #[wasm_bindgen] #[allow(non_snake_case)] pub fn [](filename: String, election: &[], opts: &STVOptions) -> String { return describe_count(filename, &election.0, &opts.0); } /// Wrapper for [update_results_table] #[wasm_bindgen] #[allow(non_snake_case)] pub fn [](stage_num: usize, state: &[], opts: &STVOptions, report_style: &str) -> Array { return update_results_table(stage_num, &state.0, &opts.0, report_style); } /// Wrapper for [update_stage_comments] #[wasm_bindgen] #[allow(non_snake_case)] pub fn [](state: &[], stage_num: usize) -> String { return update_stage_comments(&state.0, stage_num); } /// Wrapper for [finalise_results_table] #[wasm_bindgen] #[allow(non_snake_case)] pub fn [](state: &[], report_style: &str) -> Array { return finalise_results_table(&state.0, report_style); } /// Wrapper for [final_result_summary] #[wasm_bindgen] #[allow(non_snake_case)] pub fn [](state: &[], opts: &STVOptions) -> String { return final_result_summary(&state.0, &opts.0); } // Wrapper structs /// Wrapper for [CountState] /// /// This is required as `&'static` cannot be specified in wasm-bindgen: see [issue 1187](https://github.com/rustwasm/wasm-bindgen/issues/1187). /// #[wasm_bindgen] pub struct [](CountState<'static, $type>); #[wasm_bindgen] impl [] { /// Create a new [CountState] wrapper pub fn new(election: &[]) -> Self { return [](CountState::new(election.as_static())); } /// Call [render_html](crate::stv::transfers::TransferTable::render_html) on [CountState::transfer_table] pub fn transfer_table_render_html(&self, opts: &STVOptions) -> Option { return match &self.0.transfer_table { Some(tt) => Some(tt.render_text(&opts.0)), // TODO None => None, }; } } /// Wrapper for [Election] /// /// This is required as `&'static` cannot be specified in wasm-bindgen: see [issue 1187](https://github.com/rustwasm/wasm-bindgen/issues/1187). /// #[wasm_bindgen] pub struct [](Election<$type>); #[wasm_bindgen] impl [] { /// Return [Election::seats] pub fn seats(&self) -> usize { self.0.seats } /// Return the underlying [Election] as a `&'static Election` /// /// # Safety /// This assumes that the underlying [Election] is valid for the `'static` lifetime, as it would be if the [Election] were created from Javascript. /// fn as_static(&self) -> &'static Election<$type> { unsafe { let ptr = &self.0 as *const Election<$type>; &*ptr } } } }} } impl_type!(Fixed); impl_type!(GuardedFixed); impl_type!(NativeFloat64); impl_type!(Rational); /// Wrapper for [stv::STVOptions] #[wasm_bindgen] pub struct STVOptions(stv::STVOptions); #[wasm_bindgen] impl STVOptions { /// Wrapper for [stv::STVOptions::new] pub fn new( round_surplus_fractions: Option, round_values: Option, round_votes: Option, round_quota: Option, sum_surplus_transfers: &str, meek_surplus_tolerance: String, normalise_ballots: bool, quota: &str, quota_criterion: &str, quota_mode: &str, ties: Array, random_seed: String, surplus: &str, surplus_order: &str, transferable_only: bool, exclusion: &str, meek_nz_exclusion: bool, sample: &str, sample_per_ballot: bool, early_bulk_elect: bool, bulk_exclude: bool, defer_surpluses: bool, immediate_elect: bool, min_threshold: String, constraints_path: Option, constraint_mode: &str, pp_decimals: usize, ) -> Self { Self(stv::STVOptions::new( round_surplus_fractions, round_values, round_votes, round_quota, sum_surplus_transfers.into(), meek_surplus_tolerance, normalise_ballots, quota.into(), quota_criterion.into(), quota_mode.into(), ties::from_strs(ties.iter().map(|v| v.as_string().unwrap()).collect(), Some(random_seed)), surplus.into(), surplus_order.into(), transferable_only, exclusion.into(), meek_nz_exclusion, sample.into(), sample_per_ballot, early_bulk_elect, bulk_exclude, defer_surpluses, immediate_elect, min_threshold, constraints_path, constraint_mode.into(), false, false, false, pp_decimals, )) } /// Wrapper for [stv::STVOptions::validate] pub fn validate(&self) { match self.0.validate() { Ok(_) => {} Err(err) => { wasm_error!("Error", err) } } } } impl STVOptions { /// Return the underlying [stv::STVOptions] as a `&'static stv::STVOptions` /// /// # Safety /// This assumes that the underlying [stv::STVOptions] is valid for the `'static` lifetime, as it would be if the [stv::STVOptions] were created from Javascript. /// fn as_static(&self) -> &'static stv::STVOptions { unsafe { let ptr = &self.0 as *const stv::STVOptions; &*ptr } } } // Reporting /// Generate the lead-in description of the count in HTML fn describe_count(filename: String, election: &Election, opts: &stv::STVOptions) -> String { let mut result = String::from("

Count computed by OpenTally (revision "); result.push_str(crate::VERSION); let total_ballots = election.ballots.iter().fold(N::zero(), |acc, b| { acc + &b.orig_value }); result.push_str(&format!(r#"). Read {:.0} ballots from ‘{}’ for election ‘{}’. There are {} candidates for {} vacancies. "#, total_ballots, filename, election.name, election.candidates.len(), election.seats)); let opts_str = opts.describe::(); if opts_str.len() > 0 { result.push_str(&format!(r#"Counting using options {}.

"#, opts_str)) } else { result.push_str(r#"Counting using default options.

"#); } return result; } /// Generate the first column of the HTML results table fn init_results_table(election: &Election, opts: &stv::STVOptions, report_style: &str) -> String { let mut result = String::from(r#""#); if report_style == "ballots_votes" { result.push_str(&r#""#); } for candidate in election.candidates.iter() { if report_style == "votes_transposed" { result.push_str(&format!(r#"{}"#, candidate.name)); } else { result.push_str(&format!(r#"{}"#, candidate.name)); } } if report_style == "votes_transposed" { result.push_str(r#"Exhausted"#); } else { result.push_str(r#"Exhausted"#); } if report_style == "votes_transposed" { result.push_str(r#"Loss by fractionTotalQuota"#); } else { result.push_str(r#"Loss by fractionTotalQuota"#); } if stv::should_show_vre(opts) { result.push_str(r#"Vote required for election"#); } return result; } /// Generate subsequent columns of the HTML results table fn update_results_table(stage_num: usize, state: &CountState, opts: &stv::STVOptions, report_style: &str) -> Array { let result = Array::new(); // Insert borders to left of new exclusions in Wright STV let classes_o; // Outer version let classes_i; // Inner version if opts.exclusion == stv::ExclusionMethod::Wright && (if let StageKind::ExclusionOf(_) = state.title { true } else { false }) { classes_o = r#" class="blw""#; classes_i = r#"blw "#; } else { classes_o = ""; classes_i = ""; } // Hide transfers column for first preferences if transposed let hide_xfers_trsp; if let StageKind::FirstPreferences = state.title { hide_xfers_trsp = true; } else if opts.exclusion == stv::ExclusionMethod::Wright && (if let StageKind::ExclusionOf(_) = state.title { true } else { false }) { hide_xfers_trsp = true; } else { hide_xfers_trsp = false; } // Header rows let kind_str = state.title.kind_as_string(); let title_str; match &state.title { StageKind::FirstPreferences | StageKind::SurplusesDistributed | StageKind::BulkElection => { title_str = format!("{}", state.title); } StageKind::SurplusOf(candidate) => { title_str = candidate.name.clone(); } StageKind::ExclusionOf(candidates) => { if candidates.len() > 5 { let first_4_cands = candidates.iter().map(|c| &c.name).sorted().take(4).join(",
"); title_str = format!("{},
and {} others", first_4_cands, candidates.len() - 4); } else { title_str = candidates.iter().map(|c| &c.name).join(",
"); } } }; match report_style { "votes" => { result.push(&format!(r##"{1}"##, classes_o, stage_num).into()); result.push(&format!(r#"{}"#, classes_o, kind_str).into()); result.push(&format!(r#"{}"#, classes_o, title_str).into()); } "votes_transposed" => { if hide_xfers_trsp { result.push(&format!(r##"{1}"##, classes_o, stage_num).into()); result.push(&format!(r#"{}"#, classes_o, kind_str).into()); result.push(&format!(r#"{}"#, classes_o, title_str).into()); } else { result.push(&format!(r##"{1}"##, classes_o, stage_num).into()); result.push(&format!(r#"{}"#, classes_o, kind_str).into()); result.push(&format!(r#"{}"#, classes_o, title_str).into()); //result.push(&format!(r#"X'fersTotal"#, tdclasses1).into()); } } "ballots_votes" => { result.push(&format!(r##"{1}"##, classes_o, stage_num).into()); result.push(&format!(r#"{}"#, classes_o, kind_str).into()); result.push(&format!(r#"{}"#, classes_o, title_str).into()); result.push(&format!(r#"BallotsVotes"#, classes_o).into()); } _ => unreachable!("Invalid report_style") } for candidate in state.election.candidates.iter() { let count_card = &state.candidates[candidate]; // TODO: REFACTOR THIS!! match report_style { "votes" => { match count_card.state { CandidateState::Hopeful | CandidateState::Guarded => { result.push(&format!(r#"{}"#, classes_i, pps(&count_card.transfers, opts.pp_decimals)).into()); result.push(&format!(r#"{}"#, classes_i, pp(&count_card.votes, opts.pp_decimals)).into()); } CandidateState::Elected => { result.push(&format!(r#"{}"#, classes_i, pps(&count_card.transfers, opts.pp_decimals)).into()); result.push(&format!(r#"{}"#, classes_i, pp(&count_card.votes, opts.pp_decimals)).into()); } CandidateState::Doomed => { result.push(&format!(r#"{}"#, classes_i, pps(&count_card.transfers, opts.pp_decimals)).into()); result.push(&format!(r#"{}"#, classes_i, pp(&count_card.votes, opts.pp_decimals)).into()); } CandidateState::Withdrawn => { result.push(&format!(r#""#, classes_i).into()); result.push(&format!(r#"WD"#, classes_i).into()); } CandidateState::Excluded => { result.push(&format!(r#"{}"#, classes_i, pps(&count_card.transfers, opts.pp_decimals)).into()); if count_card.votes.is_zero() && count_card.parcels.iter().all(|p| p.votes.is_empty()) { result.push(&format!(r#"Ex"#, classes_i).into()); } else { result.push(&format!(r#"{}"#, classes_i, pp(&count_card.votes, opts.pp_decimals)).into()); } } } } "votes_transposed" => { match count_card.state { CandidateState::Hopeful | CandidateState::Guarded => { if hide_xfers_trsp { result.push(&format!(r#"{}"#, classes_i, pp(&count_card.votes, opts.pp_decimals)).into()); } else { result.push(&format!(r#"{}{}"#, classes_i, pps(&count_card.transfers, opts.pp_decimals), pp(&count_card.votes, opts.pp_decimals)).into()); } } CandidateState::Elected => { if hide_xfers_trsp { result.push(&format!(r#"{}"#, classes_i, pp(&count_card.votes, opts.pp_decimals)).into()); } else { result.push(&format!(r#"{}{}"#, classes_i, pps(&count_card.transfers, opts.pp_decimals), pp(&count_card.votes, opts.pp_decimals)).into()); } } CandidateState::Doomed => { if hide_xfers_trsp { result.push(&format!(r#"{}"#, classes_i, pp(&count_card.votes, opts.pp_decimals)).into()); } else { result.push(&format!(r#"{}{}"#, classes_i, pps(&count_card.transfers, opts.pp_decimals), pp(&count_card.votes, opts.pp_decimals)).into()); } } CandidateState::Withdrawn => { if hide_xfers_trsp { result.push(&format!(r#"WD"#, classes_i).into()); } else { result.push(&format!(r#"WD"#, classes_i).into()); } } CandidateState::Excluded => { if count_card.votes.is_zero() && count_card.parcels.iter().all(|p| p.votes.is_empty()) { if hide_xfers_trsp { result.push(&format!(r#"Ex"#, classes_i).into()); } else { result.push(&format!(r#"{}Ex"#, classes_i, pps(&count_card.transfers, opts.pp_decimals)).into()); } } else { if hide_xfers_trsp { result.push(&format!(r#"{}"#, classes_i, pp(&count_card.votes, opts.pp_decimals)).into()); } else { result.push(&format!(r#"{}{}"#, classes_i, pps(&count_card.transfers, opts.pp_decimals), pp(&count_card.votes, opts.pp_decimals)).into()); } } } } } "ballots_votes" => { match count_card.state { CandidateState::Hopeful | CandidateState::Guarded => { result.push(&format!(r#"{}{}"#, classes_i, pps(&count_card.ballot_transfers, 0), pps(&count_card.transfers, opts.pp_decimals)).into()); result.push(&format!(r#"{}{}"#, classes_i, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)).into()); } CandidateState::Elected => { result.push(&format!(r#"{}{}"#, classes_i, pps(&count_card.ballot_transfers, 0), pps(&count_card.transfers, opts.pp_decimals)).into()); result.push(&format!(r#"{}{}"#, classes_i, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)).into()); } CandidateState::Doomed => { result.push(&format!(r#"{}{}"#, classes_i, pps(&count_card.ballot_transfers, 0), pps(&count_card.transfers, opts.pp_decimals)).into()); result.push(&format!(r#"{}{}"#, classes_i, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)).into()); } CandidateState::Withdrawn => { result.push(&format!(r#""#, classes_i).into()); result.push(&format!(r#"WD"#, classes_i).into()); } CandidateState::Excluded => { result.push(&format!(r#"{}{}"#, classes_i, pps(&count_card.ballot_transfers, 0), pps(&count_card.transfers, opts.pp_decimals)).into()); if count_card.votes.is_zero() && count_card.parcels.iter().all(|p| p.votes.is_empty()) { result.push(&format!(r#"Ex"#, classes_i).into()); } else { result.push(&format!(r#"{}{}"#, classes_i, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)).into()); } } } } _ => unreachable!("Invalid report_style") } } match report_style { "votes" => { result.push(&format!(r#"{}"#, classes_i, pps(&state.exhausted.transfers, opts.pp_decimals)).into()); result.push(&format!(r#"{}"#, classes_i, pp(&state.exhausted.votes, opts.pp_decimals)).into()); result.push(&format!(r#"{}"#, classes_i, pps(&state.loss_fraction.transfers, opts.pp_decimals)).into()); result.push(&format!(r#"{}"#, classes_i, pp(&state.loss_fraction.votes, opts.pp_decimals)).into()); } "votes_transposed" => { if hide_xfers_trsp { result.push(&format!(r#"{}"#, classes_i, pp(&state.exhausted.votes, opts.pp_decimals)).into()); result.push(&format!(r#"{}"#, classes_i, pp(&state.loss_fraction.votes, opts.pp_decimals)).into()); } else { result.push(&format!(r#"{}{}"#, classes_i, pps(&state.exhausted.transfers, opts.pp_decimals), pp(&state.exhausted.votes, opts.pp_decimals)).into()); result.push(&format!(r#"{}{}"#, classes_i, pps(&state.loss_fraction.transfers, opts.pp_decimals), pp(&state.loss_fraction.votes, opts.pp_decimals)).into()); } } "ballots_votes" => { result.push(&format!(r#"{}{}"#, classes_i, pps(&state.exhausted.ballot_transfers, 0), pps(&state.exhausted.transfers, opts.pp_decimals)).into()); result.push(&format!(r#"{}{}"#, classes_i, pp(&state.exhausted.num_ballots(), 0), pp(&state.exhausted.votes, opts.pp_decimals)).into()); result.push(&format!(r#"{}"#, classes_i, pps(&state.loss_fraction.transfers, opts.pp_decimals)).into()); result.push(&format!(r#"{}"#, classes_i, pp(&state.loss_fraction.votes, opts.pp_decimals)).into()); } _ => unreachable!("Invalid report_style") } // Calculate total votes let mut total_vote = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes }); total_vote += &state.exhausted.votes; total_vote += &state.loss_fraction.votes; match report_style { "votes" => { result.push(&format!(r#"{}"#, classes_i, pp(&total_vote, opts.pp_decimals)).into()); } "votes_transposed" => { if hide_xfers_trsp { result.push(&format!(r#"{}"#, classes_i, pp(&total_vote, opts.pp_decimals)).into()); } else { result.push(&format!(r#"{}"#, classes_i, pp(&total_vote, opts.pp_decimals)).into()); } } "ballots_votes" => { // Calculate total ballots let mut total_ballots = state.candidates.values().fold(N::zero(), |acc, cc| { acc + cc.num_ballots() }); total_ballots += state.exhausted.num_ballots(); result.push(&format!(r#"{}{}"#, classes_i, pp(&total_ballots, 0), pp(&total_vote, opts.pp_decimals)).into()); } _ => unreachable!("Invalid report_style") } if report_style == "votes" || (report_style == "votes_transposed" && hide_xfers_trsp) { result.push(&format!(r#"{}"#, classes_i, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)).into()); } else { result.push(&format!(r#"{}"#, classes_i, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)).into()); } if stv::should_show_vre(opts) { if let Some(vre) = &state.vote_required_election { if report_style == "votes" || (report_style == "votes_transposed" && hide_xfers_trsp) { result.push(&format!(r#"{}"#, classes_i, pp(vre, opts.pp_decimals)).into()); } else { result.push(&format!(r#"{}"#, classes_i, pp(vre, opts.pp_decimals)).into()); } } else { if report_style == "votes" || (report_style == "votes_transposed" && hide_xfers_trsp) { result.push(&format!(r#""#, classes_i).into()); } else { result.push(&format!(r#""#, classes_i).into()); } } } return result; } /// Get the comment for the current stage fn update_stage_comments(state: &CountState, stage_num: usize) -> String { let mut comments = state.logger.render().join(" "); if let Some(_) = state.transfer_table { comments.push_str(&format!(r##" [View detailed transfers]"##, stage_num)); } return comments; } /// Generate the final column of the HTML results table fn finalise_results_table(state: &CountState, report_style: &str) -> Array { let result = Array::new(); // Header rows match report_style { "votes" | "votes_transposed" => { result.push(&r#""#.into()); result.push(&"".into()); result.push(&"".into()); } "ballots_votes" => { result.push(&r#""#.into()); result.push(&"".into()); result.push(&"".into()); result.push(&"".into()); } _ => unreachable!("Invalid report_style") } let rowspan = if report_style == "votes_transposed" { "" } else { r#" rowspan="2""# }; // Candidate states for candidate in state.election.candidates.iter() { let count_card = &state.candidates[candidate]; if count_card.state == stv::CandidateState::Elected { result.push(&format!(r#"ELECTED {}"#, rowspan, count_card.order_elected).into()); } else if count_card.state == stv::CandidateState::Excluded { result.push(&format!(r#"Excluded {}"#, rowspan, -count_card.order_elected).into()); } else if count_card.state == stv::CandidateState::Withdrawn { result.push(&format!(r#"Withdrawn"#, rowspan).into()); } else { result.push(&format!(r#""#, rowspan).into()); } if report_style != "votes_transposed" { result.push(&"".into()); } } return result; } /// Generate the final lead-out text summarising the result of the election fn final_result_summary(state: &CountState, opts: &stv::STVOptions) -> String { let mut result = String::from("

Count complete. The winning candidates are, in order of election:

    "); let mut winners = Vec::new(); for (candidate, count_card) in state.candidates.iter() { if count_card.state == CandidateState::Elected { winners.push((candidate, count_card.order_elected, &count_card.keep_value)); } } winners.sort_unstable_by(|a, b| a.1.cmp(&b.1)); for (winner, _, kv_opt) in winners.into_iter() { if let Some(kv) = kv_opt { result.push_str(&format!("
  1. {} (kv = {:.dps2$})
  2. ", winner.name, kv, dps2=max(opts.pp_decimals, 2))); } else { result.push_str(&format!("
  3. {}
  4. ", winner.name)); } } result.push_str("
"); return result; } /// HTML pretty-print the number to the specified decimal places fn pp(n: &N, dps: usize) -> String { if n.is_zero() { return "".to_string(); } let mut raw = format!("{:.dps$}", n, dps=dps); if raw.contains('.') { raw = raw.replacen(".", ".", 1); raw.push_str(""); } if raw.starts_with('-') { raw = raw.replacen("-", "−", 1); } return raw; } /// Signed version of [pp] fn pps(n: &N, dps: usize) -> String { if n.is_zero() { return "".to_string(); } let mut raw = format!("{:.dps$}", n, dps=dps); if raw.contains('.') { raw = raw.replacen(".", ".", 1); raw.push_str(""); } if raw.starts_with('-') { raw = raw.replacen("-", "−", 1); } else { raw.insert(0, '+'); } return raw; }