lcax_convert 3.4.2

LCAx is an open, machine and human-readable data format for exchanging LCA results.
Documentation
use crate::br_standard::models::{BRComponent, BREnvironmentalData, BROperation, BRProjectInfo};
use crate::br_standard::parse::parse_br_standard;
use crate::br_standard::translations::GENERAL_INFORMATION_TRANSLATION;
use calamine::{open_workbook, open_workbook_from_rs, Data, DataType, Error, Reader, Xlsx};
use lcax_models::life_cycle_base::{ImpactCategory, ImpactCategoryKey, Impacts, LifeCycleModule};
use lcax_models::project::Project;
use std::collections::HashMap;
use std::io::{Cursor, Seek};
use std::path::PathBuf;

pub fn br_standard_from_file(file_path: PathBuf) -> Result<Project, String> {
    let (project_info, components, operations) = read_br_standard_from_file(&file_path).unwrap();
    let file_name = file_path.file_name().unwrap().to_str().unwrap();
    Ok(parse_br_standard(
        file_name,
        &project_info,
        &components,
        &operations,
    )?)
}

pub fn read_br_standard<T>(
    workbook: &mut Xlsx<T>,
) -> Result<(BRProjectInfo, Vec<BRComponent>, Vec<BROperation>), Error>
where
    T: Seek,
    T: std::io::Read,
{
    let project_info = read_project_info(workbook)?;
    let components = read_components(workbook, &project_info.gross_floor_area, &50.0)?;
    let operations = read_operations(workbook, &50.0)?;
    Ok((project_info, components, operations))
}

pub fn read_br_standard_from_file(
    file_path: &PathBuf,
) -> Result<(BRProjectInfo, Vec<BRComponent>, Vec<BROperation>), Error> {
    let mut workbook: Xlsx<_> = open_workbook(file_path)?;
    Ok(read_br_standard(&mut workbook)?)
}

pub fn read_br_standard_from_bytes(
    file: Vec<u8>,
) -> Result<(BRProjectInfo, Vec<BRComponent>, Vec<BROperation>), Error> {
    let buffer: Cursor<Vec<u8>> = Cursor::new(file);
    let mut workbook: Xlsx<_> = open_workbook_from_rs(buffer)?;
    Ok(read_br_standard(&mut workbook)?)
}

