diff --git a/src/blenderbim/blenderbim/bim/module/csv/operator.py b/src/blenderbim/blenderbim/bim/module/csv/operator.py index c1b63e4dd3..daa85fbab9 100644 --- a/src/blenderbim/blenderbim/bim/module/csv/operator.py +++ b/src/blenderbim/blenderbim/bim/module/csv/operator.py @@ -101,6 +101,7 @@ class ImportCsvAttributes(bpy.types.Operator): new.sort = attribute["sort"] new.group = attribute["group"] new.summary = attribute["summary"] + new.formatting = attribute["formatting"] return {"FINISHED"} def invoke(self, context, event): @@ -123,7 +124,14 @@ class ExportCsvAttributes(bpy.types.Operator): data = { "query": tool.Search.export_filter_query(props.filter_groups), "attributes": [ - {"name": a.name, "header": a.header, "sort": a.sort, "group": a.group, "summary": a.summary} + { + "name": a.name, + "header": a.header, + "sort": a.sort, + "group": a.group, + "summary": a.summary, + "formatting": a.formatting, + } for a in props.csv_attributes ], } @@ -173,6 +181,7 @@ class ExportIfcCsv(bpy.types.Operator): sort = [] groups = [] summaries = [] + formatting = [] for attribute in props.csv_attributes: if attribute.sort != "NONE": sort.append({"name": attribute.name, "order": attribute.sort}) @@ -181,6 +190,9 @@ class ExportIfcCsv(bpy.types.Operator): if attribute.summary != "NONE": summaries.append({"name": attribute.name, "type": attribute.summary}) + if attribute.formatting != "{{value}}" and "{{value}}" in attribute.formatting: + formatting.append({"name": attribute.name, "format": attribute.formatting}) + sep = props.csv_custom_delimiter if props.csv_delimiter == "CUSTOM" else props.csv_delimiter ifc_csv.export( ifc_file, @@ -198,6 +210,7 @@ class ExportIfcCsv(bpy.types.Operator): sort=sort, groups=groups, summaries=summaries, + formatting=formatting, ) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/csv/prop.py b/src/blenderbim/blenderbim/bim/module/csv/prop.py index 31a334e389..db1ae07071 100644 --- a/src/blenderbim/blenderbim/bim/module/csv/prop.py +++ b/src/blenderbim/blenderbim/bim/module/csv/prop.py @@ -58,6 +58,7 @@ class CsvAttribute(PropertyGroup): ("MAX", "Max", "Gets the maximum value of all rows"), ] ) + formatting: StringProperty(default="{{value}}", name="Formatting") class CsvProperties(PropertyGroup): @@ -94,4 +95,5 @@ class CsvProperties(PropertyGroup): should_show_sort: BoolProperty(default=False, name="Show Sorting") should_show_group: BoolProperty(default=False, name="Show Grouping") should_show_summary: BoolProperty(default=False, name="Show Summary") + should_show_formatting: BoolProperty(default=False, name="Show Formatting") should_load_from_memory: BoolProperty(default=False, name="Load from Memory") diff --git a/src/blenderbim/blenderbim/bim/module/csv/ui.py b/src/blenderbim/blenderbim/bim/module/csv/ui.py index c1e5c93222..425bb2cb8d 100644 --- a/src/blenderbim/blenderbim/bim/module/csv/ui.py +++ b/src/blenderbim/blenderbim/bim/module/csv/ui.py @@ -23,7 +23,7 @@ from blenderbim.bim.module.search.data import SearchData class BIM_PT_ifccsv(Panel): - bl_label = "CSV Import/Export" + bl_label = "Spreadsheet Import/Export" bl_idname = "BIM_PT_ifccsv" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" @@ -86,6 +86,7 @@ class BIM_PT_ifccsv(Panel): row.prop(props, "should_show_sort", icon="SORTSIZE", text="") row.prop(props, "should_show_group", icon="OUTLINER_COLLECTION", text="") row.prop(props, "should_show_summary", icon="SYNTAX_ON", text="") + row.prop(props, "should_show_formatting", icon="CON_TRANSLIKE", text="") total = len(props.csv_attributes) for index, attribute in enumerate(props.csv_attributes): @@ -100,6 +101,8 @@ class BIM_PT_ifccsv(Panel): row.prop(attribute, "varies_value", text="") if props.should_show_summary: row.prop(attribute, "summary", text="") + if props.should_show_formatting: + row.prop(attribute, "formatting", text="") if total > 1: if index != 0: op = row.operator(f"bim.reorder_csv_attribute", icon="TRIA_UP", text="") diff --git a/src/ifccsv/ifccsv.py b/src/ifccsv/ifccsv.py index 621f2d6595..ae5c03ef47 100755 --- a/src/ifccsv/ifccsv.py +++ b/src/ifccsv/ifccsv.py @@ -75,6 +75,7 @@ class IfcCsv: sort=None, groups=None, summaries=None, + formatting=None, ): self.ifc_file = ifc_file self.results = [] @@ -116,6 +117,7 @@ class IfcCsv: self.group_results(groups, attributes) self.summarise_results(summaries, attributes) self.sort_results(sort, attributes, include_global_id) + self.format_results(formatting, attributes, null) if format == "csv": self.export_csv(output, delimiter=delimiter) @@ -225,7 +227,27 @@ class IfcCsv: self.summaries[si] = max(summary_values[si]) self.summaries[si] = summary_type.title() + ": " + str(self.summaries[si]) + def format_results(self, formatting, attributes, null): + if not formatting: + return + + formatting_indices = {} + + for data in formatting: + index = attributes.index(data["name"]) + formatting_indices[index] = data["format"] + + for row in self.results: + for index, format_query in formatting_indices.items(): + if row[index] == null: + continue + if not isinstance(row[index], str): + row[index] = '"' + str(row[index]) + '"' + row[index] = ifcopenshell.util.selector.format(format_query.replace("{{value}}", row[index])) + def sort_results(self, sort, attributes, include_global_id): + if not self.results: + return if sort: def natural_sort(value): if isinstance(value, str): @@ -240,7 +262,10 @@ class IfcCsv: reverse = sort_data["order"] == "DESC" self.results = sorted(self.results, key=lambda x: natural_sort(x[i]), reverse=reverse) else: - self.results = sorted(self.results, key=lambda x: x[1 if include_global_id else 0]) + if include_global_id and len(self.results[0]) > 1: + self.results = sorted(self.results, key=lambda x: x[1]) + elif not include_global_id: + self.results = sorted(self.results, key=lambda x: x[0]) def export_csv(self, output, delimiter=None): with open(output, "w", newline="", encoding="utf-8") as f: diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py index 097ea409ff..0acd0aa44e 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/selector.py +++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py @@ -20,6 +20,7 @@ import re import lark import ifcopenshell.util import ifcopenshell.util.fm +import ifcopenshell.util.unit import ifcopenshell.util.element import ifcopenshell.util.classification @@ -112,6 +113,106 @@ get_element_grammar = lark.Lark( """ ) +format_grammar = lark.Lark( + """start: function + + function: round | format_length | lower | upper | title | concat | ESCAPED_STRING | NUMBER + + round: "round(" function "," NUMBER ")" + format_length: metric_length | imperial_length + metric_length: "metric_length(" function "," NUMBER "," NUMBER ")" + imperial_length: "imperial_length(" function "," NUMBER ["," ESCAPED_STRING] ")" + lower: "lower(" function ")" + upper: "upper(" function ")" + title: "title(" function ")" + concat: "concat(" function ("," function)* ")" + + // Embed common.lark for packaging + DIGIT: "0".."9" + HEXDIGIT: "a".."f"|"A".."F"|DIGIT + INT: DIGIT+ + SIGNED_INT: ["+"|"-"] INT + DECIMAL: INT "." INT? | "." INT + _EXP: ("e"|"E") SIGNED_INT + FLOAT: INT _EXP | DECIMAL _EXP? + SIGNED_FLOAT: ["+"|"-"] FLOAT + NUMBER: FLOAT | INT + SIGNED_NUMBER: ["+"|"-"] NUMBER + _STRING_INNER: /.*?/ + _STRING_ESC_INNER: _STRING_INNER /(?. from math import pi +from fractions import Fraction prefixes = { "EXA": 1e18, diff --git a/src/ifcopenshell-python/test/util/test_selector.py b/src/ifcopenshell-python/test/util/test_selector.py index 67749417bd..650e6e2c99 100644 --- a/src/ifcopenshell-python/test/util/test_selector.py +++ b/src/ifcopenshell-python/test/util/test_selector.py @@ -22,6 +22,32 @@ import ifcopenshell.api import ifcopenshell.util.selector as subject +class TestFormat(): + def test_no_formatting(self): + assert subject.format("123") == "123" + assert subject.format('\"123\"') == "123" + assert subject.format('\"foo\"') == "foo" + + def test_string_formatting(self): + assert subject.format('upper(\"fOo\")') == "FOO" + assert subject.format('lower(\"fOo\")') == "foo" + assert subject.format('title(\"fOo\")') == "Foo" + assert subject.format('concat(\"fOo\", \"bar\")') == "fOobar" + assert subject.format('upper(concat(\"fOo\", \"bar\"))') == "FOOBAR" + + def test_number_formatting(self): + assert subject.format("round(123, 5)") == "125.0" + assert subject.format('round(\"123\", 5)') == "125.0" + assert subject.format('metric_length(123, 5, 2)') == "125.00" + assert subject.format('metric_length(123.123, 0.1, 2)') == "123.10" + assert subject.format('metric_length(\"123\", 5, 2)') == "125.00" + assert subject.format('imperial_length(1, 1)') == "1'" + assert subject.format('imperial_length(3.123, 1)') == "3' - 1\"" + assert subject.format('imperial_length(3.123, 2)') == "3' - 1 1/2\"" + assert subject.format('imperial_length(\"3.123\", 2)') == "3' - 1 1/2\"" + assert subject.format('imperial_length(\"123.123\", 2, \"inch\")') == "10' - 3\"" + + class TestGetElementValue(test.bootstrap.IFC4): def test_selecting_an_elements_class_or_id_using_a_query(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")