1use crate::exception::{ExceptionKind, SolverException};
13use crate::throw;
14use crate::types::{Index, Number};
15use std::cell::RefCell;
16use std::collections::BTreeMap;
17use std::rc::Rc;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21#[allow(non_camel_case_types)]
22pub enum OptionType {
23 OT_Number,
24 OT_Integer,
25 OT_String,
26 OT_Unknown,
27}
28
29#[derive(Debug, Clone)]
31pub struct StringEntry {
32 pub value: String,
33 pub description: String,
34}
35
36#[derive(Debug, Clone)]
37pub enum DefaultValue {
38 None,
39 Number(Number),
40 Integer(Index),
41 String(String),
42}
43
44#[derive(Debug, Clone)]
46pub struct RegisteredOption {
47 pub name: String,
48 pub short_description: String,
49 pub long_description: String,
50 pub category: String,
51 pub counter: Index,
52 pub advanced: bool,
53 pub option_type: OptionType,
54 pub default: DefaultValue,
55 pub has_lower: bool,
56 pub lower: Number,
57 pub lower_strict: bool,
58 pub has_upper: bool,
59 pub upper: Number,
60 pub upper_strict: bool,
61 pub valid_strings: Vec<StringEntry>,
62}
63
64impl RegisteredOption {
65 fn new(
66 name: String,
67 short: String,
68 long: String,
69 category: String,
70 counter: Index,
71 advanced: bool,
72 ) -> Self {
73 Self {
74 name,
75 short_description: short,
76 long_description: long,
77 category,
78 counter,
79 advanced,
80 option_type: OptionType::OT_Unknown,
81 default: DefaultValue::None,
82 has_lower: false,
83 lower: 0.0,
84 lower_strict: false,
85 has_upper: false,
86 upper: 0.0,
87 upper_strict: false,
88 valid_strings: Vec::new(),
89 }
90 }
91
92 pub fn is_valid_number(&self, v: Number) -> bool {
94 if self.has_lower {
95 let ok = if self.lower_strict {
96 v > self.lower
97 } else {
98 v >= self.lower
99 };
100 if !ok {
101 return false;
102 }
103 }
104 if self.has_upper {
105 let ok = if self.upper_strict {
106 v < self.upper
107 } else {
108 v <= self.upper
109 };
110 if !ok {
111 return false;
112 }
113 }
114 true
115 }
116
117 pub fn is_valid_integer(&self, v: Index) -> bool {
118 self.is_valid_number(v as Number)
119 }
120
121 pub fn is_valid_string(&self, value: &str) -> bool {
126 let v = value.to_ascii_lowercase();
127 self.valid_strings
128 .iter()
129 .any(|e| e.value == "*" || e.value.eq_ignore_ascii_case(&v))
130 }
131
132 pub fn canonical_string(&self, value: &str) -> Option<&str> {
135 self.valid_strings
136 .iter()
137 .find(|e| e.value.eq_ignore_ascii_case(value))
138 .map(|e| e.value.as_str())
139 }
140
141 pub fn map_string_to_enum(&self, value: &str) -> Option<Index> {
143 self.valid_strings
144 .iter()
145 .position(|e| e.value.eq_ignore_ascii_case(value))
146 .map(|i| i as Index)
147 }
148}
149
150#[derive(Debug, Default)]
152pub struct RegisteredOptions {
153 options: RefCell<BTreeMap<String, Rc<RegisteredOption>>>,
155 order: RefCell<Vec<String>>,
157 current_category: RefCell<String>,
159 next_counter: RefCell<Index>,
160}
161
162impl RegisteredOptions {
163 pub fn new() -> Rc<Self> {
164 Rc::new(Self::default())
165 }
166
167 pub fn set_registering_category(&self, category: impl Into<String>) {
168 *self.current_category.borrow_mut() = category.into();
169 }
170
171 fn alloc_counter(&self) -> Index {
172 let mut c = self.next_counter.borrow_mut();
173 let v = *c;
174 *c += 1;
175 v
176 }
177
178 fn register(&self, opt: RegisteredOption) -> Result<Rc<RegisteredOption>, SolverException> {
179 let key = opt.name.to_ascii_lowercase();
180 let mut opts = self.options.borrow_mut();
181 if opts.contains_key(&key) {
182 throw!(
183 ExceptionKind::OPTION_ALREADY_REGISTERED,
184 format!("Option {} already registered.", opt.name)
185 );
186 }
187 let rc = Rc::new(opt);
188 opts.insert(key.clone(), rc.clone());
189 self.order.borrow_mut().push(key);
190 Ok(rc)
191 }
192
193 pub fn add_number_option(
194 &self,
195 name: &str,
196 short_description: &str,
197 default_value: Number,
198 long_description: &str,
199 ) -> Result<Rc<RegisteredOption>, SolverException> {
200 let mut o = RegisteredOption::new(
201 name.to_string(),
202 short_description.to_string(),
203 long_description.to_string(),
204 self.current_category.borrow().clone(),
205 self.alloc_counter(),
206 false,
207 );
208 o.option_type = OptionType::OT_Number;
209 o.default = DefaultValue::Number(default_value);
210 self.register(o)
211 }
212
213 pub fn add_lower_bounded_number_option(
214 &self,
215 name: &str,
216 short_description: &str,
217 lower: Number,
218 strict: bool,
219 default_value: Number,
220 long_description: &str,
221 ) -> Result<Rc<RegisteredOption>, SolverException> {
222 let mut o = RegisteredOption::new(
223 name.to_string(),
224 short_description.to_string(),
225 long_description.to_string(),
226 self.current_category.borrow().clone(),
227 self.alloc_counter(),
228 false,
229 );
230 o.option_type = OptionType::OT_Number;
231 o.default = DefaultValue::Number(default_value);
232 o.has_lower = true;
233 o.lower = lower;
234 o.lower_strict = strict;
235 self.register(o)
236 }
237
238 #[allow(clippy::too_many_arguments)]
239 pub fn add_bounded_number_option(
240 &self,
241 name: &str,
242 short_description: &str,
243 lower: Number,
244 lower_strict: bool,
245 upper: Number,
246 upper_strict: bool,
247 default_value: Number,
248 long_description: &str,
249 ) -> Result<Rc<RegisteredOption>, SolverException> {
250 let mut o = RegisteredOption::new(
251 name.to_string(),
252 short_description.to_string(),
253 long_description.to_string(),
254 self.current_category.borrow().clone(),
255 self.alloc_counter(),
256 false,
257 );
258 o.option_type = OptionType::OT_Number;
259 o.default = DefaultValue::Number(default_value);
260 o.has_lower = true;
261 o.lower = lower;
262 o.lower_strict = lower_strict;
263 o.has_upper = true;
264 o.upper = upper;
265 o.upper_strict = upper_strict;
266 self.register(o)
267 }
268
269 pub fn add_integer_option(
270 &self,
271 name: &str,
272 short_description: &str,
273 default_value: Index,
274 long_description: &str,
275 ) -> Result<Rc<RegisteredOption>, SolverException> {
276 let mut o = RegisteredOption::new(
277 name.to_string(),
278 short_description.to_string(),
279 long_description.to_string(),
280 self.current_category.borrow().clone(),
281 self.alloc_counter(),
282 false,
283 );
284 o.option_type = OptionType::OT_Integer;
285 o.default = DefaultValue::Integer(default_value);
286 self.register(o)
287 }
288
289 pub fn add_lower_bounded_integer_option(
290 &self,
291 name: &str,
292 short_description: &str,
293 lower: Index,
294 default_value: Index,
295 long_description: &str,
296 ) -> Result<Rc<RegisteredOption>, SolverException> {
297 let mut o = RegisteredOption::new(
298 name.to_string(),
299 short_description.to_string(),
300 long_description.to_string(),
301 self.current_category.borrow().clone(),
302 self.alloc_counter(),
303 false,
304 );
305 o.option_type = OptionType::OT_Integer;
306 o.default = DefaultValue::Integer(default_value);
307 o.has_lower = true;
308 o.lower = lower as Number;
309 self.register(o)
310 }
311
312 pub fn add_bounded_integer_option(
313 &self,
314 name: &str,
315 short_description: &str,
316 lower: Index,
317 upper: Index,
318 default_value: Index,
319 long_description: &str,
320 ) -> Result<Rc<RegisteredOption>, SolverException> {
321 let mut o = RegisteredOption::new(
322 name.to_string(),
323 short_description.to_string(),
324 long_description.to_string(),
325 self.current_category.borrow().clone(),
326 self.alloc_counter(),
327 false,
328 );
329 o.option_type = OptionType::OT_Integer;
330 o.default = DefaultValue::Integer(default_value);
331 o.has_lower = true;
332 o.lower = lower as Number;
333 o.has_upper = true;
334 o.upper = upper as Number;
335 self.register(o)
336 }
337
338 pub fn add_string_option(
339 &self,
340 name: &str,
341 short_description: &str,
342 default_value: &str,
343 valid: &[(&str, &str)],
344 long_description: &str,
345 ) -> Result<Rc<RegisteredOption>, SolverException> {
346 let mut o = RegisteredOption::new(
347 name.to_string(),
348 short_description.to_string(),
349 long_description.to_string(),
350 self.current_category.borrow().clone(),
351 self.alloc_counter(),
352 false,
353 );
354 o.option_type = OptionType::OT_String;
355 o.default = DefaultValue::String(default_value.to_string());
356 o.valid_strings = valid
357 .iter()
358 .map(|(v, d)| StringEntry {
359 value: v.to_string(),
360 description: d.to_string(),
361 })
362 .collect();
363 self.register(o)
364 }
365
366 pub fn add_bool_option(
368 &self,
369 name: &str,
370 short_description: &str,
371 default_yes: bool,
372 long_description: &str,
373 ) -> Result<Rc<RegisteredOption>, SolverException> {
374 self.add_string_option(
375 name,
376 short_description,
377 if default_yes { "yes" } else { "no" },
378 &[("no", ""), ("yes", "")],
379 long_description,
380 )
381 }
382
383 pub fn get_option(&self, name: &str) -> Option<Rc<RegisteredOption>> {
387 let tag_only = match name.rfind('.') {
388 Some(pos) => &name[pos + 1..],
389 None => name,
390 };
391 self.options
392 .borrow()
393 .get(&tag_only.to_ascii_lowercase())
394 .cloned()
395 }
396
397 pub fn registered_options_in_order(&self) -> Vec<Rc<RegisteredOption>> {
399 let opts = self.options.borrow();
400 self.order
401 .borrow()
402 .iter()
403 .filter_map(|k| opts.get(k).cloned())
404 .collect()
405 }
406}
407
408#[cfg(test)]
409mod tests {
410 use super::*;
411
412 #[test]
413 fn register_and_lookup_case_insensitive() {
414 let r = RegisteredOptions::new();
415 r.set_registering_category("Test");
416 r.add_number_option("Tol", "tolerance", 1e-8, "").unwrap();
417 assert!(r.get_option("tol").is_some());
418 assert!(r.get_option("TOL").is_some());
419 }
420
421 #[test]
422 fn duplicate_registration_is_error() {
423 let r = RegisteredOptions::new();
424 r.add_number_option("alpha", "", 1.0, "").unwrap();
425 let err = r.add_number_option("ALPHA", "", 2.0, "").unwrap_err();
426 assert_eq!(err.kind, ExceptionKind::OPTION_ALREADY_REGISTERED);
427 }
428
429 #[test]
430 fn bounds_check_on_number() {
431 let r = RegisteredOptions::new();
432 r.add_lower_bounded_number_option("mu", "", 0.0, true, 0.1, "")
433 .unwrap();
434 let opt = r.get_option("mu").unwrap();
435 assert!(opt.is_valid_number(1e-12));
436 assert!(!opt.is_valid_number(0.0));
437 assert!(!opt.is_valid_number(-1.0));
438 }
439
440 #[test]
441 fn string_enum_lookup() {
442 let r = RegisteredOptions::new();
443 r.add_string_option(
444 "linear_solver",
445 "",
446 "mumps",
447 &[("mumps", "MUMPS"), ("feral", "FERAL")],
448 "",
449 )
450 .unwrap();
451 let opt = r.get_option("linear_solver").unwrap();
452 assert!(opt.is_valid_string("MuMpS"));
453 assert!(!opt.is_valid_string("ma27"));
454 assert_eq!(opt.map_string_to_enum("feral"), Some(1));
455 }
456
457 #[test]
458 fn registration_order_preserved() {
459 let r = RegisteredOptions::new();
460 r.add_number_option("c", "", 0.0, "").unwrap();
461 r.add_number_option("a", "", 0.0, "").unwrap();
462 r.add_number_option("b", "", 0.0, "").unwrap();
463 let order: Vec<_> = r
464 .registered_options_in_order()
465 .iter()
466 .map(|o| o.name.clone())
467 .collect();
468 assert_eq!(order, vec!["c", "a", "b"]);
469 }
470}