fn read_project_info<T>(workbook: &mut Xlsx<T>) -> Result<BRProjectInfo, Error>
where
    T: Seek,
    T: std::io::Read,
{
    let mut project_info_map: HashMap<&str, Data> = HashMap::new();

    for name in vec!["General information", "Generel information"] {
        match workbook.worksheet_range(name) {
            Ok(range) => {
                for row in range.rows() {
                    match &row[0] {
                        Data::String(name) => {
                            match GENERAL_INFORMATION_TRANSLATION.get(&name) {
                                Some(key) => project_info_map.insert(*key, row[1].clone()),
                                None => continue,
                            };
                        }
                        _ => continue,
                    }
                }
            }
            Err(_) => continue,
        }
    }

    Ok(BRProjectInfo {
        typology: project_info_map
            .get("typology")
            .unwrap_or(&Data::String("unknown".to_string()))
            .to_string(),
        building_type: project_info_map
            .get("building_type")
            .unwrap_or(&Data::String("unknown".to_string()))
            .to_string(),
        floors_above_ground: project_info_map
            .get("floors_above_ground")
            .unwrap()
            .as_f64()
            .unwrap() as u16,
        floors_below_ground: project_info_map
            .get("floors_below_ground")
            .unwrap()
            .as_f64()
            .unwrap() as u16,
        address: project_info_map
            .get("address")
            .unwrap_or(&Data::String("unknown".to_string()))
            .to_string(),
        building_permission_date: Some(
            project_info_map
                .get("building_permission_date")
                .unwrap()
                .to_string(),
        ),
        building_operation_date: Some(
            project_info_map
                .get("building_operation_date")
                .unwrap()
                .to_string(),
        ),
        building_regulation: project_info_map
            .get("building_regulation")
            .unwrap()
            .to_string(),
        threshold_new_constructions: project_info_map
            .get("threshold_new_constructions")
            .unwrap()
            .as_f64()
            .unwrap(),
        threshold_low_emission: project_info_map
            .get("threshold_low_emission")
            .unwrap()
            .as_f64()
            .unwrap(),
        total_emissions: project_info_map
            .get("total_emissions")
            .unwrap()
            .as_f64()
            .unwrap(),
        emission_modifiers: project_info_map
            .get("emission_modifiers")
            .unwrap()
            .as_f64()
            .unwrap(),
        emission_all_modules: project_info_map
            .get("emission_all_modules")
            .unwrap()
            .as_f64()
            .unwrap(),
        emission_module_d: project_info_map
            .get("emission_module_d")
            .unwrap()
            .as_f64()
            .unwrap(),
        gross_floor_area: project_info_map
            .get("gross_floor_area")
            .unwrap()
            .as_f64()
            .unwrap_or(0.0),
        basement_area: project_info_map.get("basement_area").unwrap().as_f64(),
        staircase_area: project_info_map
            .get("staircase_area")
            .unwrap()
            .as_f64()
            .unwrap_or(0.0),
        garage_area: project_info_map
            .get("garage_area")
            .unwrap()
            .as_f64()
            .unwrap_or(0.0),
        carport_area: project_info_map.get("carport_area").unwrap().as_f64(),
        walk_on_ceilings: project_info_map.get("walk_on_ceilings").unwrap().as_f64(),
        total_area: project_info_map
            .get("total_area")
            .unwrap()
            .as_f64()
            .unwrap_or(0.0),
        exterior_area: project_info_map.get("exterior_area").unwrap().as_f64(),
        heated_area: project_info_map
            .get("heated_area")
            .unwrap()
            .as_f64()
            .unwrap_or(0.0),
        pv_area: project_info_map.get("pv_area").unwrap().as_f64(),
        energy_class: Some(project_info_map.get("energy_class").unwrap().to_string()),
        heating_no_modifiers: project_info_map
            .get("heating_no_modifiers")
            .unwrap()
            .as_f64()
            .unwrap(),
        electricity_no_modifiers: project_info_map
            .get("electricity_no_modifiers")
            .unwrap()
            .as_f64()
            .unwrap(),
        heating_with_modifiers: project_info_map
            .get("heating_with_modifiers")
            .unwrap()
            .as_f64()
            .unwrap_or(0.0),
        electricity_with_modifiers: project_info_map
            .get("electricity_with_modifiers")
            .unwrap()
            .as_f64()
            .unwrap_or(0.0),
        electricity_production: project_info_map
            .get("electricity_production")
            .unwrap()
            .as_f64()
            .unwrap_or(0.0),
        bbr_code: Some(project_info_map.get("bbr_code").unwrap().to_string()),
        lca_software: project_info_map.get("lca_software").unwrap().to_string(),
        lca_version: project_info_map.get("lca_version").unwrap().to_string(),
    })
}

fn read_operations<T>(
    workbook: &mut Xlsx<T>,
    reference_service_life: &f64,
) -> Result<Vec<BROperation>, Error>
where
    T: Seek,
    T: std::io::Read,
{
    let mut operations = vec![];

    if let Ok(r) = workbook.worksheet_range("Drift") {
        for (index, row) in r.rows().enumerate() {
            if index < 2 {
                continue;
            } else if row[1] == Data::Empty || row[1] == "Type" {
                continue;
            }

            operations.push(BROperation {
                name: row[0].to_string(),
                category: row[1].to_string(),
                _type: row[2].to_string(),
                product: row[3].to_string(),
                included: row[4].to_string() == "Ja".to_string(),
                quantity: row[5].as_f64().unwrap_or(0.0),
                unit: row[6].to_string(),
                environmental_data: BREnvironmentalData {
                    link: row[8].to_string(),
                    epd_number: row[9].to_string(),
                    expiration_data: row[10].to_string(),
                    standard: row[11].to_string(),
                },
                results: Impacts::from([(
                    ImpactCategoryKey::GWP,
                    ImpactCategory::from([(
                        LifeCycleModule::B6,
                        Some(row[13].as_f64().unwrap_or(0.0) * reference_service_life),
                    )]),
                )]),
            })
        }
    }
    Ok(operations)
}

