Skip to main content

lib_bcsv_jmap/
csv.rs

1use std::fs::File;
2use std::io::{BufReader, BufWriter};
3use std::path::Path;
4
5use crate::entry::Entry;
6use crate::error::{JMapError, Result};
7use crate::field::{Field, FieldType, FieldValue};
8use crate::hash::HashTable;
9use crate::jmap::JMapInfo;
10
11/// Read a JMapInfo from a CSV file
12///
13/// The CSV format uses a header row where each column is formatted as:
14/// `FieldName:Type:DefaultValue`
15///
16/// For example: `ScenarioNo:Int:0,ZoneName:String:0`
17/// The delimiter between the parts can be customized (default is ':') and should not appear in field names or type names
18///
19/// # Arguments
20/// - `hash_table` - The hash table to use for field name lookups. Field names from the CSV will be added to this hash table
21/// - `path` - The path to the CSV file to read
22/// - `header_delimiter` - Optional character that separates field name, type, and default value in the header. Default is ':'
23///
24/// # Returns
25/// A JMapInfo populated with fields and entries from the CSV file
26pub fn from_csv<H: HashTable, P: AsRef<Path>>(
27    hash_table: H,
28    path: P,
29    header_delimiter: Option<char>,
30
31) -> Result<JMapInfo<H>> {
32    let delimiter = header_delimiter.unwrap_or(':');
33    let file = File::open(path)?;
34    let reader = BufReader::new(file);
35    let mut csv_reader = csv::ReaderBuilder::new()
36        .has_headers(false)
37        .from_reader(reader);
38
39    let mut jmap = JMapInfo::new(hash_table);
40    let mut records = csv_reader.records();
41
42    // Parse header
43    let header = records
44        .next()
45        .ok_or_else(|| JMapError::CsvError("CSV file is empty".to_string()))??;
46
47    let mut field_infos: Vec<(u32, FieldType)> = Vec::new();
48
49    for field_desc in header.iter() {
50        let parts: Vec<&str> = field_desc.split(delimiter).collect();
51
52        if parts.len() != 3 {
53            return Err(JMapError::InvalidCsvFieldDescriptor(format!(
54                "Expected 3 parts (name{}type{}default), got: {}",
55                delimiter,
56                delimiter,
57                field_desc
58            )));
59        }
60
61        let field_name = parts[0];
62        let type_name = parts[1];
63        let _default_str = parts[2];
64
65        if field_name.is_empty() {
66            return Err(JMapError::InvalidCsvFieldDescriptor(
67                "Field name cannot be empty".to_string(),
68            ));
69        }
70
71        let field_type = FieldType::from_csv_name(type_name).ok_or_else(|| {
72            JMapError::InvalidCsvFieldDescriptor(format!("Unknown field type: {}", type_name))
73        })?;
74
75        // Parse hash from [XXXXXXXX] format or compute from name
76        let hash = if field_name.starts_with('[') && field_name.ends_with(']') {
77            let hex_str = &field_name[1..field_name.len() - 1];
78            u32::from_str_radix(hex_str, 16).map_err(|_| {
79                JMapError::InvalidCsvFieldDescriptor(format!("Invalid hash: {}", field_name))
80            })?
81        } else {
82            jmap.hash_table_mut().add(field_name)
83        };
84
85        let default = FieldValue::default_for(field_type);
86        let field = Field::with_default(hash, field_type, default);
87        jmap.fields_map_mut().insert(hash, field);
88        field_infos.push((hash, field_type));
89    }
90
91    // Parse data rows
92    for result in records {
93        let record = result?;
94        let mut entry = Entry::with_capacity(field_infos.len());
95
96        for (i, (hash, field_type)) in field_infos.iter().enumerate() {
97            let value_str = record.get(i).unwrap_or("");
98
99            let value = if value_str.is_empty() {
100                FieldValue::default_for(*field_type)
101            } else {
102                parse_field_value(value_str, *field_type)?
103            };
104
105            entry.set_by_hash(*hash, value);
106        }
107
108        jmap.entries_vec_mut().push(entry);
109    }
110
111    Ok(jmap)
112}
113
114/// Write a JMapInfo to a CSV file
115///
116/// The CSV format uses a header row where each column is formatted as:
117/// `FieldName:Type:DefaultValue`
118///
119/// # Arguments
120/// - `jmap` - The JMapInfo to export to CSV
121/// - `path` - The path to the CSV file to write
122/// - `header_delimiter` - Optional character that separates field name, type, and default value in the header. Default is ':'
123///
124/// # Returns
125/// Ok(()) if the export was successful, or an error if the file could not be written
126pub fn to_csv<H: HashTable, P: AsRef<Path>>(jmap: &JMapInfo<H>, path: P, header_delimiter: Option<char>) -> Result<()> {
127    let delimiter = header_delimiter.unwrap_or(':');
128    let file = File::create(path)?;
129    let writer = BufWriter::new(file);
130    let mut csv_writer = csv::Writer::from_writer(writer);
131
132    // Write header
133    let headers: Vec<String> = jmap
134        .fields()
135        .map(|field| {
136            let name = jmap.field_name(field.hash);
137            let type_name = field.field_type.csv_name();
138            let default = default_csv_value(field.field_type);
139            format!("{}{}{}{}{}", name, delimiter, type_name, delimiter, default)
140        })
141        .collect();
142
143    csv_writer.write_record(&headers)?;
144
145    // Write entries
146    for entry in jmap.entries() {
147        let values: Vec<String> = jmap
148            .fields()
149            .map(|field| {
150                entry
151                    .get_by_hash(field.hash)
152                    .map(|v| v.to_string())
153                    .unwrap_or_default()
154            })
155            .collect();
156
157        csv_writer.write_record(&values)?;
158    }
159
160    csv_writer.flush()?;
161    Ok(())
162}
163
164fn parse_field_value(s: &str, field_type: FieldType) -> Result<FieldValue> {
165    match field_type {
166        FieldType::Long | FieldType::UnsignedLong | FieldType::Short | FieldType::Char => {
167            let v: i32 = s.parse().map_err(|_| {
168                JMapError::CsvError(format!("Cannot parse '{}' as integer", s))
169            })?;
170            Ok(FieldValue::Int(v))
171        }
172        FieldType::Float => {
173            let v: f32 = s.parse().map_err(|_| {
174                JMapError::CsvError(format!("Cannot parse '{}' as float", s))
175            })?;
176            Ok(FieldValue::Float(v))
177        }
178        FieldType::String | FieldType::StringOffset => Ok(FieldValue::String(s.to_string())),
179    }
180}
181
182fn default_csv_value(field_type: FieldType) -> &'static str {
183    match field_type {
184        FieldType::Long | FieldType::UnsignedLong | FieldType::Short | FieldType::Char => "0",
185        FieldType::Float => "0.0",
186        FieldType::String | FieldType::StringOffset => "0",
187    }
188}