2021-05-30 18:28:39 +10:00
/* OpenTally: Open-source election vote counting
2023-06-11 21:47:37 +10:00
* Copyright © 2021 – 2023 Lee Yingtong Li ( RunasSudo )
2021-05-30 18:28:39 +10:00
*
* 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 < https ://www.gnu.org/licenses/>.
* /
2021-06-14 20:43:36 +10:00
#![ allow(rustdoc::private_intra_doc_links) ]
2021-07-31 15:24:23 +10:00
#![ allow(unused_unsafe) ] // Confuses cargo check
2021-06-14 20:43:36 +10:00
2022-04-16 02:27:59 +10:00
use crate ::constraints ::{ self , Constraints } ;
2022-08-26 02:27:25 +10:00
use crate ::election ::{ CandidateState , CountState , Election } ;
2021-07-14 16:36:37 +10:00
//use crate::numbers::{DynNum, Fixed, GuardedFixed, NativeFloat64, Number, NumKind, Rational};
2021-06-14 21:43:43 +10:00
use crate ::numbers ::{ Fixed , GuardedFixed , NativeFloat64 , Number , Rational } ;
2021-08-20 02:16:54 +10:00
use crate ::parser ::blt ;
2021-05-30 18:28:39 +10:00
use crate ::stv ;
2021-08-05 01:12:53 +10:00
use crate ::ties ;
2021-05-30 18:28:39 +10:00
extern crate console_error_panic_hook ;
2021-06-02 22:46:36 +10:00
use js_sys ::Array ;
2022-08-26 02:27:25 +10:00
use wasm_bindgen ::{ JsValue , prelude ::wasm_bindgen } ;
2021-05-30 18:28:39 +10:00
2021-06-16 13:00:54 +10:00
use std ::cmp ::max ;
2021-07-31 15:24:23 +10:00
// 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 ) ;
} }
}
2021-06-04 22:05:48 +10:00
// Init
2022-08-22 11:35:20 +10:00
// Wrapper for [DynNum::set_kind]
2021-07-14 16:36:37 +10:00
//#[wasm_bindgen]
//pub fn dynnum_set_kind(kind: NumKind) {
// DynNum::set_kind(kind);
//}
2021-06-14 20:43:36 +10:00
/// Wrapper for [Fixed::set_dps]
2021-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-06-04 22:05:48 +10:00
pub fn fixed_set_dps ( dps : usize ) {
Fixed ::set_dps ( dps ) ;
}
2021-06-14 21:43:43 +10:00
/// Wrapper for [GuardedFixed::set_dps]
2021-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-06-14 21:43:43 +10:00
pub fn gfixed_set_dps ( dps : usize ) {
GuardedFixed ::set_dps ( dps ) ;
}
2021-05-30 23:00:28 +10:00
// Helper macros for making functions
macro_rules ! impl_type {
( $type :ident ) = > { paste ::item! {
2021-06-04 22:05:48 +10:00
// Counting
2021-05-30 23:00:28 +10:00
2021-08-20 02:16:54 +10:00
/// Wrapper for [blt::parse_iterator]
2021-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-05-30 23:00:28 +10:00
#[ allow(non_snake_case) ]
pub fn [ < election_from_blt_ $type > ] ( text : String ) -> [ < Election $type > ] {
// Install panic! hook
console_error_panic_hook ::set_once ( ) ;
2021-08-20 02:16:54 +10:00
let election : Election < $type > = match blt ::parse_iterator ( text . chars ( ) . peekable ( ) ) {
2021-07-31 15:24:23 +10:00
Ok ( e ) = > e ,
Err ( err ) = > wasm_error! ( " Syntax Error " , err ) ,
} ;
2021-05-30 23:00:28 +10:00
return [ < Election $type > ] ( election ) ;
}
2021-09-04 22:46:29 +10:00
/// Call [Constraints::from_con] and set [Election::constraints]
2021-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-09-03 23:53:15 +10:00
#[ allow(non_snake_case) ]
2022-04-16 02:27:59 +10:00
pub fn [ < election_load_constraints_ $type > ] ( election : & mut [ < Election $type > ] , text : String , opts : & STVOptions ) {
2021-10-29 23:07:38 +11:00
election . 0. constraints = match Constraints ::from_con ( text . lines ( ) ) {
Ok ( c ) = > Some ( c ) ,
Err ( err ) = > wasm_error! ( " Constraint Syntax Error " , err ) ,
} ;
// Validate constraints
2022-04-16 02:27:59 +10:00
if let Err ( err ) = election . 0. constraints . as_ref ( ) . unwrap ( ) . validate_constraints ( election . 0. candidates . len ( ) , opts . 0. constraint_mode ) {
2021-10-29 23:07:38 +11:00
wasm_error! ( " Constraint Validation Error " , err ) ;
}
2022-04-16 02:27:59 +10:00
// Add dummy candidates if required
if opts . 0. constraint_mode = = stv ::ConstraintMode ::RepeatCount {
constraints ::init_repeat_count ( & mut election . 0 ) ;
}
2021-09-03 23:53:15 +10:00
}
2021-09-04 22:46:29 +10:00
/// Wrapper for [stv::preprocess_election]
2021-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-06-27 22:09:34 +10:00
#[ allow(non_snake_case) ]
2021-09-04 22:46:29 +10:00
pub fn [ < preprocess_election_ $type > ] ( election : & mut [ < Election $type > ] , opts : & STVOptions ) {
stv ::preprocess_election ( & mut election . 0 , & opts . 0 ) ;
2021-06-27 22:09:34 +10:00
}
2021-06-14 20:43:36 +10:00
/// Wrapper for [stv::count_init]
2021-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-05-30 23:00:28 +10:00
#[ allow(non_snake_case) ]
2021-08-05 20:18:10 +10:00
pub fn [ < count_init_ $type > ] ( state : & mut [ < CountState $type > ] , opts : & STVOptions ) {
2021-06-28 00:56:28 +10:00
match stv ::count_init ( & mut state . 0 , opts . as_static ( ) ) {
2021-08-05 20:18:10 +10:00
Ok ( _ ) = > ( ) ,
2021-07-31 15:24:23 +10:00
Err ( err ) = > wasm_error! ( " Error " , err ) ,
2021-06-28 00:56:28 +10:00
}
2021-05-30 23:00:28 +10:00
}
2021-06-14 20:43:36 +10:00
/// Wrapper for [stv::count_one_stage]
2021-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-05-30 23:00:28 +10:00
#[ allow(non_snake_case) ]
2021-07-31 15:24:23 +10:00
pub fn [ < count_one_stage_ $type > ] ( state : & mut [ < CountState $type > ] , opts : & STVOptions ) -> bool {
2021-06-27 17:44:30 +10:00
match stv ::count_one_stage ::< [ < $type > ] > ( & mut state . 0 , & opts . 0 ) {
2021-07-31 15:24:23 +10:00
Ok ( v ) = > v ,
Err ( err ) = > wasm_error! ( " Error " , err ) ,
2021-06-12 02:09:26 +10:00
}
2021-05-30 23:00:28 +10:00
}
// Reporting
2021-06-14 20:43:36 +10:00
/// Wrapper for [init_results_table]
2021-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-05-30 23:00:28 +10:00
#[ allow(non_snake_case) ]
2021-08-16 18:48:49 +10:00
pub fn [ < init_results_table_ $type > ] ( election : & [ < Election $type > ] , opts : & STVOptions , report_style : & str ) -> String {
return init_results_table ( & election . 0 , & opts . 0 , report_style ) ;
2021-06-02 22:46:36 +10:00
}
2021-06-14 20:43:36 +10:00
/// Wrapper for [describe_count]
2021-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-06-03 21:35:25 +10:00
#[ allow(non_snake_case) ]
2021-06-13 00:39:49 +10:00
pub fn [ < describe_count_ $type > ] ( filename : String , election : & [ < Election $type > ] , opts : & STVOptions ) -> String {
2022-08-26 02:27:25 +10:00
return stv ::html ::describe_count ( & filename , & election . 0 , & opts . 0 ) ;
2021-06-03 21:35:25 +10:00
}
2021-06-14 20:43:36 +10:00
/// Wrapper for [update_results_table]
2021-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-06-02 22:46:36 +10:00
#[ allow(non_snake_case) ]
2021-08-16 18:48:49 +10:00
pub fn [ < update_results_table_ $type > ] ( stage_num : usize , state : & [ < CountState $type > ] , opts : & STVOptions , report_style : & str ) -> Array {
return update_results_table ( stage_num , & state . 0 , & opts . 0 , report_style ) ;
2021-06-02 22:46:36 +10:00
}
2021-06-14 20:43:36 +10:00
/// Wrapper for [update_stage_comments]
2021-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-06-03 15:47:19 +10:00
#[ allow(non_snake_case) ]
2021-09-11 21:08:36 +10:00
pub fn [ < update_stage_comments_ $type > ] ( state : & [ < CountState $type > ] , stage_num : usize ) -> String {
return update_stage_comments ( & state . 0 , stage_num ) ;
2021-06-03 15:47:19 +10:00
}
2021-06-14 20:43:36 +10:00
/// Wrapper for [finalise_results_table]
2021-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-06-03 15:47:19 +10:00
#[ allow(non_snake_case) ]
2021-08-16 18:48:49 +10:00
pub fn [ < finalise_results_table_ $type > ] ( state : & [ < CountState $type > ] , report_style : & str ) -> Array {
return finalise_results_table ( & state . 0 , report_style ) ;
2021-06-03 15:47:19 +10:00
}
2021-06-14 20:43:36 +10:00
/// Wrapper for [final_result_summary]
2021-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-06-02 22:46:36 +10:00
#[ allow(non_snake_case) ]
2021-06-16 13:00:54 +10:00
pub fn [ < final_result_summary_ $type > ] ( state : & [ < CountState $type > ] , opts : & STVOptions ) -> String {
return final_result_summary ( & state . 0 , & opts . 0 ) ;
2021-06-03 15:47:19 +10:00
}
2021-05-30 23:00:28 +10:00
// Wrapper structs
2021-06-14 20:43:36 +10:00
/// Wrapper for [CountState]
2021-06-16 17:20:29 +10:00
///
/// This is required as `&'static` cannot be specified in wasm-bindgen: see [issue 1187](https://github.com/rustwasm/wasm-bindgen/issues/1187).
///
2021-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-05-30 23:00:28 +10:00
pub struct [ < CountState $type > ] ( CountState < 'static , $type > ) ;
2021-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-05-30 23:00:28 +10:00
impl [ < CountState $type > ] {
2021-06-16 17:20:29 +10:00
/// Create a new [CountState] wrapper
2021-05-30 23:00:28 +10:00
pub fn new ( election : & [ < Election $type > ] ) -> Self {
return [ < CountState $type > ] ( CountState ::new ( election . as_static ( ) ) ) ;
}
2021-09-11 21:08:36 +10:00
2022-08-22 11:35:20 +10:00
/// Call [render_text](crate::stv::gregory::TransferTable::render_text) (as HTML) on [CountState::transfer_table]
2021-09-11 21:08:36 +10:00
pub fn transfer_table_render_html ( & self , opts : & STVOptions ) -> Option < String > {
2021-10-27 19:52:51 +11:00
return self . 0. transfer_table . as_ref ( ) . map ( | tt | tt . render_text ( & opts . 0 ) ) ;
2021-09-11 21:08:36 +10:00
}
2021-05-30 23:00:28 +10:00
}
2021-06-14 20:43:36 +10:00
/// Wrapper for [Election]
2021-06-16 17:20:29 +10:00
///
/// This is required as `&'static` cannot be specified in wasm-bindgen: see [issue 1187](https://github.com/rustwasm/wasm-bindgen/issues/1187).
///
2021-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-05-30 23:00:28 +10:00
pub struct [ < Election $type > ] ( Election < $type > ) ;
2021-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-05-30 23:00:28 +10:00
impl [ < Election $type > ] {
2021-06-16 17:20:29 +10:00
/// Return [Election::seats]
2021-05-30 23:00:28 +10:00
pub fn seats ( & self ) -> usize { self . 0. seats }
2021-06-16 17:20:29 +10:00
/// 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.
///
2021-05-30 23:00:28 +10:00
fn as_static ( & self ) -> & 'static Election < $type > {
unsafe {
let ptr = & self . 0 as * const Election < $type > ;
& * ptr
}
}
}
} }
2021-05-30 18:28:39 +10:00
}
2021-07-14 16:36:37 +10:00
//impl_type!(DynNum);
2021-06-04 22:05:48 +10:00
impl_type! ( Fixed ) ;
2021-06-14 21:43:43 +10:00
impl_type! ( GuardedFixed ) ;
2021-05-30 23:00:28 +10:00
impl_type! ( NativeFloat64 ) ;
2021-06-04 22:05:48 +10:00
impl_type! ( Rational ) ;
2021-05-30 18:28:39 +10:00
2021-06-14 20:43:36 +10:00
/// Wrapper for [stv::STVOptions]
2021-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-06-13 03:15:15 +10:00
pub struct STVOptions ( stv ::STVOptions ) ;
2021-06-13 00:39:49 +10:00
2021-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-06-13 00:39:49 +10:00
impl STVOptions {
2021-06-14 20:43:36 +10:00
/// Wrapper for [stv::STVOptions::new]
2021-06-13 00:39:49 +10:00
pub fn new (
2021-08-03 16:46:21 +10:00
round_surplus_fractions : Option < usize > ,
round_values : Option < usize > ,
2021-06-13 00:39:49 +10:00
round_votes : Option < usize > ,
round_quota : Option < usize > ,
2022-03-23 00:34:43 +11:00
round_subtransfers : & str ,
2021-08-05 01:12:53 +10:00
meek_surplus_tolerance : String ,
2021-06-13 00:39:49 +10:00
quota : & str ,
quota_criterion : & str ,
quota_mode : & str ,
ties : Array ,
2021-06-13 03:15:15 +10:00
random_seed : String ,
2021-06-13 00:39:49 +10:00
surplus : & str ,
surplus_order : & str ,
2022-03-25 02:46:30 +11:00
papers : & str ,
2021-06-13 00:39:49 +10:00
exclusion : & str ,
2021-06-20 01:28:54 +10:00
meek_nz_exclusion : bool ,
2021-08-05 18:41:39 +10:00
sample : & str ,
2021-08-04 13:46:32 +10:00
sample_per_ballot : bool ,
2021-06-23 00:52:25 +10:00
early_bulk_elect : bool ,
2021-06-13 00:39:49 +10:00
bulk_exclude : bool ,
defer_surpluses : bool ,
2021-08-07 18:51:48 +10:00
immediate_elect : bool ,
2021-08-03 23:22:52 +10:00
min_threshold : String ,
2021-06-27 22:24:25 +10:00
constraints_path : Option < String > ,
2021-06-27 21:57:24 +10:00
constraint_mode : & str ,
2021-06-13 00:39:49 +10:00
pp_decimals : usize ,
) -> Self {
Self ( stv ::STVOptions ::new (
2021-08-03 16:46:21 +10:00
round_surplus_fractions ,
round_values ,
2021-06-13 00:39:49 +10:00
round_votes ,
round_quota ,
2022-03-23 00:34:43 +11:00
round_subtransfers . into ( ) ,
2021-06-27 21:57:24 +10:00
meek_surplus_tolerance ,
2021-08-05 01:12:53 +10:00
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 ( ) ,
2022-03-25 02:46:30 +11:00
if papers = = " transferable " | | papers = = " subtract_nontransferable " { true } else { false } ,
2023-06-11 21:47:37 +10:00
if papers = = " assume_progress_total " | | papers = = " subtract_nontransferable " { true } else { false } ,
2021-08-05 01:12:53 +10:00
exclusion . into ( ) ,
2021-06-20 01:28:54 +10:00
meek_nz_exclusion ,
2021-08-05 18:41:39 +10:00
sample . into ( ) ,
2021-08-04 13:46:32 +10:00
sample_per_ballot ,
2021-06-23 00:52:25 +10:00
early_bulk_elect ,
2021-06-13 00:39:49 +10:00
bulk_exclude ,
defer_surpluses ,
2021-08-07 18:51:48 +10:00
immediate_elect ,
2021-08-03 23:22:52 +10:00
min_threshold ,
2021-08-05 01:12:53 +10:00
constraints_path ,
constraint_mode . into ( ) ,
2021-07-31 17:41:28 +10:00
false ,
false ,
2021-09-11 18:42:15 +10:00
false ,
2021-06-13 00:39:49 +10:00
pp_decimals ,
) )
}
2021-06-22 14:34:26 +10:00
/// Wrapper for [stv::STVOptions::validate]
pub fn validate ( & self ) {
2021-07-31 15:24:23 +10:00
match self . 0. validate ( ) {
Ok ( _ ) = > { }
Err ( err ) = > { wasm_error! ( " Error " , err ) }
}
2021-06-22 14:34:26 +10:00
}
2021-06-13 00:39:49 +10:00
}
2021-06-13 00:15:14 +10:00
2021-06-13 03:15:15 +10:00
impl STVOptions {
2021-06-14 20:43:36 +10:00
/// Return the underlying [stv::STVOptions] as a `&'static stv::STVOptions`
///
/// # Safety
2021-06-16 17:20:29 +10:00
/// 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.
2021-06-14 20:43:36 +10:00
///
2021-06-13 03:15:15 +10:00
fn as_static ( & self ) -> & 'static stv ::STVOptions {
unsafe {
let ptr = & self . 0 as * const stv ::STVOptions ;
& * ptr
}
}
}
2021-05-30 18:28:39 +10:00
// Reporting
2021-06-14 20:43:36 +10:00
/// Generate the first column of the HTML results table
2021-09-26 02:27:37 +10:00
pub fn init_results_table < N : Number > ( election : & Election < N > , opts : & stv ::STVOptions , report_style : & str ) -> String {
2022-08-26 02:27:25 +10:00
return stv ::html ::init_results_table ( election , opts , report_style ) . join ( " " ) ;
2021-06-14 20:43:36 +10:00
}
/// Generate subsequent columns of the HTML results table
2021-09-26 02:27:37 +10:00
pub fn update_results_table < N : Number > ( stage_num : usize , state : & CountState < N > , opts : & stv ::STVOptions , report_style : & str ) -> Array {
2022-08-26 02:27:25 +10:00
return stv ::html ::update_results_table ( stage_num , state , opts , report_style )
. into_iter ( )
. map ( | s | JsValue ::from ( s ) )
. collect ( ) ;
2021-06-02 22:46:36 +10:00
}
2021-06-14 20:43:36 +10:00
/// Get the comment for the current stage
2021-09-26 02:27:37 +10:00
pub fn update_stage_comments < N : Number > ( state : & CountState < N > , stage_num : usize ) -> String {
2021-09-11 21:08:36 +10:00
let mut comments = state . logger . render ( ) . join ( " " ) ;
2021-10-27 19:52:51 +11:00
if state . transfer_table . is_some ( ) {
2021-09-11 21:08:36 +10:00
comments . push_str ( & format! ( r ## " <a href="#" class="detailedTransfersLink" onclick="viewDetailedTransfers({});return false;">[View detailed transfers]</a>"## , stage_num ) ) ;
}
return comments ;
2021-06-03 15:47:19 +10:00
}
2021-06-14 20:43:36 +10:00
/// Generate the final column of the HTML results table
2021-09-26 02:27:37 +10:00
pub fn finalise_results_table < N : Number > ( state : & CountState < N > , report_style : & str ) -> Array {
2022-08-26 02:27:25 +10:00
return stv ::html ::finalise_results_table ( state , report_style )
. into_iter ( )
. map ( | s | JsValue ::from ( s ) )
. collect ( ) ;
2021-05-30 18:28:39 +10:00
}
2021-06-14 20:43:36 +10:00
/// Generate the final lead-out text summarising the result of the election
2021-09-26 02:27:37 +10:00
pub fn final_result_summary < N : Number > ( state : & CountState < N > , opts : & stv ::STVOptions ) -> String {
2021-06-03 15:47:19 +10:00
let mut result = String ::from ( " <p>Count complete. The winning candidates are, in order of election:</p><ol> " ) ;
2021-05-30 18:28:39 +10:00
2021-06-03 15:47:19 +10:00
let mut winners = Vec ::new ( ) ;
for ( candidate , count_card ) in state . candidates . iter ( ) {
2021-06-12 00:50:01 +10:00
if count_card . state = = CandidateState ::Elected {
2021-06-16 13:00:54 +10:00
winners . push ( ( candidate , count_card . order_elected , & count_card . keep_value ) ) ;
2021-06-03 15:47:19 +10:00
}
}
2021-06-29 15:31:38 +10:00
winners . sort_unstable_by ( | a , b | a . 1. cmp ( & b . 1 ) ) ;
2021-05-30 18:28:39 +10:00
2021-06-16 13:00:54 +10:00
for ( winner , _ , kv_opt ) in winners . into_iter ( ) {
if let Some ( kv ) = kv_opt {
result . push_str ( & format! ( " <li> {} (<i>kv</i> = {:.dps2$} )</li> " , winner . name , kv , dps2 = max ( opts . pp_decimals , 2 ) ) ) ;
} else {
result . push_str ( & format! ( " <li> {} </li> " , winner . name ) ) ;
}
2021-06-03 15:47:19 +10:00
}
2021-05-30 18:28:39 +10:00
2021-06-03 15:47:19 +10:00
result . push_str ( " </ol> " ) ;
return result ;
}