trait FromBR<T> {
    fn from_br(_: T) -> Self;
}

impl FromBR<(&[Data], i32, &f64, &f64)> for Impacts {
    fn from_br(
        (row, start_index, gross_floor_area, reference_service_life): (&[Data], i32, &f64, &f64),
    ) -> Self {
        let categories = [
            ImpactCategoryKey::GWP,
            ImpactCategoryKey::GWP_FOS,
            ImpactCategoryKey::GWP_BIO,
            ImpactCategoryKey::GWP_LUL,
            ImpactCategoryKey::ODP,
            ImpactCategoryKey::AP,
            ImpactCategoryKey::EP_FW,
            ImpactCategoryKey::EP_MAR,
            ImpactCategoryKey::EP_TER,
            ImpactCategoryKey::POCP,
            ImpactCategoryKey::ADPE,
            ImpactCategoryKey::ADPF,
            ImpactCategoryKey::WDP,
        ];
        let stages = [
            LifeCycleModule::A1A3,
            LifeCycleModule::A4,
            LifeCycleModule::A5,
            LifeCycleModule::B1,
            LifeCycleModule::B2,
            LifeCycleModule::B3,
            LifeCycleModule::B4,
            LifeCycleModule::B5,
            LifeCycleModule::B6,
            LifeCycleModule::B7,
            LifeCycleModule::C1,
            LifeCycleModule::C2,
            LifeCycleModule::C3,
            LifeCycleModule::C4,
            LifeCycleModule::D,
        ];
        let mut results = Impacts::new();
        for (c_index, category) in categories.iter().enumerate() {
            let mut result_category = ImpactCategory::new();
            for (s_index, stage) in stages.iter().enumerate() {
                let index = start_index + (c_index as i32 * 15) + s_index as i32;
                let data = &row[index as usize];
                if !data.is_empty() {
                    result_category.insert(
                        stage.clone(),
                        Some(data.as_f64().unwrap() * gross_floor_area * reference_service_life),
                    );
                }
            }
            results.insert(category.clone(), result_category);
        }
        results
    }
}

fn read_components<T>(
    workbook: &mut Xlsx<T>,
    gross_floor_area: &f64,
    reference_service_life: &f64,
) -> Result<Vec<BRComponent>, Error>
where
    T: Seek,
    T: std::io::Read,
{
    let mut components = vec![];

    for name in vec!["Bygningsdele", "BR18 vejledning"] {
        match workbook.worksheet_range(name) {
            Ok(range) => {
                for (index, row) in range.rows().enumerate() {
                    if index < 4 {
                        continue;
                    } else if row[1] == Data::Empty {
                        continue;
                    }

                    components.push(BRComponent {
                        category: row[0].to_string(),
                        _type: row[1].to_string(),
                        building_part: row[2].to_string(),
                        building_part_main: row[3].to_string(),
                        building_part_sub: row[4].to_string(),
                        construction: row[5].to_string(),
                        product: row[6].to_string(),
                        included: row[7].to_string() == "Ja".to_string(),
                        recycled: row[8].to_string() == "Ja".to_string(),
                        structural: row[9].to_string() == "Ja".to_string(),
                        input_quantity: row[10].as_f64().unwrap_or(0.0),
                        input_unit: row[11].to_string(),
                        computed_quantity: row[12].as_f64().unwrap_or(0.0),
                        computed_unit: row[13].to_string(),
                        reference_service_life: row[14].as_f64().unwrap_or(0.0) as u16,
                        delayed_start: row[15].as_f64().unwrap_or(0.0) as u16,
                        replacements: row[16].as_f64().unwrap_or(0.0) as u16,
                        weight: row[17].as_f64().unwrap_or(0.0),
                        weight_unit: row[18].to_string(),
                        environmental_data: BREnvironmentalData {
                            link: row[22].to_string(),
                            epd_number: row[23].to_string(),
                            expiration_data: row[24].to_string(),
                            standard: row[25].to_string(),
                        },
                        results: Impacts::from_br((
                            row,
                            28,
                            gross_floor_area,
                            reference_service_life,
                        )),
                    })
                }
            }
            Err(_) => continue,
        }
    }
    Ok(components)
}