Skip to content

Commit 1347ea9

Browse files
authored
Merge pull request #29 from AlexisOlson/master
Add script to create minimized version of a .bim file
2 parents d178714 + 5d1cca2 commit 1347ea9

File tree

2 files changed

+550
-0
lines changed

2 files changed

+550
-0
lines changed

Intermediate/bim_slimmer.csx

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
//
2+
// Title: BIM Slimmer - strip metadata bloat to reduce file size
3+
//
4+
// Author: Alexis Olson using GPT-5 Thinking and Claude Opus 4.1
5+
//
6+
// Description:
7+
// Opens a Tabular model .bim file, removes UI/engine bloat while preserving model semantics,
8+
// and saves a sibling .slim JSON. Preserves structural containers (model, tables, columns,
9+
// measures, partitions, relationships, etc.), removes empty/unused values, and supports
10+
// optional switches for display/query group metadata. Shows a summary with items removed and
11+
// size savings.
12+
//
13+
// How to use:
14+
// - Use in Tabular Editor (2 or 3) Advanced Scripting.
15+
// - (Optional) Customize the configuration options at the top of the script.
16+
// - When prompted, select a <ModelName>.bim file.
17+
// - The script writes <ModelName>.slim and displays a summary.
18+
19+
using System;
20+
using System.Collections.Generic;
21+
using System.IO;
22+
using System.Linq;
23+
using System.Windows.Forms;
24+
using Newtonsoft.Json;
25+
using Newtonsoft.Json.Linq;
26+
27+
// ==================== CONFIGURATION ====================
28+
// Use defaults specified or customize as needed
29+
30+
// Core metadata removal (recommended: ON)
31+
bool REMOVE_Annotations = true; // annotations, changedProperties, extendedProperties
32+
bool REMOVE_Lineage = true; // lineageTag, sourceLineageTag
33+
bool REMOVE_LanguageData = true; // cultures, translations, synonyms, linguisticMetadata
34+
35+
// Value-based cleanup (recommended: ON)
36+
bool REMOVE_DefaultValues = true; // dataCategory:Uncategorized, summarizeBy:none
37+
bool REMOVE_RedundantNames = true; // sourceColumn==name, displayName==name
38+
bool REMOVE_EmptyContainers = true; // empty {} and [] (preserves structural containers)
39+
40+
// Presentation properties (optional)
41+
bool REMOVE_SummarizeBy = true; // summarizeBy (all values, not just none)
42+
bool REMOVE_DisplayProps = true; // isHidden, displayFolder
43+
bool REMOVE_QueryGroups = false; // queryGroup, queryGroups, folder
44+
bool REMOVE_FormatString = true; // formatString literal only (NEVER formatStringDefinition)
45+
46+
// Additional metadata (recommended: ON)
47+
bool REMOVE_ExtraMetadata = true; // sourceProviderType, isNameInferred, isDataTypeInferred
48+
49+
// ==================== OUTPUT FORMAT =====================
50+
// Human-friendly indented JSON (false) or compacted (true)
51+
bool MINIFY_OUTPUT = true;
52+
53+
// ==================== MAIN EXECUTION ====================
54+
try
55+
{
56+
// Select file
57+
string inputPath;
58+
using (var dialog = new OpenFileDialog {
59+
Title = "Select BIM file to slim",
60+
Filter = "Tabular Model (*.bim)|*.bim|All files (*.*)|*.*",
61+
RestoreDirectory = true
62+
}) {
63+
if (dialog.ShowDialog() != DialogResult.OK) return;
64+
inputPath = dialog.FileName;
65+
}
66+
67+
// Generate output path
68+
var outputPath = Path.ChangeExtension(inputPath, ".slim");
69+
var originalSize = new FileInfo(inputPath).Length;
70+
71+
// Parse JSON
72+
var root = JToken.Parse(File.ReadAllText(inputPath));
73+
74+
// Build removal rules
75+
var dropKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
76+
if (REMOVE_Annotations) {
77+
dropKeys.UnionWith(new[] { "annotations", "changedProperties", "extendedProperties" });
78+
}
79+
if (REMOVE_Lineage) {
80+
dropKeys.UnionWith(new[] { "lineageTag", "sourceLineageTag" });
81+
}
82+
if (REMOVE_LanguageData) {
83+
dropKeys.UnionWith(new[] { "cultures", "translations", "synonyms", "linguisticMetadata" });
84+
}
85+
if (REMOVE_SummarizeBy) {
86+
dropKeys.Add("summarizeBy");
87+
}
88+
if (REMOVE_DisplayProps) {
89+
dropKeys.UnionWith(new[] { "isHidden", "displayFolder" });
90+
}
91+
if (REMOVE_QueryGroups) {
92+
dropKeys.UnionWith(new[] { "queryGroup", "queryGroups", "folder" });
93+
}
94+
if (REMOVE_ExtraMetadata) {
95+
dropKeys.UnionWith(new[] { "sourceProviderType", "isNameInferred", "isDataTypeInferred" });
96+
}
97+
98+
// Structural containers - never remove even if empty (preserves model schema)
99+
var preserve = new HashSet<string>(new[] {
100+
"model", "tables", "columns", "measures", "relationships", "partitions",
101+
"roles", "hierarchies", "levels", "dataSources", "perspectives", "expressions"
102+
}, StringComparer.OrdinalIgnoreCase);
103+
104+
// Track removals
105+
var stats = new Dictionary<string, int>();
106+
Action<string> Track = delegate(string key) {
107+
stats[key] = stats.ContainsKey(key) ? stats[key] + 1 : 1;
108+
};
109+
110+
// Helpers
111+
Func<string, string, bool> Eq = delegate(string a, string b) {
112+
return string.Equals(
113+
a != null ? a.Trim() : null,
114+
b != null ? b.Trim() : null
115+
);
116+
};
117+
118+
Func<JToken, bool> IsEmpty = delegate(JToken t) {
119+
return t == null || t.Type == JTokenType.Null ||
120+
(t is JContainer && !((JContainer)t).HasValues) ||
121+
(t.Type == JTokenType.String && string.IsNullOrWhiteSpace((string)t));
122+
};
123+
124+
// Recursive cleaner
125+
Action<JToken> Clean = null;
126+
Clean = delegate(JToken token) {
127+
if (token == null) return;
128+
129+
if (token.Type == JTokenType.Object) {
130+
var obj = (JObject)token;
131+
132+
// Recurse first (depth-first)
133+
foreach (var prop in obj.Properties().ToList()) Clean(prop.Value);
134+
135+
var toRemove = new List<JProperty>();
136+
137+
foreach (var prop in obj.Properties()) {
138+
// Name-based removals
139+
if (dropKeys.Contains(prop.Name)) {
140+
toRemove.Add(prop);
141+
Track(prop.Name);
142+
continue;
143+
}
144+
145+
// formatString special handling (protect formatStringDefinition)
146+
if (REMOVE_FormatString && Eq(prop.Name, "formatString")) {
147+
toRemove.Add(prop);
148+
Track("formatString");
149+
continue;
150+
}
151+
152+
// Empty container removal (with structural preservation)
153+
if (REMOVE_EmptyContainers && IsEmpty(prop.Value) && !preserve.Contains(prop.Name)) {
154+
toRemove.Add(prop);
155+
Track("empty");
156+
continue;
157+
}
158+
}
159+
160+
// Value-based removals (checked after structure scan)
161+
if (REMOVE_DefaultValues) {
162+
var dc = obj.Property("dataCategory");
163+
if (dc != null && dc.Value is JValue && Eq((string)dc.Value, "Uncategorized")) {
164+
toRemove.Add(dc);
165+
Track("dataCategory=default");
166+
}
167+
168+
var sb = obj.Property("summarizeBy");
169+
if (sb != null && sb.Value is JValue && Eq((string)sb.Value, "none")) {
170+
toRemove.Add(sb);
171+
Track("summarizeBy=none");
172+
}
173+
}
174+
175+
if (REMOVE_RedundantNames) {
176+
var name = obj.Property("name");
177+
if (name != null && name.Value is JValue) {
178+
var nameStr = (string)name.Value;
179+
var nameBracketed = nameStr != null ? string.Format("[{0}]", nameStr) : null;
180+
181+
var src = obj.Property("sourceColumn");
182+
if (
183+
src != null &&
184+
src.Value is JValue &&
185+
(
186+
Eq((string)src.Value, nameStr) ||
187+
Eq((string)src.Value, nameBracketed)
188+
)
189+
) {
190+
toRemove.Add(src);
191+
Track("sourceColumn=name");
192+
}
193+
194+
var disp = obj.Property("displayName");
195+
if (
196+
disp != null &&
197+
disp.Value is JValue &&
198+
(
199+
Eq((string)disp.Value, nameStr) ||
200+
Eq((string)disp.Value, nameBracketed)
201+
)
202+
) {
203+
toRemove.Add(disp);
204+
Track("displayName=name");
205+
}
206+
}
207+
}
208+
209+
// Apply all removals
210+
foreach (var prop in toRemove.Distinct()) prop.Remove();
211+
}
212+
else if (token.Type == JTokenType.Array) {
213+
var arr = (JArray)token;
214+
foreach (var item in arr.ToList()) {
215+
Clean(item);
216+
if (REMOVE_EmptyContainers && IsEmpty(item)) {
217+
item.Remove();
218+
Track("empty");
219+
}
220+
}
221+
}
222+
};
223+
224+
// Execute cleaning
225+
Clean(root);
226+
227+
// Save result
228+
var formatting = MINIFY_OUTPUT ? Formatting.None : Formatting.Indented;
229+
File.WriteAllText(outputPath, root.ToString(formatting));
230+
231+
// Report results
232+
var newSize = new FileInfo(outputPath).Length;
233+
var reduction = (1.0 - (double)newSize / originalSize) * 100;
234+
var summary =
235+
"BIM Slimmer Results\n" +
236+
"==================\n" +
237+
string.Format("Input: {0} ({1:N1} KB)\n", Path.GetFileName(inputPath), originalSize / 1024.0) +
238+
string.Format("Output: {0} ({1:N1} KB)\n", Path.GetFileName(outputPath), newSize / 1024.0) +
239+
string.Format("Saved: {0:F1}%\n\n", reduction) +
240+
string.Format("Removed: {0:N0} items\n", stats.Values.Sum()) +
241+
string.Join(
242+
"\n",
243+
stats.OrderBy(k => k.Key)
244+
.Select(k => string.Format(" • {0}: {1:N0}", k.Key, k.Value))
245+
.ToArray()
246+
);
247+
248+
Info(summary);
249+
}
250+
catch (Exception ex)
251+
{
252+
Error(string.Format("Processing failed: {0}", ex.Message));
253+
}

0 commit comments

Comments
 (0)