Skip to main content

sheetkit_core/
doc_props.rs

1//! User-facing document properties types and conversion helpers.
2//!
3//! These types provide an ergonomic API for working with the three kinds of
4//! OOXML document properties (core, extended/application, and custom).
5
6use sheetkit_xml::doc_props::{
7    CoreProperties, CustomProperty, CustomPropertyValue as XmlCustomPropertyValue,
8    ExtendedProperties, CUSTOM_PROPERTY_FMTID,
9};
10use sheetkit_xml::namespaces;
11use sheetkit_xml::workbook::{CalcPr, WorkbookPr};
12
13/// User-facing document core properties.
14#[derive(Debug, Clone, Default)]
15pub struct DocProperties {
16    pub title: Option<String>,
17    pub subject: Option<String>,
18    pub creator: Option<String>,
19    pub keywords: Option<String>,
20    pub description: Option<String>,
21    pub last_modified_by: Option<String>,
22    pub revision: Option<String>,
23    pub created: Option<String>,
24    pub modified: Option<String>,
25    pub category: Option<String>,
26    pub content_status: Option<String>,
27}
28
29impl From<&CoreProperties> for DocProperties {
30    fn from(props: &CoreProperties) -> Self {
31        Self {
32            title: props.title.clone(),
33            subject: props.subject.clone(),
34            creator: props.creator.clone(),
35            keywords: props.keywords.clone(),
36            description: props.description.clone(),
37            last_modified_by: props.last_modified_by.clone(),
38            revision: props.revision.clone(),
39            created: props.created.clone(),
40            modified: props.modified.clone(),
41            category: props.category.clone(),
42            content_status: props.content_status.clone(),
43        }
44    }
45}
46
47impl DocProperties {
48    /// Convert to the XML-level `CoreProperties` struct.
49    pub fn to_core_properties(&self) -> CoreProperties {
50        CoreProperties {
51            title: self.title.clone(),
52            subject: self.subject.clone(),
53            creator: self.creator.clone(),
54            keywords: self.keywords.clone(),
55            description: self.description.clone(),
56            last_modified_by: self.last_modified_by.clone(),
57            revision: self.revision.clone(),
58            created: self.created.clone(),
59            modified: self.modified.clone(),
60            category: self.category.clone(),
61            content_status: self.content_status.clone(),
62        }
63    }
64}
65
66/// User-facing application properties.
67#[derive(Debug, Clone, Default)]
68pub struct AppProperties {
69    pub application: Option<String>,
70    pub doc_security: Option<u32>,
71    pub company: Option<String>,
72    pub app_version: Option<String>,
73    pub manager: Option<String>,
74    pub template: Option<String>,
75}
76
77impl From<&ExtendedProperties> for AppProperties {
78    fn from(props: &ExtendedProperties) -> Self {
79        Self {
80            application: props.application.clone(),
81            doc_security: props.doc_security,
82            company: props.company.clone(),
83            app_version: props.app_version.clone(),
84            manager: props.manager.clone(),
85            template: props.template.clone(),
86        }
87    }
88}
89
90impl AppProperties {
91    /// Convert to the XML-level `ExtendedProperties` struct.
92    pub fn to_extended_properties(&self) -> ExtendedProperties {
93        ExtendedProperties {
94            xmlns: namespaces::EXTENDED_PROPERTIES.to_string(),
95            xmlns_vt: Some(namespaces::VT.to_string()),
96            application: self.application.clone(),
97            doc_security: self.doc_security,
98            scale_crop: None,
99            company: self.company.clone(),
100            links_up_to_date: None,
101            shared_doc: None,
102            hyperlinks_changed: None,
103            app_version: self.app_version.clone(),
104            template: self.template.clone(),
105            manager: self.manager.clone(),
106        }
107    }
108}
109
110/// Value type for custom properties.
111#[derive(Debug, Clone, PartialEq)]
112pub enum CustomPropertyValue {
113    String(String),
114    Int(i32),
115    Float(f64),
116    Bool(bool),
117    DateTime(String),
118}
119
120impl CustomPropertyValue {
121    /// Convert to the XML-level `CustomPropertyValue`.
122    pub(crate) fn to_xml(&self) -> XmlCustomPropertyValue {
123        match self {
124            Self::String(s) => XmlCustomPropertyValue::String(s.clone()),
125            Self::Int(n) => XmlCustomPropertyValue::Int(*n),
126            Self::Float(f) => XmlCustomPropertyValue::Float(*f),
127            Self::Bool(b) => XmlCustomPropertyValue::Bool(*b),
128            Self::DateTime(dt) => XmlCustomPropertyValue::DateTime(dt.clone()),
129        }
130    }
131
132    /// Convert from the XML-level `CustomPropertyValue`.
133    pub(crate) fn from_xml(val: &XmlCustomPropertyValue) -> Self {
134        match val {
135            XmlCustomPropertyValue::String(s) => Self::String(s.clone()),
136            XmlCustomPropertyValue::Int(n) => Self::Int(*n),
137            XmlCustomPropertyValue::Float(f) => Self::Float(*f),
138            XmlCustomPropertyValue::Bool(b) => Self::Bool(*b),
139            XmlCustomPropertyValue::DateTime(dt) => Self::DateTime(dt.clone()),
140        }
141    }
142}
143
144/// Find a custom property by name and return its value, or None.
145pub(crate) fn find_custom_property(
146    props: &sheetkit_xml::doc_props::CustomProperties,
147    name: &str,
148) -> Option<CustomPropertyValue> {
149    props
150        .properties
151        .iter()
152        .find(|p| p.name == name)
153        .map(|p| CustomPropertyValue::from_xml(&p.value))
154}
155
156/// Set a custom property by name (insert or update).
157/// Returns the next available pid.
158pub(crate) fn set_custom_property(
159    props: &mut sheetkit_xml::doc_props::CustomProperties,
160    name: &str,
161    value: CustomPropertyValue,
162) {
163    if let Some(existing) = props.properties.iter_mut().find(|p| p.name == name) {
164        existing.value = value.to_xml();
165        return;
166    }
167
168    let next_pid = props
169        .properties
170        .iter()
171        .map(|p| p.pid)
172        .max()
173        .map(|m| m + 1)
174        .unwrap_or(2);
175
176    props.properties.push(CustomProperty {
177        fmtid: CUSTOM_PROPERTY_FMTID.to_string(),
178        pid: next_pid,
179        name: name.to_string(),
180        value: value.to_xml(),
181    });
182}
183
184/// Remove a custom property by name. Returns true if found and removed.
185pub(crate) fn delete_custom_property(
186    props: &mut sheetkit_xml::doc_props::CustomProperties,
187    name: &str,
188) -> bool {
189    let before = props.properties.len();
190    props.properties.retain(|p| p.name != name);
191    props.properties.len() < before
192}
193
194/// High-level workbook properties (maps to the `WorkbookPr` XML element).
195///
196/// All fields are optional; `None` means the attribute is omitted from the XML.
197#[derive(Debug, Clone, Default)]
198pub struct WorkbookSettings {
199    /// Use 1904 date system instead of 1900.
200    pub date1904: Option<bool>,
201    /// Filter privacy setting.
202    pub filter_privacy: Option<bool>,
203    /// Default theme version.
204    pub default_theme_version: Option<u32>,
205    /// Show objects mode (e.g. "all", "placeholders", "none").
206    pub show_objects: Option<String>,
207    /// Code name for VBA.
208    pub code_name: Option<String>,
209    /// Check compatibility on save.
210    pub check_compatibility: Option<bool>,
211    /// Auto compress pictures.
212    pub auto_compress_pictures: Option<bool>,
213    /// Backup file setting.
214    pub backup_file: Option<bool>,
215    /// Save external link values.
216    pub save_external_link_values: Option<bool>,
217    /// Update links mode.
218    pub update_links: Option<String>,
219    /// Hide pivot field list.
220    pub hide_pivot_field_list: Option<bool>,
221    /// Show pivot chart filter.
222    pub show_pivot_chart_filter: Option<bool>,
223    /// Allow refresh query.
224    pub allow_refresh_query: Option<bool>,
225    /// Publish items.
226    pub publish_items: Option<bool>,
227    /// Show border on unselected tables.
228    pub show_border_unselected_tables: Option<bool>,
229    /// Prompted solutions.
230    pub prompted_solutions: Option<bool>,
231    /// Show ink annotation.
232    pub show_ink_annotation: Option<bool>,
233}
234
235impl From<&WorkbookPr> for WorkbookSettings {
236    fn from(pr: &WorkbookPr) -> Self {
237        Self {
238            date1904: pr.date1904,
239            filter_privacy: pr.filter_privacy,
240            default_theme_version: pr.default_theme_version,
241            show_objects: pr.show_objects.clone(),
242            code_name: pr.code_name.clone(),
243            check_compatibility: pr.check_compatibility,
244            auto_compress_pictures: pr.auto_compress_pictures,
245            backup_file: pr.backup_file,
246            save_external_link_values: pr.save_external_link_values,
247            update_links: pr.update_links.clone(),
248            hide_pivot_field_list: pr.hide_pivot_field_list,
249            show_pivot_chart_filter: pr.show_pivot_chart_filter,
250            allow_refresh_query: pr.allow_refresh_query,
251            publish_items: pr.publish_items,
252            show_border_unselected_tables: pr.show_border_unselected_tables,
253            prompted_solutions: pr.prompted_solutions,
254            show_ink_annotation: pr.show_ink_annotation,
255        }
256    }
257}
258
259impl WorkbookSettings {
260    /// Convert to the XML-level `WorkbookPr` struct.
261    pub fn to_workbook_pr(&self) -> WorkbookPr {
262        WorkbookPr {
263            date1904: self.date1904,
264            filter_privacy: self.filter_privacy,
265            default_theme_version: self.default_theme_version,
266            show_objects: self.show_objects.clone(),
267            code_name: self.code_name.clone(),
268            check_compatibility: self.check_compatibility,
269            auto_compress_pictures: self.auto_compress_pictures,
270            backup_file: self.backup_file,
271            save_external_link_values: self.save_external_link_values,
272            update_links: self.update_links.clone(),
273            hide_pivot_field_list: self.hide_pivot_field_list,
274            show_pivot_chart_filter: self.show_pivot_chart_filter,
275            allow_refresh_query: self.allow_refresh_query,
276            publish_items: self.publish_items,
277            show_border_unselected_tables: self.show_border_unselected_tables,
278            prompted_solutions: self.prompted_solutions,
279            show_ink_annotation: self.show_ink_annotation,
280        }
281    }
282}
283
284/// High-level calculation properties (maps to the `CalcPr` XML element).
285///
286/// All fields are optional; `None` means the attribute is omitted from the XML.
287#[derive(Debug, Clone, Default)]
288pub struct CalcSettings {
289    /// Calculation engine ID.
290    pub calc_id: Option<u32>,
291    /// Calculation mode: "auto", "manual", or "autoNoTable".
292    pub calc_mode: Option<String>,
293    /// Full calculation on load.
294    pub full_calc_on_load: Option<bool>,
295    /// Reference mode: "A1" or "R1C1".
296    pub ref_mode: Option<String>,
297    /// Enable iterative calculation.
298    pub iterate: Option<bool>,
299    /// Maximum iterations for iterative calculation.
300    pub iterate_count: Option<u32>,
301    /// Delta threshold for iterative calculation convergence.
302    pub iterate_delta: Option<f64>,
303    /// Full precision for calculations.
304    pub full_precision: Option<bool>,
305    /// Whether calculation was completed before save.
306    pub calc_completed: Option<bool>,
307    /// Calculate on save.
308    pub calc_on_save: Option<bool>,
309    /// Enable concurrent calculation.
310    pub concurrent_calc: Option<bool>,
311    /// Manual concurrent calculation thread count.
312    pub concurrent_manual_count: Option<u32>,
313    /// Force full calculation.
314    pub force_full_calc: Option<bool>,
315}
316
317impl From<&CalcPr> for CalcSettings {
318    fn from(pr: &CalcPr) -> Self {
319        Self {
320            calc_id: pr.calc_id,
321            calc_mode: pr.calc_mode.clone(),
322            full_calc_on_load: pr.full_calc_on_load,
323            ref_mode: pr.ref_mode.clone(),
324            iterate: pr.iterate,
325            iterate_count: pr.iterate_count,
326            iterate_delta: pr.iterate_delta,
327            full_precision: pr.full_precision,
328            calc_completed: pr.calc_completed,
329            calc_on_save: pr.calc_on_save,
330            concurrent_calc: pr.concurrent_calc,
331            concurrent_manual_count: pr.concurrent_manual_count,
332            force_full_calc: pr.force_full_calc,
333        }
334    }
335}
336
337impl CalcSettings {
338    /// Convert to the XML-level `CalcPr` struct.
339    pub fn to_calc_pr(&self) -> CalcPr {
340        CalcPr {
341            calc_id: self.calc_id,
342            calc_mode: self.calc_mode.clone(),
343            full_calc_on_load: self.full_calc_on_load,
344            ref_mode: self.ref_mode.clone(),
345            iterate: self.iterate,
346            iterate_count: self.iterate_count,
347            iterate_delta: self.iterate_delta,
348            full_precision: self.full_precision,
349            calc_completed: self.calc_completed,
350            calc_on_save: self.calc_on_save,
351            concurrent_calc: self.concurrent_calc,
352            concurrent_manual_count: self.concurrent_manual_count,
353            force_full_calc: self.force_full_calc,
354        }
355    }
356}
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361
362    #[test]
363    fn test_doc_properties_from_core_properties() {
364        let core = CoreProperties {
365            title: Some("T".to_string()),
366            creator: Some("C".to_string()),
367            ..Default::default()
368        };
369        let doc = DocProperties::from(&core);
370        assert_eq!(doc.title.as_deref(), Some("T"));
371        assert_eq!(doc.creator.as_deref(), Some("C"));
372        assert!(doc.subject.is_none());
373    }
374
375    #[test]
376    fn test_doc_properties_to_core_properties() {
377        let doc = DocProperties {
378            title: Some("T".to_string()),
379            subject: Some("S".to_string()),
380            ..Default::default()
381        };
382        let core = doc.to_core_properties();
383        assert_eq!(core.title.as_deref(), Some("T"));
384        assert_eq!(core.subject.as_deref(), Some("S"));
385        assert!(core.creator.is_none());
386    }
387
388    #[test]
389    fn test_app_properties_from_extended_properties() {
390        let ext = ExtendedProperties {
391            xmlns: namespaces::EXTENDED_PROPERTIES.to_string(),
392            xmlns_vt: None,
393            application: Some("TestApp".to_string()),
394            doc_security: Some(0),
395            company: Some("Corp".to_string()),
396            ..Default::default()
397        };
398        let app = AppProperties::from(&ext);
399        assert_eq!(app.application.as_deref(), Some("TestApp"));
400        assert_eq!(app.doc_security, Some(0));
401        assert_eq!(app.company.as_deref(), Some("Corp"));
402    }
403
404    #[test]
405    fn test_app_properties_to_extended_properties() {
406        let app = AppProperties {
407            application: Some("SheetKit".to_string()),
408            company: Some("Acme".to_string()),
409            ..Default::default()
410        };
411        let ext = app.to_extended_properties();
412        assert_eq!(ext.xmlns, namespaces::EXTENDED_PROPERTIES);
413        assert_eq!(ext.application.as_deref(), Some("SheetKit"));
414        assert_eq!(ext.company.as_deref(), Some("Acme"));
415    }
416
417    #[test]
418    fn test_custom_property_value_roundtrip() {
419        let vals = vec![
420            CustomPropertyValue::String("hello".to_string()),
421            CustomPropertyValue::Int(42),
422            CustomPropertyValue::Float(3.15),
423            CustomPropertyValue::Bool(true),
424            CustomPropertyValue::DateTime("2024-01-01T00:00:00Z".to_string()),
425        ];
426        for v in &vals {
427            let xml = v.to_xml();
428            let back = CustomPropertyValue::from_xml(&xml);
429            assert_eq!(*v, back);
430        }
431    }
432
433    #[test]
434    fn test_set_and_find_custom_property() {
435        let mut props = sheetkit_xml::doc_props::CustomProperties::default();
436        set_custom_property(
437            &mut props,
438            "Project",
439            CustomPropertyValue::String("SK".to_string()),
440        );
441        let found = find_custom_property(&props, "Project");
442        assert_eq!(found, Some(CustomPropertyValue::String("SK".to_string())));
443        assert_eq!(props.properties[0].pid, 2);
444    }
445
446    #[test]
447    fn test_set_custom_property_update_existing() {
448        let mut props = sheetkit_xml::doc_props::CustomProperties::default();
449        set_custom_property(
450            &mut props,
451            "Key",
452            CustomPropertyValue::String("old".to_string()),
453        );
454        set_custom_property(
455            &mut props,
456            "Key",
457            CustomPropertyValue::String("new".to_string()),
458        );
459        assert_eq!(props.properties.len(), 1);
460        assert_eq!(
461            find_custom_property(&props, "Key"),
462            Some(CustomPropertyValue::String("new".to_string()))
463        );
464    }
465
466    #[test]
467    fn test_delete_custom_property() {
468        let mut props = sheetkit_xml::doc_props::CustomProperties::default();
469        set_custom_property(&mut props, "Key", CustomPropertyValue::Int(1));
470        assert!(delete_custom_property(&mut props, "Key"));
471        assert!(!delete_custom_property(&mut props, "Key")); // already gone
472        assert!(find_custom_property(&props, "Key").is_none());
473    }
474
475    #[test]
476    fn test_custom_property_pid_auto_increment() {
477        let mut props = sheetkit_xml::doc_props::CustomProperties::default();
478        set_custom_property(&mut props, "A", CustomPropertyValue::Int(1));
479        set_custom_property(&mut props, "B", CustomPropertyValue::Int(2));
480        set_custom_property(&mut props, "C", CustomPropertyValue::Int(3));
481        assert_eq!(props.properties[0].pid, 2);
482        assert_eq!(props.properties[1].pid, 3);
483        assert_eq!(props.properties[2].pid, 4);
484    }
485
486    // WorkbookSettings tests
487
488    #[test]
489    fn test_workbook_settings_default() {
490        let settings = WorkbookSettings::default();
491        assert!(settings.date1904.is_none());
492        assert!(settings.filter_privacy.is_none());
493        assert!(settings.default_theme_version.is_none());
494        assert!(settings.show_objects.is_none());
495        assert!(settings.code_name.is_none());
496        assert!(settings.check_compatibility.is_none());
497        assert!(settings.auto_compress_pictures.is_none());
498        assert!(settings.backup_file.is_none());
499        assert!(settings.save_external_link_values.is_none());
500        assert!(settings.update_links.is_none());
501        assert!(settings.hide_pivot_field_list.is_none());
502        assert!(settings.show_pivot_chart_filter.is_none());
503        assert!(settings.allow_refresh_query.is_none());
504        assert!(settings.publish_items.is_none());
505        assert!(settings.show_border_unselected_tables.is_none());
506        assert!(settings.prompted_solutions.is_none());
507        assert!(settings.show_ink_annotation.is_none());
508    }
509
510    #[test]
511    fn test_workbook_settings_to_xml_roundtrip() {
512        let settings = WorkbookSettings {
513            date1904: Some(false),
514            filter_privacy: Some(true),
515            default_theme_version: Some(166925),
516            show_objects: Some("all".to_string()),
517            code_name: Some("ThisWorkbook".to_string()),
518            check_compatibility: Some(true),
519            auto_compress_pictures: Some(false),
520            backup_file: Some(true),
521            save_external_link_values: Some(true),
522            update_links: Some("always".to_string()),
523            hide_pivot_field_list: Some(false),
524            show_pivot_chart_filter: Some(true),
525            allow_refresh_query: Some(true),
526            publish_items: Some(false),
527            show_border_unselected_tables: Some(true),
528            prompted_solutions: Some(false),
529            show_ink_annotation: Some(true),
530        };
531        let pr = settings.to_workbook_pr();
532        let back = WorkbookSettings::from(&pr);
533
534        assert_eq!(back.date1904, Some(false));
535        assert_eq!(back.filter_privacy, Some(true));
536        assert_eq!(back.default_theme_version, Some(166925));
537        assert_eq!(back.show_objects.as_deref(), Some("all"));
538        assert_eq!(back.code_name.as_deref(), Some("ThisWorkbook"));
539        assert_eq!(back.check_compatibility, Some(true));
540        assert_eq!(back.auto_compress_pictures, Some(false));
541        assert_eq!(back.backup_file, Some(true));
542        assert_eq!(back.save_external_link_values, Some(true));
543        assert_eq!(back.update_links.as_deref(), Some("always"));
544        assert_eq!(back.hide_pivot_field_list, Some(false));
545        assert_eq!(back.show_pivot_chart_filter, Some(true));
546        assert_eq!(back.allow_refresh_query, Some(true));
547        assert_eq!(back.publish_items, Some(false));
548        assert_eq!(back.show_border_unselected_tables, Some(true));
549        assert_eq!(back.prompted_solutions, Some(false));
550        assert_eq!(back.show_ink_annotation, Some(true));
551    }
552
553    #[test]
554    fn test_workbook_settings_date1904() {
555        let settings = WorkbookSettings {
556            date1904: Some(true),
557            ..Default::default()
558        };
559        let pr = settings.to_workbook_pr();
560        assert_eq!(pr.date1904, Some(true));
561        // All other fields should be None
562        assert!(pr.filter_privacy.is_none());
563        assert!(pr.default_theme_version.is_none());
564        assert!(pr.code_name.is_none());
565
566        let back = WorkbookSettings::from(&pr);
567        assert_eq!(back.date1904, Some(true));
568        assert!(back.filter_privacy.is_none());
569    }
570
571    // CalcSettings tests
572
573    #[test]
574    fn test_calc_settings_default() {
575        let settings = CalcSettings::default();
576        assert!(settings.calc_id.is_none());
577        assert!(settings.calc_mode.is_none());
578        assert!(settings.full_calc_on_load.is_none());
579        assert!(settings.ref_mode.is_none());
580        assert!(settings.iterate.is_none());
581        assert!(settings.iterate_count.is_none());
582        assert!(settings.iterate_delta.is_none());
583        assert!(settings.full_precision.is_none());
584        assert!(settings.calc_completed.is_none());
585        assert!(settings.calc_on_save.is_none());
586        assert!(settings.concurrent_calc.is_none());
587        assert!(settings.concurrent_manual_count.is_none());
588        assert!(settings.force_full_calc.is_none());
589    }
590
591    #[test]
592    fn test_calc_settings_to_xml_roundtrip() {
593        let settings = CalcSettings {
594            calc_id: Some(191029),
595            calc_mode: Some("auto".to_string()),
596            full_calc_on_load: Some(true),
597            ref_mode: Some("A1".to_string()),
598            iterate: Some(true),
599            iterate_count: Some(100),
600            iterate_delta: Some(0.001),
601            full_precision: Some(true),
602            calc_completed: Some(true),
603            calc_on_save: Some(true),
604            concurrent_calc: Some(true),
605            concurrent_manual_count: Some(4),
606            force_full_calc: Some(false),
607        };
608        let pr = settings.to_calc_pr();
609        let back = CalcSettings::from(&pr);
610
611        assert_eq!(back.calc_id, Some(191029));
612        assert_eq!(back.calc_mode.as_deref(), Some("auto"));
613        assert_eq!(back.full_calc_on_load, Some(true));
614        assert_eq!(back.ref_mode.as_deref(), Some("A1"));
615        assert_eq!(back.iterate, Some(true));
616        assert_eq!(back.iterate_count, Some(100));
617        assert_eq!(back.iterate_delta, Some(0.001));
618        assert_eq!(back.full_precision, Some(true));
619        assert_eq!(back.calc_completed, Some(true));
620        assert_eq!(back.calc_on_save, Some(true));
621        assert_eq!(back.concurrent_calc, Some(true));
622        assert_eq!(back.concurrent_manual_count, Some(4));
623        assert_eq!(back.force_full_calc, Some(false));
624    }
625
626    #[test]
627    fn test_calc_settings_manual_mode() {
628        let settings = CalcSettings {
629            calc_mode: Some("manual".to_string()),
630            calc_on_save: Some(false),
631            ..Default::default()
632        };
633        let pr = settings.to_calc_pr();
634        assert_eq!(pr.calc_mode.as_deref(), Some("manual"));
635        assert_eq!(pr.calc_on_save, Some(false));
636        // All other fields should be None
637        assert!(pr.calc_id.is_none());
638        assert!(pr.iterate.is_none());
639
640        let back = CalcSettings::from(&pr);
641        assert_eq!(back.calc_mode.as_deref(), Some("manual"));
642        assert_eq!(back.calc_on_save, Some(false));
643        assert!(back.calc_id.is_none());
644    }
645
646    #[test]
647    fn test_calc_settings_iterative() {
648        let settings = CalcSettings {
649            iterate: Some(true),
650            iterate_count: Some(200),
651            iterate_delta: Some(0.0001),
652            ..Default::default()
653        };
654        let pr = settings.to_calc_pr();
655        assert_eq!(pr.iterate, Some(true));
656        assert_eq!(pr.iterate_count, Some(200));
657        assert_eq!(pr.iterate_delta, Some(0.0001));
658        // Other fields None
659        assert!(pr.calc_mode.is_none());
660        assert!(pr.ref_mode.is_none());
661
662        let back = CalcSettings::from(&pr);
663        assert_eq!(back.iterate, Some(true));
664        assert_eq!(back.iterate_count, Some(200));
665        assert_eq!(back.iterate_delta, Some(0.0001));
666    }
667}