From a4e76ed6eeebfd339e6dcc42354390a1b4ce8013 Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Fri, 5 Sep 2025 20:22:56 +0200 Subject: [PATCH] Updated with core developer feedback: fixed for Tonne and added tests --- src/bonsai/bonsai/bim/prop.py | 4 +- src/bonsai/bonsai/tool/unit.py | 6 +- src/bonsai/test/core/test_unit.py | 155 +++++++++++- src/bonsai/test/tool/test_unit.py | 143 +++++++++++ .../api/unit/add_conversion_based_unit.py | 2 +- .../ifcopenshell/util/unit.py | 3 + .../test/util/test_unit.py | 230 ++++++++++++++++++ 7 files changed, 535 insertions(+), 8 deletions(-) diff --git a/src/bonsai/bonsai/bim/prop.py b/src/bonsai/bonsai/bim/prop.py index 93730b475b..7b24d2a1d1 100644 --- a/src/bonsai/bonsai/bim/prop.py +++ b/src/bonsai/bonsai/bim/prop.py @@ -592,7 +592,7 @@ class BIMProperties(PropertyGroup): ("GRAM", "Gram", "Grams"), ("POUND", "Pound", "Pounds"), ("OUNCE", "Ounce", "Ounces"), - ("TON", "Ton", "Metric Tons"), + ("TONNE", "Tonne", "Metric Tons"), ], name="Mass Unit", default="KILOGRAM", @@ -601,7 +601,7 @@ class BIMProperties(PropertyGroup): time_unit: EnumProperty( items=[ ("SECOND", "Second", "Seconds"), - ("MINUTE", "Minutes", "Minutes"), + ("MINUTE", "Minute", "Minutes"), ("HOUR", "Hour", "Hours"), ("DAY", "Day", "Days"), ], diff --git a/src/bonsai/bonsai/tool/unit.py b/src/bonsai/bonsai/tool/unit.py index 293136fb8c..a3c9569d62 100644 --- a/src/bonsai/bonsai/tool/unit.py +++ b/src/bonsai/bonsai/tool/unit.py @@ -344,9 +344,9 @@ class Unit(bonsai.core.tool.Unit): elif unit_type == "VOLUMEUNIT": return bim_props.volume_unit elif unit_type == "MASSUNIT": - return bim_props.mass_unit + return bim_props.mass_unit.lower() elif unit_type == "TIMEUNIT": - return bim_props.time_unit + return bim_props.time_unit.lower() else: assert_never(unit_type) @@ -369,7 +369,7 @@ class Unit(bonsai.core.tool.Unit): return None elif unit == "KILOGRAM": return "KILO" - elif unit == "TON": + elif unit == "TONNE": return "MEGA" elif unit in ["POUND", "OUNCE"]: return "CONVERSION" diff --git a/src/bonsai/test/core/test_unit.py b/src/bonsai/test/core/test_unit.py index d79b6b83fb..d758b7829c 100644 --- a/src/bonsai/test/core/test_unit.py +++ b/src/bonsai/test/core/test_unit.py @@ -26,6 +26,9 @@ class TestAssignSceneUnits: unit.get_scene_unit_si_prefix("LENGTHUNIT").should_be_called().will_return("prefix") unit.get_scene_unit_si_prefix("AREAUNIT").should_be_called().will_return("prefix") unit.get_scene_unit_si_prefix("VOLUMEUNIT").should_be_called().will_return("prefix") + unit.get_scene_unit_si_prefix("MASSUNIT").should_be_called().will_return("KILO") + unit.get_scene_unit_si_prefix("TIMEUNIT").should_be_called().will_return(None) + ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix="prefix").should_be_called().will_return( "lengthunit" ) @@ -38,7 +41,13 @@ class TestAssignSceneUnits: ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit") - ifc.run("unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit"]).should_be_called() + ifc.run("unit.add_si_unit", unit_type="MASSUNIT", prefix="KILO").should_be_called().will_return("massunit") + + ifc.run("unit.add_si_unit", unit_type="TIMEUNIT", prefix=None).should_be_called().will_return("timeunit") + + ifc.run( + "unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit", "massunit", "timeunit"] + ).should_be_called() subject.assign_scene_units(ifc, unit) def test_creating_and_assigning_imperial_units(self, ifc, unit): @@ -54,7 +63,15 @@ class TestAssignSceneUnits: ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit") - ifc.run("unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit"]).should_be_called() + unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return("massname") + ifc.run("unit.add_conversion_based_unit", name="massname").should_be_called().will_return("massunit") + + unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return("timename") + ifc.run("unit.add_conversion_based_unit", name="timename").should_be_called().will_return("timeunit") + + ifc.run( + "unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit", "massunit", "timeunit"] + ).should_be_called() subject.assign_scene_units(ifc, unit) @@ -162,3 +179,137 @@ class TestEditUnit: unit.import_units().should_be_called() unit.clear_active_unit().should_be_called() subject.edit_unit(ifc, unit, unit="unit") + + +class TestAssignSceneUnitsWithMassAndTime: + def test_creating_and_assigning_metric_units_with_mass_and_time(self, ifc, unit): + unit.is_scene_unit_metric().should_be_called().will_return(True) + unit.get_scene_unit_si_prefix("LENGTHUNIT").should_be_called().will_return("prefix") + unit.get_scene_unit_si_prefix("AREAUNIT").should_be_called().will_return("prefix") + unit.get_scene_unit_si_prefix("VOLUMEUNIT").should_be_called().will_return("prefix") + unit.get_scene_unit_si_prefix("MASSUNIT").should_be_called().will_return("KILO") + unit.get_scene_unit_si_prefix("TIMEUNIT").should_be_called().will_return("CONVERSION") + unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return("minute") + + ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix="prefix").should_be_called().will_return( + "lengthunit" + ) + ifc.run("unit.add_si_unit", unit_type="AREAUNIT", prefix="prefix").should_be_called().will_return("areaunit") + ifc.run("unit.add_si_unit", unit_type="VOLUMEUNIT", prefix="prefix").should_be_called().will_return( + "volumeunit" + ) + ifc.run("unit.add_si_unit", unit_type="MASSUNIT", prefix="KILO").should_be_called().will_return("massunit") + ifc.run("unit.add_conversion_based_unit", name="minute").should_be_called().will_return("timeunit") + ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit") + + ifc.run( + "unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit", "massunit", "timeunit"] + ).should_be_called() + subject.assign_scene_units(ifc, unit) + + def test_creating_and_assigning_imperial_units_with_mass_and_time(self, ifc, unit): + unit.is_scene_unit_metric().should_be_called().will_return(False) + unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("foot") + unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return("square foot") + unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return("cubic foot") + unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return("pound") + unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return("SECOND") + + ifc.run("unit.add_conversion_based_unit", name="foot").should_be_called().will_return("lengthunit") + ifc.run("unit.add_conversion_based_unit", name="square foot").should_be_called().will_return("areaunit") + ifc.run("unit.add_conversion_based_unit", name="cubic foot").should_be_called().will_return("volumeunit") + ifc.run("unit.add_conversion_based_unit", name="pound").should_be_called().will_return("massunit") + ifc.run("unit.add_si_unit", unit_type="TIMEUNIT", prefix=None).should_be_called().will_return("timeunit") + ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit") + + ifc.run( + "unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit", "massunit", "timeunit"] + ).should_be_called() + subject.assign_scene_units(ifc, unit) + + def test_creating_and_assigning_conversion_based_mass_units(self, ifc, unit): + unit.is_scene_unit_metric().should_be_called().will_return(False) + unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("foot") + unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return("square foot") + unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return("cubic foot") + unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return("tonne") + unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return("minute") + + ifc.run("unit.add_conversion_based_unit", name="foot").should_be_called().will_return("lengthunit") + ifc.run("unit.add_conversion_based_unit", name="square foot").should_be_called().will_return("areaunit") + ifc.run("unit.add_conversion_based_unit", name="cubic foot").should_be_called().will_return("volumeunit") + ifc.run("unit.add_conversion_based_unit", name="tonne").should_be_called().will_return("massunit") + ifc.run("unit.add_conversion_based_unit", name="minute").should_be_called().will_return("timeunit") + ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit") + + ifc.run( + "unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit", "massunit", "timeunit"] + ).should_be_called() + subject.assign_scene_units(ifc, unit) + + +class TestAddTimeUnit: + def test_adding_si_time_unit(self, ifc, unit): + ifc.run("unit.add_si_unit", unit_type="TIMEUNIT").should_be_called().will_return("unit") + unit.import_units().should_be_called() + assert subject.add_si_unit(ifc, unit, unit_type="TIMEUNIT") == "unit" + + def test_adding_conversion_based_time_unit_minute(self, ifc, unit): + ifc.run("unit.add_conversion_based_unit", name="minute").should_be_called().will_return("unit") + unit.import_units().should_be_called() + assert subject.add_conversion_based_unit(ifc, unit, name="minute") == "unit" + + def test_adding_conversion_based_time_unit_hour(self, ifc, unit): + ifc.run("unit.add_conversion_based_unit", name="hour").should_be_called().will_return("unit") + unit.import_units().should_be_called() + assert subject.add_conversion_based_unit(ifc, unit, name="hour") == "unit" + + def test_adding_conversion_based_time_unit_day(self, ifc, unit): + ifc.run("unit.add_conversion_based_unit", name="day").should_be_called().will_return("unit") + unit.import_units().should_be_called() + assert subject.add_conversion_based_unit(ifc, unit, name="day") == "unit" + + +class TestAssignMassAndTimeUnits: + def test_assigning_mass_unit(self, ifc, unit): + ifc.run("unit.assign_unit", units=["massunit"]).should_be_called() + unit.import_units().should_be_called() + subject.assign_unit(ifc, unit, unit="massunit") + + def test_assigning_time_unit(self, ifc, unit): + ifc.run("unit.assign_unit", units=["timeunit"]).should_be_called() + unit.import_units().should_be_called() + subject.assign_unit(ifc, unit, unit="timeunit") + + def test_assigning_multiple_mass_and_time_units(self, ifc, unit): + ifc.run("unit.assign_unit", units=["massunit"]).should_be_called() + unit.import_units().should_be_called() + ifc.run("unit.assign_unit", units=["timeunit"]).should_be_called() + unit.import_units().should_be_called() + + subject.assign_unit(ifc, unit, unit="massunit") + subject.assign_unit(ifc, unit, unit="timeunit") + + +class TestUnassignMassAndTimeUnits: + def test_unassigning_mass_unit(self, ifc, unit): + ifc.run("unit.unassign_unit", units=["massunit"]).should_be_called() + unit.import_units().should_be_called() + subject.unassign_unit(ifc, unit, unit="massunit") + + def test_unassigning_time_unit(self, ifc, unit): + ifc.run("unit.unassign_unit", units=["timeunit"]).should_be_called() + unit.import_units().should_be_called() + subject.unassign_unit(ifc, unit, unit="timeunit") + + +class TestRemoveMassAndTimeUnits: + def test_removing_mass_unit(self, ifc, unit): + ifc.run("unit.remove_unit", unit="massunit").should_be_called() + unit.import_units().should_be_called() + subject.remove_unit(ifc, unit, unit="massunit") + + def test_removing_time_unit(self, ifc, unit): + ifc.run("unit.remove_unit", unit="timeunit").should_be_called() + unit.import_units().should_be_called() + subject.remove_unit(ifc, unit, unit="timeunit") diff --git a/src/bonsai/test/tool/test_unit.py b/src/bonsai/test/tool/test_unit.py index 212c72a559..190a8fa302 100644 --- a/src/bonsai/test/tool/test_unit.py +++ b/src/bonsai/test/tool/test_unit.py @@ -135,6 +135,35 @@ class TestGetSceneUnitName(NewFile): bpy.context.scene.unit_settings.system = "NONE" assert subject.get_scene_unit_name("LENGTHUNIT") == "foot" + def test_getting_mass_unit_names(self): + """Test getting mass unit names for different systems""" + assert bpy.context.scene + props = tool.Blender.get_bim_props() + props.mass_unit = "GRAM" + assert subject.get_scene_unit_name("MASSUNIT") == "gram" + props.mass_unit = "KILOGRAM" + assert subject.get_scene_unit_name("MASSUNIT") == "kilogram" + props.mass_unit = "POUND" + assert subject.get_scene_unit_name("MASSUNIT") == "pound" + props.mass_unit = "OUNCE" + assert subject.get_scene_unit_name("MASSUNIT") == "ounce" + props.mass_unit = "TONNE" + assert subject.get_scene_unit_name("MASSUNIT") == "tonne" + + def test_getting_time_unit_names(self): + """Test getting time unit names for different systems""" + assert bpy.context.scene + props = tool.Blender.get_bim_props() + + props.time_unit = "SECOND" + assert subject.get_scene_unit_name("TIMEUNIT") == "second" + props.time_unit = "MINUTE" + assert subject.get_scene_unit_name("TIMEUNIT") == "minute" + props.time_unit = "HOUR" + assert subject.get_scene_unit_name("TIMEUNIT") == "hour" + props.time_unit = "DAY" + assert subject.get_scene_unit_name("TIMEUNIT") == "day" + class TestGetSceneUnitSIPrefix: def test_run(self): @@ -162,6 +191,30 @@ class TestGetSceneUnitSIPrefix: props.volume_unit = "MILLI/CUBIC_METRE" assert subject.get_scene_unit_si_prefix("VOLUMEUNIT") == "MILLI" + def test_mass_and_time_unit_prefixes(self): + assert bpy.context.scene + props = tool.Blender.get_bim_props() + + props.mass_unit = "KILOGRAM" + assert subject.get_scene_unit_si_prefix("MASSUNIT") == "KILO" + props.mass_unit = "GRAM" + assert subject.get_scene_unit_si_prefix("MASSUNIT") is None + props.mass_unit = "POUND" + assert subject.get_scene_unit_si_prefix("MASSUNIT") == "CONVERSION" + props.mass_unit = "OUNCE" + assert subject.get_scene_unit_si_prefix("MASSUNIT") == "CONVERSION" + props.mass_unit = "TONNE" + assert subject.get_scene_unit_si_prefix("MASSUNIT") == "MEGA" + + props.time_unit = "SECOND" + assert subject.get_scene_unit_si_prefix("TIMEUNIT") is None + props.time_unit = "MINUTE" + assert subject.get_scene_unit_si_prefix("TIMEUNIT") == "CONVERSION" + props.time_unit = "HOUR" + assert subject.get_scene_unit_si_prefix("TIMEUNIT") == "CONVERSION" + props.time_unit = "DAY" + assert subject.get_scene_unit_si_prefix("TIMEUNIT") == "CONVERSION" + class TestImportUnitAttributes(NewFile): def test_importing_derived_units(self): @@ -298,6 +351,96 @@ class TestImportUnits(NewFile): assert props.units[5].unit_type == unit6.UnitType assert props.units[5].ifc_class == unit6.is_a() + def test_importing_mass_and_time_units(self): + """Test importing mass and time conversion based units""" + ifc = ifcopenshell.api.project.create_file() + tool.Ifc.set(ifc) + + tonne_unit = ifcopenshell.api.unit.add_conversion_based_unit(ifc, name="tonne") + pound_unit = ifcopenshell.api.unit.add_conversion_based_unit(ifc, name="pound") + ounce_unit = ifcopenshell.api.unit.add_conversion_based_unit(ifc, name="ounce") + + minute_unit = ifcopenshell.api.unit.add_conversion_based_unit(ifc, name="minute") + hour_unit = ifcopenshell.api.unit.add_conversion_based_unit(ifc, name="hour") + day_unit = ifcopenshell.api.unit.add_conversion_based_unit(ifc, name="day") + + kg_unit = ifcopenshell.api.unit.add_si_unit(ifc, unit_type="MASSUNIT", prefix="KILO") + gram_unit = ifcopenshell.api.unit.add_si_unit(ifc, unit_type="MASSUNIT") + second_unit = ifcopenshell.api.unit.add_si_unit(ifc, unit_type="TIMEUNIT") + + ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcProject") + ifcopenshell.api.unit.assign_unit(ifc, units=[tonne_unit, minute_unit, kg_unit]) + + subject.import_units() + props = tool.Unit.get_unit_props() + + assert len(props.units) == 15 + + unit_ids = [u.ifc_definition_id for u in props.units] + assert tonne_unit.id() in unit_ids + assert pound_unit.id() in unit_ids + assert ounce_unit.id() in unit_ids + assert minute_unit.id() in unit_ids + assert hour_unit.id() in unit_ids + assert day_unit.id() in unit_ids + assert kg_unit.id() in unit_ids + assert gram_unit.id() in unit_ids + assert second_unit.id() in unit_ids + + tonne_prop = next(u for u in props.units if u.ifc_definition_id == tonne_unit.id()) + assert tonne_prop.name == "tonne" + assert tonne_prop.unit_type == "MASSUNIT" + assert tonne_prop.is_assigned is True + assert tonne_prop.ifc_class == "IfcConversionBasedUnit" + + pound_prop = next(u for u in props.units if u.ifc_definition_id == pound_unit.id()) + assert pound_prop.name == "pound" + assert pound_prop.unit_type == "MASSUNIT" + assert pound_prop.is_assigned is False + assert pound_prop.ifc_class == "IfcConversionBasedUnit" + + ounce_prop = next(u for u in props.units if u.ifc_definition_id == ounce_unit.id()) + assert ounce_prop.name == "ounce" + assert ounce_prop.unit_type == "MASSUNIT" + assert ounce_prop.is_assigned is False + assert ounce_prop.ifc_class == "IfcConversionBasedUnit" + + kg_prop = next(u for u in props.units if u.ifc_definition_id == kg_unit.id()) + assert kg_prop.name == "KILOGRAM" + assert kg_prop.unit_type == "MASSUNIT" + assert kg_prop.is_assigned is True + assert kg_prop.ifc_class == "IfcSIUnit" + + gram_prop = next(u for u in props.units if u.ifc_definition_id == gram_unit.id()) + assert gram_prop.name == "GRAM" + assert gram_prop.unit_type == "MASSUNIT" + assert gram_prop.is_assigned is False + assert gram_prop.ifc_class == "IfcSIUnit" + + minute_prop = next(u for u in props.units if u.ifc_definition_id == minute_unit.id()) + assert minute_prop.name == "minute" + assert minute_prop.unit_type == "TIMEUNIT" + assert minute_prop.is_assigned is True + assert minute_prop.ifc_class == "IfcConversionBasedUnit" + + hour_prop = next(u for u in props.units if u.ifc_definition_id == hour_unit.id()) + assert hour_prop.name == "hour" + assert hour_prop.unit_type == "TIMEUNIT" + assert hour_prop.is_assigned is False + assert hour_prop.ifc_class == "IfcConversionBasedUnit" + + day_prop = next(u for u in props.units if u.ifc_definition_id == day_unit.id()) + assert day_prop.name == "day" + assert day_prop.unit_type == "TIMEUNIT" + assert day_prop.is_assigned is False + assert day_prop.ifc_class == "IfcConversionBasedUnit" + + second_prop = next(u for u in props.units if u.ifc_definition_id == second_unit.id()) + assert second_prop.name == "SECOND" + assert second_prop.unit_type == "TIMEUNIT" + assert second_prop.is_assigned is False + assert second_prop.ifc_class == "IfcSIUnit" + class TestIsSceneUnitMetric(NewFile): def test_run(self): diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/add_conversion_based_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/add_conversion_based_unit.py index 51aa200810..6a4f17f8fd 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/unit/add_conversion_based_unit.py +++ b/src/ifcopenshell-python/ifcopenshell/api/unit/add_conversion_based_unit.py @@ -32,7 +32,7 @@ def add_conversion_based_unit( function. You can choose from one of: inch, foot, yard, mile, square inch, square foot, square yard, acre, square mile, cubic inch, cubic foot, cubic yard, litre, fluid ounce UK, fluid ounce US, pint UK, pint - US, gallon UK, gallon US, degree, ounce, pound, ton UK, ton US, lbf, + US, gallon UK, gallon US, degree, ounce, pound, ton UK, ton US, tonne, lbf, kip, psi, ksi, minute, hour, day, btu, and fahrenheit. :param name: A converted name chosen from the list above. diff --git a/src/ifcopenshell-python/ifcopenshell/util/unit.py b/src/ifcopenshell-python/ifcopenshell/util/unit.py index cc767fe3a7..86254cb826 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/unit.py +++ b/src/ifcopenshell-python/ifcopenshell/util/unit.py @@ -209,6 +209,7 @@ si_conversions = { "pound": 0.454, "ton UK": 1016.0469088, "ton US": 907.18474, + "tonne": 1000.0, "lbf": 4.4482216153, "kip": 4448.2216153, "psi": 6894.7572932, @@ -253,6 +254,7 @@ imperial_types = { "pound": "MASSUNIT", "ton UK": "MASSUNIT", "ton US": "MASSUNIT", + "tonne": "MASSUNIT", "lbf": "FORCEUNIT", "kip": "FORCEUNIT", "psi": "PRESSUREUNIT", @@ -323,6 +325,7 @@ unit_symbols = { "pound": "lb", "ton UK": "ton", "ton US": "ton", + "tonne": "t", "lbf": "lbf", "kip": "kip", "psi": "psi", diff --git a/src/ifcopenshell-python/test/util/test_unit.py b/src/ifcopenshell-python/test/util/test_unit.py index 6e28a28e70..ff7b7eede5 100644 --- a/src/ifcopenshell-python/test/util/test_unit.py +++ b/src/ifcopenshell-python/test/util/test_unit.py @@ -391,3 +391,233 @@ class TestConvertFileLengthUnitsIFC4(test.bootstrap.IFC4, TestConvertFileLengthU class TestConvertFileLengthUnitsIFC4X3(test.bootstrap.IFC4X3, TestConvertFileLengthUnits): pass + + +class TestAddConversionBasedUnitMassAndTime(test.bootstrap.IFC4): + def test_adding_mass_units_creates_proper_massunit(self): + mass_units = [ + ("tonne", 1000.0), + ("pound", 0.454), + ("ounce", 0.02835), + ("ton UK", 1016.0469088), + ("ton US", 907.18474), + ] + + for name, expected_conversion in mass_units: + unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name=name) + + assert unit.is_a("IfcConversionBasedUnit") + + assert unit.UnitType == "MASSUNIT" + + assert unit.Name == name + + actual_conversion = unit.ConversionFactor.ValueComponent.wrappedValue + assert actual_conversion == expected_conversion + + target_unit = unit.ConversionFactor.UnitComponent + assert target_unit.is_a("IfcSIUnit") + assert target_unit.UnitType == "MASSUNIT" + assert target_unit.Name == "GRAM" + assert target_unit.Prefix == "KILO" + + def test_adding_time_units_creates_proper_timeunit(self): + time_units = [ + ("minute", 60), + ("hour", 3600), + ("day", 86400), + ] + + for name, expected_conversion in time_units: + unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name=name) + + assert unit.is_a("IfcConversionBasedUnit") + + assert unit.UnitType == "TIMEUNIT" + + assert unit.Name == name + + actual_conversion = unit.ConversionFactor.ValueComponent.wrappedValue + assert actual_conversion == expected_conversion + + target_unit = unit.ConversionFactor.UnitComponent + assert target_unit.is_a("IfcSIUnit") + assert target_unit.UnitType == "TIMEUNIT" + assert target_unit.Name == "SECOND" + assert target_unit.Prefix is None + + def test_mass_unit_integration_with_project(self): + ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") + + kg_unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="MASSUNIT", prefix="KILO") + tonne_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="tonne") + pound_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="pound") + + ifcopenshell.api.unit.assign_unit(self.file, units=[kg_unit]) + project_mass_unit = subject.get_project_unit(self.file, "MASSUNIT") + assert project_mass_unit == kg_unit + + unit_assignment = subject.get_unit_assignment(self.file) + assert unit_assignment + assigned_units = list(unit_assignment.Units or []) + assert kg_unit in assigned_units + + ifcopenshell.api.unit.assign_unit(self.file, units=[tonne_unit]) + project_mass_unit = subject.get_project_unit(self.file, "MASSUNIT") + assert project_mass_unit == tonne_unit + + unit_assignment = subject.get_unit_assignment(self.file) + assigned_units = list(unit_assignment.Units or []) + assert tonne_unit in assigned_units + + length_unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT") + ifcopenshell.api.unit.assign_unit(self.file, units=[pound_unit, length_unit]) + + unit_assignment = subject.get_unit_assignment(self.file) + assigned_units = list(unit_assignment.Units or []) + assert pound_unit in assigned_units + assert length_unit in assigned_units + + def test_time_unit_integration_with_project(self): + ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") + + second_unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="TIMEUNIT") + minute_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="minute") + hour_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="hour") + + ifcopenshell.api.unit.assign_unit(self.file, units=[second_unit]) + project_time_unit = subject.get_project_unit(self.file, "TIMEUNIT") + assert project_time_unit == second_unit + + unit_assignment = subject.get_unit_assignment(self.file) + assert unit_assignment + assigned_units = list(unit_assignment.Units or []) + assert second_unit in assigned_units + + ifcopenshell.api.unit.assign_unit(self.file, units=[minute_unit]) + project_time_unit = subject.get_project_unit(self.file, "TIMEUNIT") + assert project_time_unit == minute_unit + + unit_assignment = subject.get_unit_assignment(self.file) + assigned_units = list(unit_assignment.Units or []) + assert minute_unit in assigned_units + + length_unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT") + ifcopenshell.api.unit.assign_unit(self.file, units=[hour_unit, length_unit]) + + unit_assignment = subject.get_unit_assignment(self.file) + assigned_units = list(unit_assignment.Units or []) + assert hour_unit in assigned_units + assert length_unit in assigned_units + + def test_multiple_unit_types_can_coexist(self): + ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") + + kg_unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="MASSUNIT", prefix="KILO") + minute_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="minute") + meter_unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT") + + ifcopenshell.api.unit.assign_unit(self.file, units=[kg_unit, minute_unit, meter_unit]) + + unit_assignment = subject.get_unit_assignment(self.file) + assigned_units = list(unit_assignment.Units or []) + + assert kg_unit in assigned_units + assert minute_unit in assigned_units + assert meter_unit in assigned_units + + assert subject.get_project_unit(self.file, "MASSUNIT") == kg_unit + assert subject.get_project_unit(self.file, "TIMEUNIT") == minute_unit + assert subject.get_project_unit(self.file, "LENGTHUNIT") == meter_unit + + def test_unknown_mass_or_time_units_fall_back_to_userdefined(self): + unknown_mass_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="unknown_mass_unit") + assert unknown_mass_unit.UnitType == "USERDEFINED" + + unknown_time_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="unknown_time_unit") + assert unknown_time_unit.UnitType == "USERDEFINED" + + def test_mass_units_individually(self): + tonne_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="tonne") + assert tonne_unit.UnitType == "MASSUNIT" + assert tonne_unit.ConversionFactor.ValueComponent.wrappedValue == 1000.0 + + pound_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="pound") + assert pound_unit.UnitType == "MASSUNIT" + assert pound_unit.ConversionFactor.ValueComponent.wrappedValue == 0.454 + + ounce_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="ounce") + assert ounce_unit.UnitType == "MASSUNIT" + assert ounce_unit.ConversionFactor.ValueComponent.wrappedValue == 0.02835 + + def test_time_units_individually(self): + minute_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="minute") + assert minute_unit.UnitType == "TIMEUNIT" + assert minute_unit.ConversionFactor.ValueComponent.wrappedValue == 60 + + hour_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="hour") + assert hour_unit.UnitType == "TIMEUNIT" + assert hour_unit.ConversionFactor.ValueComponent.wrappedValue == 3600 + + day_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="day") + assert day_unit.UnitType == "TIMEUNIT" + assert day_unit.ConversionFactor.ValueComponent.wrappedValue == 86400 + + +class TestMassAndTimeUnitDictionaries(test.bootstrap.IFC4): + def test_mass_units_in_imperial_types(self): + expected_mass_units = ["ounce", "pound", "ton UK", "ton US", "tonne"] + + def test_mass_units_in_imperial_types(self): + expected_mass_units = ["ounce", "pound", "ton UK", "ton US", "tonne"] + + for unit_name in expected_mass_units: + assert unit_name in subject.imperial_types + assert subject.imperial_types[unit_name] == "MASSUNIT" + + def test_time_units_in_imperial_types(self): + expected_time_units = ["minute", "hour", "day"] + + for unit_name in expected_time_units: + assert unit_name in subject.imperial_types + assert subject.imperial_types[unit_name] == "TIMEUNIT" + + def test_mass_units_have_conversion_factors(self): + expected_mass_conversions = { + "ounce": 0.02835, + "pound": 0.454, + "ton UK": 1016.0469088, + "ton US": 907.18474, + "tonne": 1000.0, + } + + for unit_name, expected_factor in expected_mass_conversions.items(): + assert unit_name in subject.si_conversions + assert subject.si_conversions[unit_name] == expected_factor + + def test_time_units_have_conversion_factors(self): + expected_time_conversions = { + "minute": 60, + "hour": 3600, + "day": 86400, + } + + for unit_name, expected_factor in expected_time_conversions.items(): + assert unit_name in subject.si_conversions + assert subject.si_conversions[unit_name] == expected_factor + + def test_mass_and_time_units_have_symbols(self): + expected_symbols = { + "ounce": "oz", + "pound": "lb", + "ton UK": "ton", + "ton US": "ton", + "tonne": "t", + "minute": "min", + "hour": "hr", + "day": "day", + } + + for unit_name, expected_symbol in expected_symbols.items(): + assert unit_name in subject.unit_symbols + assert subject.unit_symbols[unit_name] == expected_symbol