From 974f35e02c0cb2f7510fa8f233a26eb47e124655 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morten=20L=C3=B8nskov?= Date: Thu, 7 Dec 2023 13:38:38 +0100 Subject: [PATCH] Updated Scripts and Macros (#75) * Updated note * Adjusted scripts in Macroactions to fit script library * Added scripts from MacroActions * Updated note on SymLink * Updated table groups script * Udpated MacroFile to fit script library * Name change for script files * Updates table groups script * Fixed uncomplete sentence * Fix script remove measures with outdatet method --- assets/file-types/MacroActions.json | 76 ++++++++++++++----- .../script-remove-measures-with-error.md | 7 +- .../Beginner/script-count-things.md | 2 +- .../Beginner/script-create-measure-table.md | 24 ++++++ ...cript-create-sum-measures-from-columns.md} | 2 +- .../Beginner/script-create-table-groups.md | 55 ++++++++++++++ .../script-format-numeric-measures.md | 44 +++++++++++ common/toc.md | 10 ++- te3/features/supported-files.md | 1 + te3/features/table-groups.md | 16 ++-- 10 files changed, 204 insertions(+), 33 deletions(-) create mode 100644 common/CSharpScripts/Beginner/script-create-measure-table.md rename common/CSharpScripts/Beginner/{create-sum-measures-from-columns.md => script-create-sum-measures-from-columns.md} (97%) create mode 100644 common/CSharpScripts/Beginner/script-create-table-groups.md create mode 100644 common/CSharpScripts/Beginner/script-format-numeric-measures.md diff --git a/assets/file-types/MacroActions.json b/assets/file-types/MacroActions.json index 5f24a466..2d552e99 100644 --- a/assets/file-types/MacroActions.json +++ b/assets/file-types/MacroActions.json @@ -1,55 +1,55 @@ { "Actions": [ { - "Id": 3, - "Name": "Formatting\\Format numeric measures", + "Id": 1, + "Name": "Formatting\\Beginner\\Format Numeric Measures", "Enabled": "true", - "Execute": "// This script is meant to format all measures with a default formatstring\nforeach (var ms in Selected.Measures) {\n\n\tif (ms.IsHidden) continue;\n\n\tif (!string.IsNullOrWhiteSpace(ms.FormatString)) continue;\n\n\tif (ms.DataType == DataType.Int64) ms.FormatString = \"#,##0\";\n\n\tif (ms.DataType == DataType.Double || ms.DataType == DataType.Decimal) {\n\t\tif (ms.Name.Contains(\"#\")\n\t\t\t|| ms.Name.IndexOf(\"QTY\", StringComparison.OrdinalIgnoreCase) >= 0) ms.FormatString = \"#,##0\";\n\t\telse ms.FormatString = \"#,##0.00\";\n\t}\n}", + "Execute": "// This script is meant to format all measures with a default formatstring\nforeach (var ms in Selected.Measures) {\n//Don't set format string on hidden measures\n\tif (ms.IsHidden) continue;\n// If the format string is empty continue. \n\tif (!string.IsNullOrWhiteSpace(ms.FormatString)) continue;\n//If the data type is int set a whole number format string\n\tif (ms.DataType == DataType.Int64) ms.FormatString = \"#,##0\";\n//If the datatype is double or decimal \n\tif (ms.DataType == DataType.Double || ms.DataType == DataType.Decimal) {\n //and the name contains # or QTY then set the format string to a whole number\n\t\tif (ms.Name.Contains(\"#\")\n\t\t\t|| ms.Name.IndexOf(\"QTY\", StringComparison.OrdinalIgnoreCase) >= 0) ms.FormatString = \"#,##0\";\n\t\t//otherwise set it a decimal format string. \n else ms.FormatString = \"#,##0.00\";\n\t}\n}", "Tooltip": "", "ValidContexts": "Measure" }, { - "Id": 5, - "Name": "Modelling\\Power BI\\Configure Incremental refresh policy", + "Id": 2, + "Name": "Modelling\\Advanced\\Power BI\\Configure Incremental refresh policy", "Enabled": "true", "Execute": "// This script will automatically generate an Incremental Refresh policy for a selected table\n// It is generated based on the selected column\n// It requires input from the user with a dialogue pop-up box.\nusing System.Drawing;\nusing System.Windows.Forms;\n\n// Hide the 'Running Macro' spinbox\nScriptHelper.WaitFormVisible = false;\n\n// Initialize Variables\nTable _Table = Model.Tables[0];\nstring _MExpression = \"\";\nColumn _Column = Model.AllColumns.ToList()[0];\nstring _ColumnName = \"\";\nDataType _ColumnDataType = DataType.DateTime;\n\ntry\n { \n // Select a Table for which you will configure Incremental Refresh.\n // The Refresh Policy will be enabled and configured for this table.\n _Table = \n Model.Tables.Where(\n\n // Exclude tables that already have a refresh policy\n t => \n t.EnableRefreshPolicy != true && \n\n // Include only 'Table' objects\n t.ObjectType == ObjectType.Table && \n\n // Exclude Calculated Tables\n t.Columns[0].Type != ColumnType.CalculatedTableColumn && \n\n // Exclude tables that have columns on the 'From' end of a Relationship\n t.Columns.Any(c => Model.Relationships.Any(r => r.FromColumn == c) ) && \n\n // Exclude tables that don't have a DateTime or Integer column\n (\n t.Columns.Any(c => c.DataType == DataType.DateTime) || \n t.Columns.Any(c => c.DataType == DataType.Int64)\n )\n ).SelectTable(null,\"Select a Table for which you will configure Incremental Refresh:\");\n \n _MExpression = _Table.Partitions[0].Expression;\n \n try\n {\n // Select the column to apply the Refresh Policy. \n // The M Expression will be modified using the name of this column.\n _Column = \n _Table.Columns.Where(\n\n // Include only DateTime or Int columns\n c => \n c.DataType == DataType.DateTime || \n c.DataType == DataType.Int64\n\n ).SelectColumn(null, \"Select a DateTime or DateKey (Int) Column to apply the Refresh Policy.\");\n \n _ColumnName = _Column.DaxObjectName;\n _ColumnDataType = _Column.DataType;\n \n try \n { // Test if 'RangeStart' exists\n Model.Expressions.Contains(Model.Expressions[\"RangeStart\"]);\n Info (\"RangeStart already exists!\");\n }\n catch\n {\n // Add RangeStart parameter\n Model.AddExpression( \n \"RangeStart\", \n @\"\n #datetime(2023, 01, 01, 0, 0, 0) meta\n [\n IsParameterQuery = true,\n IsParameterQueryRequired = true,\n Type = type datetime\n ]\"\n );\n \n // Success message for adding 'RangeStart'\n Info ( \"Created 'RangeStart' M Parameter!\" );\n }\n \n // Test if the RangeEnd parameter exists\n try \n { // Test if 'RangeEnd' exists\n Model.Expressions.Contains(Model.Expressions[\"RangeEnd\"]);\n Info (\"RangeEnd already exists!\");\n }\n catch\n {\n // Add RangeEnd parameter\n Model.AddExpression( \n \"RangeEnd\", \n @\"\n #datetime(2023, 31, 01, 0, 0, 0) meta\n [\n IsParameterQuery = true,\n IsParameterQueryRequired = true,\n Type = type datetime\n ]\"\n );\n \n // Success message for adding 'RangeEnd'\n Info ( \"Created 'RangeEnd' M Parameter!\" );\n \n }\n \n // Incremental Refresh Configuration\n // Input box config\n Font _fontConfig = new Font(\"Segoe UI\", 11);\n \n // Label for how long data should be stored\n var storeDataLabel = new Label();\n storeDataLabel.Text = \"Store data in the last:\";\n storeDataLabel.Location = new Point(20, 20);\n storeDataLabel.AutoSize = true;\n storeDataLabel.Font = _fontConfig;\n \n // User input for how long data should be stored\n var storeDataTextBox = new TextBox();\n storeDataTextBox.Location = new Point(storeDataLabel.Location.X + TextRenderer.MeasureText(storeDataLabel.Text, storeDataLabel.Font).Width + 20, storeDataLabel.Location.Y);\n storeDataTextBox.Size = new Size(100, 20);\n storeDataTextBox.Text = \"3\";\n storeDataTextBox.Font = _fontConfig;\n \n // User selection for how long data should be stored (granularity)\n var storeDataComboBox = new ComboBox();\n storeDataComboBox.Location = new Point(storeDataTextBox.Location.X + storeDataTextBox.Width + 20, storeDataLabel.Location.Y);\n storeDataComboBox.Size = new Size(100, 20);\n storeDataComboBox.DropDownStyle = ComboBoxStyle.DropDownList;\n storeDataComboBox.Items.AddRange(new object[] { \"days\", \"months\", \"quarters\", \"years\" });\n storeDataComboBox.SelectedIndex = 3;\n storeDataComboBox.Font = _fontConfig;\n \n // Label for how much data should be refreshed\n var refreshDataLabel = new Label();\n refreshDataLabel.Text = \"Refresh data in the last:\";\n refreshDataLabel.Location = new Point(20, storeDataLabel.Location.Y + storeDataLabel.Height + 15);\n refreshDataLabel.AutoSize = true;\n refreshDataLabel.Font = _fontConfig;\n \n // User input for how much data should be refreshed\n var refreshDataTextBox = new TextBox();\n refreshDataTextBox.Location = new Point(storeDataTextBox.Location.X, refreshDataLabel.Location.Y);\n refreshDataTextBox.Size = new Size(100, 20);\n refreshDataTextBox.Text = \"30\";\n refreshDataTextBox.Font = _fontConfig;\n \n // User selection for how much data should be refreshed (Period)\n var refreshDataComboBox = new ComboBox();\n refreshDataComboBox.Location = new Point(storeDataComboBox.Location.X, refreshDataLabel.Location.Y);\n refreshDataComboBox.Size = new Size(100, 20);\n refreshDataComboBox.DropDownStyle = ComboBoxStyle.DropDownList;\n refreshDataComboBox.Items.AddRange(new object[] { \"days\", \"months\", \"quarters\", \"years\" });\n refreshDataComboBox.SelectedIndex = 0;\n refreshDataComboBox.Font = _fontConfig;\n \n // User input to refresh full periods or not\n var fullPeriodsCheckBox = new CheckBox();\n fullPeriodsCheckBox.Text = \"Refresh only full periods\";\n fullPeriodsCheckBox.Location = new Point(storeDataLabel.Location.X + 3, refreshDataLabel.Location.Y + refreshDataLabel.Height + 15);\n fullPeriodsCheckBox.AutoSize = true;\n fullPeriodsCheckBox.Font = _fontConfig;\n \n // Form OK button\n var okButton = new Button();\n okButton.Text = \"OK\";\n okButton.Location = new Point(storeDataLabel.Location.X, fullPeriodsCheckBox.Location.Y + fullPeriodsCheckBox.Height + 15);\n okButton.MinimumSize = new Size(80, 25);\n okButton.AutoSize = true;\n okButton.DialogResult = DialogResult.OK;\n okButton.Font = _fontConfig;\n \n // Form cancel button\n var cancelButton = new Button();\n cancelButton.Text = \"Cancel\";\n cancelButton.Location = new Point(okButton.Location.X + okButton.Width + 10, okButton.Location.Y);\n cancelButton.MinimumSize = new Size(80, 25);\n cancelButton.AutoSize = true;\n cancelButton.DialogResult = DialogResult.Cancel;\n cancelButton.Font = _fontConfig;\n \n // Adjust the Location of the storeDataLabel to align with the storeDataTextBox\n storeDataLabel.Location = new Point(storeDataLabel.Location.X, storeDataLabel.Location.Y + 4);\n refreshDataLabel.Location = new Point(refreshDataLabel.Location.X, refreshDataLabel.Location.Y + 4);\n \n // Form config\n var form = new Form();\n form.Text = \"Incremental Refresh configuration:\";\n form.AutoSize = true;\n form.MinimumSize = new Size(450, 0);\n form.FormBorderStyle = FormBorderStyle.FixedDialog;\n form.MaximizeBox = false;\n form.MinimizeBox = false;\n \n // Open the dialogue in the center of the screen\n form.StartPosition = FormStartPosition.CenterScreen;\n \n // Set the AutoScaleMode property to Dpi\n form.AutoScaleMode = AutoScaleMode.Dpi;\n \n // Add controls to form specified above\n form.Controls.Add(storeDataLabel);\n form.Controls.Add(storeDataTextBox);\n form.Controls.Add(storeDataComboBox);\n form.Controls.Add(refreshDataLabel);\n form.Controls.Add(refreshDataTextBox);\n form.Controls.Add(refreshDataComboBox);\n form.Controls.Add(fullPeriodsCheckBox);\n form.Controls.Add(okButton);\n form.Controls.Add(cancelButton);\n \n // Draw the form\n var result = form.ShowDialog();\n \n // Get the values of the user input if entered\n if (result == DialogResult.OK)\n {\n // Enables the refresh policy\n _Table.EnableRefreshPolicy = true;\n \n var storeDataValue = storeDataTextBox.Text;\n var storeDataComboBoxValue = storeDataComboBox.SelectedItem.ToString();\n var refreshDataValue = refreshDataTextBox.Text;\n var refreshDataComboBoxValue = refreshDataComboBox.SelectedItem.ToString();\n var fullPeriodsChecked = fullPeriodsCheckBox.Checked;\n \n // Display the input values in a message box\n var message = string.Format(\n \"Store data in the last: {0} {1}\" + \n \"\\nRefresh data in the last: {2} {3}\" + \n \"\\nRefresh only full periods: {4}\",\n storeDataTextBox.Text,\n storeDataComboBox.SelectedItem.ToString(),\n refreshDataTextBox.Text,\n refreshDataComboBox.SelectedItem.ToString(),\n fullPeriodsCheckBox.Checked);\n \n Info(message);\n \n // Convert StoreDataGranularity to correct TOM Property\n RefreshGranularityType StoreDataGranularity = RefreshGranularityType.Day;\n switch (storeDataComboBox.SelectedItem.ToString())\n {\n case \"years\":\n StoreDataGranularity = RefreshGranularityType.Year;\n break;\n \n case \"quarters\":\n StoreDataGranularity = RefreshGranularityType.Quarter;\n break;\n \n case \"months\":\n StoreDataGranularity = RefreshGranularityType.Month;\n break;\n \n case \"days\":\n StoreDataGranularity = RefreshGranularityType.Day;\n break; \n \n default:\n Error(\"Bad selection for Incremental Granularity.\");\n break;\n }\n\n \n // Convert IncrementalGranularity to correct TOM Property\n RefreshGranularityType IncrementalPeriodGranularity = RefreshGranularityType.Year;\n switch (refreshDataComboBox.SelectedItem.ToString())\n {\n case \"years\":\n IncrementalPeriodGranularity = RefreshGranularityType.Year;\n break;\n \n case \"quarters\":\n IncrementalPeriodGranularity = RefreshGranularityType.Quarter;\n break;\n \n case \"months\":\n IncrementalPeriodGranularity = RefreshGranularityType.Month;\n break;\n \n case \"days\":\n IncrementalPeriodGranularity = RefreshGranularityType.Day;\n break; \n \n default:\n Error ( \"Bad selection for Incremental Granularity.\" );\n break;\n }\n \n // Convert RefreshCompletePeriods checkbox to correct TOM property\n int RefreshCompletePeriods;\n if ( fullPeriodsCheckBox.Checked == true )\n { \n RefreshCompletePeriods = -1;\n }\n else\n {\n RefreshCompletePeriods = 0;\n }\n \n // Set incremental window: period to be refreshed\n _Table.IncrementalGranularity = IncrementalPeriodGranularity;\n \n // Default: 30 days - change # if you want\n _Table.IncrementalPeriods = Convert.ToInt16(refreshDataTextBox.Text);\n \n // Only refresh complete days. Change to 0 if you don't want.\n _Table.IncrementalPeriodsOffset = RefreshCompletePeriods;\n \n // Set rolling window: period to be archived\n // Granularity = day, can change to month, quarter, year...\n _Table.RollingWindowGranularity = StoreDataGranularity;\n \n // Keep data for 1 year. Includes 1 full year and current partial year \n // i.e. if it is Nov 2023, keeps data from Jan 1, 2022. \n // On Jan 1, 2024, it will drop 2022 automatically.\n _Table.RollingWindowPeriods = Convert.ToInt16(storeDataTextBox.Text);\n \n // If the selected date column is an integer of type YYYYMMDD...\n if ( _ColumnDataType == DataType.Int64 )\n {\n // Add DateTimeToInt Function\n var _DateTimeToInt = \n Model.AddExpression( \n \"fxDateTimeToInt\", \n @\"(x as datetime) => Date.Year(x) * 10000 + Date.Month(x) * 100 + Date.Day(x)\"\n );\n \n _DateTimeToInt.SetAnnotation(\"PBI_ResultType\", \"Function\");\n _DateTimeToInt.Kind = ExpressionKind.M;\n \n // Source expression obtained from the original M partition\n _Table.SourceExpression = \n \n // Gets expression before final \"in\" keyword\n _MExpression.Split(\"\\nin\")[0].TrimEnd() +\n \n // Adds comma and newline\n \",\\n\" +\n \n // Adds step called \"Incremental Refresh\" for filtering\n @\" #\"\"Incremental Refresh\"\" = Table.SelectRows( \" +\n \n // Gets name of last step (after \"in\" keyword)\n _MExpression.Split(\"\\nin\")[1].TrimStart() +\n \n // Adds 'each' keyword\n @\", each \" +\n \n // Bases incremental refresh on current column name\n _ColumnName +\n \n // Greater than or equal to RangeStart\n @\" >= fxDateTimeToInt ( #\"\"RangeStart\"\" ) and \" +\n \n // and\n _ColumnName +\n \n // Less than RangeEnd\n @\" < fxDateTimeToInt ( #\"\"RangeEnd\"\" ) )\" +\n \n // re-add 'in' keyword\n \"\\nin\\n\" +\n \n // Reference final step just added\n @\" #\"\"Incremental Refresh\"\"\";\n }\n \n \n // Otherwise treat it like a normal date/datetime column\n else\n {\n // Source expression obtained from the original M partition\n _Table.SourceExpression = \n // Gets expression before final \"in\" keyword\n _MExpression.Split(\"\\nin\")[0].TrimEnd() +\n \n // Adds comma and newline\n \",\\n\" +\n \n // Adds step called \"Incremental Refresh\" for filtering\n @\" #\"\"Incremental Refresh\"\" = Table.SelectRows( \" +\n \n // Gets name of last step (after \"in\" keyword)\n _MExpression.Split(\"\\nin\")[1].TrimStart() +\n \n // Adds 'each' keyword\n @\", each \" +\n \n // Bases incremental refresh on current column name\n _ColumnName +\n \n // Greater than or equal to RangeStart\n @\" >= Date.From ( #\"\"RangeStart\"\" ) and \" +\n \n // and\n _ColumnName +\n \n // Less than RangeEnd\n @\" < Date.From ( #\"\"RangeEnd\"\" ) )\" +\n \n // re-add 'in' keyword\n \"\\nin\\n\" +\n \n // Reference final step just added\n @\" #\"\"Incremental Refresh\"\"\";\n }\n \n // Success message for Refresh Policy configuration\n Info ( \n \"Successfully configured the Incremental Refresh policy.\\n\" + \n \"\\nSelect the table and right-click on 'Apply Refresh Policy...'\" + \n \"\\nSelect & peform a 'Full Refresh' of all new policy partitons that are created.\" \n );\n }\n else if (result == DialogResult.Cancel)\n {\n // if the user clicks the Cancel button, close the form and exit the script\n form.Close();\n Error ( \"Cancelled configuration! Ending script without changes.\" );\n return;\n }\n }\n catch\n {\n Error( \"No valid column selected! Ending script without changes.\" );\n }\n \n}\ncatch\n{\n Error( \"No valid table selected! Ending script without changes.\" );\n}", "Tooltip": "", "ValidContexts": "Model" }, { - "Id": 6, - "Name": "Modelling\\Create date table", + "Id": 3, + "Name": "Modelling\\Advanced\\Create a Date Table", "Enabled": "true", "Execute": "// To use this C# Script:\n//\n// 1. Run the script\n// 2. Select the column that has the earliest date\n// 3. Select the column that has the latest date\n\n// List of all DateTime columns in the model\nvar _dateColumns = Model.AllColumns.Where(c => c.DataType == DataType.DateTime ).ToList();\n\n// Select the column with the earliest date in the model\ntry\n{\n string _EarliestDate = \n SelectColumn(\n _dateColumns, \n null, \n \"Select the Column with the Earliest Date:\"\n ).DaxObjectFullName;\n \n try\n {\n // Select the column with the latest date in the model\n string _LatestDate = \n SelectColumn(\n _dateColumns, \n null, \n \"Select the Column with the Latest Date:\"\n ).DaxObjectFullName;\n \n \n // Create measure for reference date\n var _RefDateMeasure = _dateColumns[0].Table.AddMeasure(\n \"RefDate\",\n \"CALCULATE ( MAX ( \" + _LatestDate + \" ), REMOVEFILTERS ( ) )\"\n );\n \n \n // Formatted date table DAX\n // Based on date table from https://www.sqlbi.com/topics/date-table/\n // To adjust, copy everything after the @\" into a DAX query window & replace\n \n var _DateDaxExpression = @\"-- Reference date for the latest date in the report\n -- Until when the business wants to see data in reports\n VAR _Refdate_Measure = [RefDate]\n VAR _Today = TODAY ( )\n \n -- Replace with \"\"Today\"\" if [RefDate] evaluates blank\n VAR _Refdate = IF ( ISBLANK ( _Refdate_Measure ), _Today, _Refdate_Measure )\n VAR _RefYear = YEAR ( _Refdate )\n VAR _RefQuarter = _RefYear * 100 + QUARTER(_Refdate)\n VAR _RefMonth = _RefYear * 100 + MONTH(_Refdate)\n VAR _RefWeek_EU = _RefYear * 100 + WEEKNUM(_Refdate, 2)\n \n -- Earliest date in the model scope\n VAR _EarliestDate = DATE ( YEAR ( MIN ( \" + _EarliestDate + @\" ) ) - 2, 1, 1 )\n VAR _EarliestDate_Safe = MIN ( _EarliestDate, DATE ( YEAR ( _Today ) + 1, 1, 1 ) )\n \n -- Latest date in the model scope\n VAR _LatestDate_Safe = DATE ( YEAR ( _Refdate ) + 2, 12, 1 )\n \n ------------------------------------------\n -- Base calendar table\n VAR _Base_Calendar = CALENDAR ( _EarliestDate_Safe, _LatestDate_Safe )\n ------------------------------------------\n \n \n \n ------------------------------------------\n VAR _IntermediateResult = \n ADDCOLUMNS ( _Base_Calendar,\n \n ------------------------------------------\n \"\"Calendar Year Number (ie 2021)\"\", --|\n YEAR ([Date]), --|-- Year\n --|\n \"\"Calendar Year (ie 2021)\"\", --|\n FORMAT ([Date], \"\"YYYY\"\"), --|\n ------------------------------------------\n \n ------------------------------------------\n \"\"Calendar Quarter Year (ie Q1 2021)\"\", --|\n \"\"Q\"\" & --|-- Quarter\n CONVERT(QUARTER([Date]), STRING) & --|\n \"\" \"\" & --|\n CONVERT(YEAR([Date]), STRING), --|\n --|\n \"\"Calendar Year Quarter (ie 202101)\"\", --|\n YEAR([Date]) * 100 + QUARTER([Date]), --|\n ------------------------------------------\n \n ------------------------------------------\n \"\"Calendar Month Year (ie Jan 21)\"\", --|\n FORMAT ( [Date], \"\"MMM YY\"\" ), --|-- Month\n --|\n \"\"Calendar Year Month (ie 202101)\"\", --|\n YEAR([Date]) * 100 + MONTH([Date]), --|\n --|\n \"\"Calendar Month (ie Jan)\"\", --|\n FORMAT ( [Date], \"\"MMM\"\" ), --|\n --|\n \"\"Calendar Month # (ie 1)\"\", --|\n MONTH ( [Date] ), --|\n ------------------------------------------\n \n ------------------------------------------\n \"\"Calendar Week EU (ie WK25)\"\", --|\n \"\"WK\"\" & WEEKNUM( [Date], 2 ), --|-- Week\n --|\n \"\"Calendar Week Number EU (ie 25)\"\", --|\n WEEKNUM( [Date], 2 ), --|\n --|\n \"\"Calendar Year Week Number EU (ie 202125)\"\", --|\n YEAR ( [Date] ) * 100 --|\n + --|\n WEEKNUM( [Date], 2 ), --|\n --|\n \"\"Calendar Week US (ie WK25)\"\", --|\n \"\"WK\"\" & WEEKNUM( [Date], 1 ), --|\n --|\n \"\"Calendar Week Number US (ie 25)\"\", --|\n WEEKNUM( [Date], 1 ), --|\n --|\n \"\"Calendar Year Week Number US (ie 202125)\"\", --|\n YEAR ( [Date] ) * 100 --|\n + --|\n WEEKNUM( [Date], 1 ), --|\n --|\n \"\"Calendar Week ISO (ie WK25)\"\", --|\n \"\"WK\"\" & WEEKNUM( [Date], 21 ), --|\n --|\n \"\"Calendar Week Number ISO (ie 25)\"\", --|\n WEEKNUM( [Date], 21 ), --|\n --|\n \"\"Calendar Year Week Number ISO (ie 202125)\"\",--|\n YEAR ( [Date] ) * 100 --|\n + --|\n WEEKNUM( [Date], 21 ), --|\n ------------------------------------------\n \n ------------------------------------------\n \"\"Weekday Short (i.e. Mon)\"\", --|\n FORMAT ( [Date], \"\"DDD\"\" ), --|-- Weekday\n --|\n \"\"Weekday Name (i.e. Monday)\"\", --|\n FORMAT ( [Date], \"\"DDDD\"\" ), --|\n --|\n \"\"Weekday Number EU (i.e. 1)\"\", --|\n WEEKDAY ( [Date], 2 ), --|\n ------------------------------------------\n \n ------------------------------------------\n \"\"Calendar Month Day (i.e. Jan 05)\"\", --|\n FORMAT ( [Date], \"\"MMM DD\"\" ), --|-- Day\n --|\n \"\"Calendar Month Day (i.e. 0105)\"\", --|\n MONTH([Date]) * 100 --|\n + --|\n DAY([Date]), --|\n --|\n \"\"YYYYMMDD\"\", --|\n YEAR ( [Date] ) * 10000 --|\n + --|\n MONTH ( [Date] ) * 100 --|\n + --|\n DAY ( [Date] ), --|\n ------------------------------------------\n \n \n ------------------------------------------\n \"\"IsDateInScope\"\", --|\n [Date] <= _Refdate --|-- Boolean\n && --|\n YEAR([Date]) > YEAR(_EarliestDate), --|\n --|\n \"\"IsBeforeThisMonth\"\", --|\n [Date] <= EOMONTH ( _Refdate, -1 ), --|\n --|\n \"\"IsLastMonth\"\", --|\n [Date] <= EOMONTH ( _Refdate, 0 ) --|\n && --|\n [Date] > EOMONTH ( _Refdate, -1 ), --|\n --|\n \"\"IsYTD\"\", --|\n MONTH([Date]) --|\n <= --|\n MONTH(EOMONTH ( _Refdate, 0 )), --|\n --|\n \"\"IsActualToday\"\", --|\n [Date] = _Today, --|\n --|\n \"\"IsRefDate\"\", --|\n [Date] = _Refdate, --|\n --|\n \"\"IsHoliday\"\", --|\n MONTH([Date]) * 100 --|\n + --|\n DAY([Date]) --|\n IN {0101, 0501, 1111, 1225}, --|\n --|\n \"\"IsWeekday\"\", --|\n WEEKDAY([Date], 2) --|\n IN {1, 2, 3, 4, 5}) --|\n ------------------------------------------\n \n VAR _Result = \n \n --------------------------------------------\n ADDCOLUMNS ( --|\n _IntermediateResult, --|-- Boolean #2\n \"\"IsThisYear\"\", --|\n [Calendar Year Number (ie 2021)] --|\n = _RefYear, --|\n --|\n \"\"IsThisMonth\"\", --|\n [Calendar Year Month (ie 202101)] --|\n = _RefMonth, --|\n --|\n \"\"IsThisQuarter\"\", --|\n [Calendar Year Quarter (ie 202101)] --|\n = _RefQuarter, --|\n --|\n \"\"IsThisWeek\"\", --|\n [Calendar Year Week Number EU (ie 202125)]--|\n = _RefWeek_EU --|\n ) --|\n --------------------------------------------\n \n RETURN \n _Result\";\n \n // Create date table\n var _date = Model.AddCalculatedTable(\n \"Date\",\n _DateDaxExpression\n );\n \n //-------------------------------------------------------------------------------------------//\n \n // Sort by...\n \n // Sort Weekdays\n (_date.Columns[\"Weekday Name (i.e. Monday)\"] as CalculatedTableColumn).SortByColumn = (_date.Columns[\"Weekday Number EU (i.e. 1)\"] as CalculatedTableColumn);\n (_date.Columns[\"Weekday Short (i.e. Mon)\"] as CalculatedTableColumn).SortByColumn = (_date.Columns[\"Weekday Number EU (i.e. 1)\"] as CalculatedTableColumn);\n \n // Sort Weeks\n (_date.Columns[\"Calendar Week EU (ie WK25)\"] as CalculatedTableColumn).SortByColumn = (_date.Columns[\"Calendar Week Number EU (ie 25)\"] as CalculatedTableColumn);\n (_date.Columns[\"Calendar Week ISO (ie WK25)\"] as CalculatedTableColumn).SortByColumn = (_date.Columns[\"Calendar Week Number ISO (ie 25)\"] as CalculatedTableColumn);\n (_date.Columns[\"Calendar Week US (ie WK25)\"] as CalculatedTableColumn).SortByColumn = (_date.Columns[\"Calendar Week Number US (ie 25)\"] as CalculatedTableColumn);\n \n // Sort Months\n (_date.Columns[\"Calendar Month (ie Jan)\"] as CalculatedTableColumn).SortByColumn = (_date.Columns[\"Calendar Month # (ie 1)\"] as CalculatedTableColumn);\n (_date.Columns[\"Calendar Month Day (i.e. Jan 05)\"] as CalculatedTableColumn).SortByColumn = (_date.Columns[\"Calendar Month Day (i.e. 0105)\"] as CalculatedTableColumn);\n (_date.Columns[\"Calendar Month Year (ie Jan 21)\"] as CalculatedTableColumn).SortByColumn = (_date.Columns[\"Calendar Year Month (ie 202101)\"] as CalculatedTableColumn);\n \n // Sort Quarters\n (_date.Columns[\"Calendar Quarter Year (ie Q1 2021)\"] as CalculatedTableColumn).SortByColumn = (_date.Columns[\"Calendar Year Quarter (ie 202101)\"] as CalculatedTableColumn);\n \n // Sort Years\n (_date.Columns[\"Calendar Year (ie 2021)\"] as CalculatedTableColumn).SortByColumn = (_date.Columns[\"Calendar Year Number (ie 2021)\"] as CalculatedTableColumn);\n \n \n //-------------------------------------------------------------------------------------------//\n \n \n // For all the columns in the date table:\n foreach (var c in _date.Columns )\n {\n c.DisplayFolder = \"7. Boolean Fields\";\n c.IsHidden = true;\n \n // Organize the date table into folders\n if ( ( c.DataType == DataType.DateTime & c.Name.Contains(\"Date\") ) )\n {\n c.DisplayFolder = \"6. Calendar Date\";\n c.IsHidden = false;\n c.IsKey = true;\n }\n \n if ( c.Name == \"YYMMDDDD\" )\n {\n c.DisplayFolder = \"6. Calendar Date\";\n c.IsHidden = true;\n }\n \n if ( c.Name.Contains(\"Year\") & c.DataType != DataType.Boolean )\n {\n c.DisplayFolder = \"1. Year\";\n c.IsHidden = false;\n }\n \n if ( c.Name.Contains(\"Week\") & c.DataType != DataType.Boolean )\n {\n c.DisplayFolder = \"4. Week\";\n c.IsHidden = true;\n }\n \n if ( c.Name.Contains(\"day\") & c.DataType != DataType.Boolean )\n {\n c.DisplayFolder = \"5. Weekday / Workday\\\\Weekday\";\n c.IsHidden = false;\n }\n \n if ( c.Name.Contains(\"Month\") & c.DataType != DataType.Boolean )\n {\n c.DisplayFolder = \"3. Month\";\n c.IsHidden = false;\n }\n \n if ( c.Name.Contains(\"Quarter\") & c.DataType != DataType.Boolean )\n {\n c.DisplayFolder = \"2. Quarter\";\n c.IsHidden = false;\n }\n \n }\n \n // Mark as date table\n _date.DataCategory = \"Time\";\n \n \n //-------------------------------------------------------------------------------------------//\n \n \n // Create Workdays MTD, QTD, YTD logic \n // (separate into measures & calc. column to be easier to maintain)\n //\n // Add calculated columns for Workdays MTD, QTD, YTD\n \n string _WorkdaysDax = @\"VAR _Holidays =\n CALCULATETABLE (\n DISTINCT ('Date'[Date]),\n 'Date'[IsHoliday] <> TRUE\n )\n VAR _WeekdayName = CALCULATE ( SELECTEDVALUE ( 'Date'[Weekday Short (i.e. Mon)] ) )\n VAR _WeekendDays = SWITCH (\n _WeekdayName,\n \"\"Sat\"\", 2,\n \"\"Sun\"\", 3,\n 0\n )\n VAR _WorkdaysMTD =\n CALCULATE (\n NETWORKDAYS (\n CALCULATE (\n MIN ('Date'[Date]),\n ALLEXCEPT ('Date', 'Date'[Calendar Month Year (ie Jan 21)])\n ),\n CALCULATE (MAX ('Date'[Date]) - _WeekendDays),\n 1,\n _Holidays\n )\n )\n + 1\n RETURN\n IF (_WorkdaysMTD < 1, 1, _WorkdaysMTD)\";\n \n _date.AddCalculatedColumn(\n \"Workdays MTD\",\n _WorkdaysDax,\n \"5. Weekday / Workday\\\\Workdays\"\n );\n \n _date.AddCalculatedColumn(\n \"Workdays QTD\",\n _WorkdaysDax.Replace(\"'Date'[Calendar Month Year (ie Jan 21)]\", \"'Date'[Calendar Quarter Year (ie Q1 2021)]\"),\n \"5. Weekday / Workday\\\\Workdays\"\n );\n \n _date.AddCalculatedColumn(\n \"Workdays YTD\",\n _WorkdaysDax.Replace(\"'Date'[Calendar Month Year (ie Jan 21)]\", \"'Date'[Calendar Year (ie 2021)]\"),\n \"5. Weekday / Workday\\\\Workdays\"\n );\n \n \n //-------------------------------------------------------------------------------------------//\n \n \n // Create measures for showing how many workdays passed\n _WorkdaysDax = @\"CALCULATE(\n MAX( 'Date'[Workdays MTD] ),\n 'Date'[IsDateInScope] = TRUE\n )\";\n \n _date.AddMeasure(\n \"# Workdays MTD\",\n _WorkdaysDax,\n \"5. Weekday / Workday\\\\Measures\\\\# Workdays\"\n );\n \n _date.AddMeasure(\n \"# Workdays QTD\",\n _WorkdaysDax.Replace(\"MTD\", \"QTD\"),\n \"5. Weekday / Workday\\\\Measures\\\\# Workdays\"\n );\n \n _date.AddMeasure(\n \"# Workdays YTD\",\n _WorkdaysDax.Replace(\"MTD\", \"YTD\"),\n \"5. Weekday / Workday\\\\Measures\\\\# Workdays\"\n );\n \n // Create measures showing how many workdays are in the selected period\n \n _WorkdaysDax = @\"IF (\n HASONEVALUE ('Date'[Calendar Month Year (ie Jan 21)]),\n CALCULATE (\n MAX ('Date'[Workdays MTD]),\n VALUES ('Date'[Calendar Month Year (ie Jan 21)])\n )\n )\";\n \n _date.AddMeasure(\n \"# Workdays in Selected Month\",\n _WorkdaysDax,\n \"5. Weekday / Workday\\\\Measures\\\\# Workdays\"\n );\n \n _date.AddMeasure(\n \"# Workdays in Selected Quarter\",\n _WorkdaysDax.Replace(\"MTD\", \"QTD\").Replace(\"'Date'[Calendar Month Year (ie Jan 21)]\", \"'Date'[Calendar Quarter Year (ie Q1 2021)]\"),\n \"5. Weekday / Workday\\\\Measures\\\\# Workdays\"\n );\n \n _date.AddMeasure(\n \"# Workdays in Selected Year\",\n _WorkdaysDax.Replace(\"MTD\", \"YTD\").Replace(\"'Date'[Calendar Month Year (ie Jan 21)]\", \"'Date'[Calendar Year (ie 2021)]\"),\n \"5. Weekday / Workday\\\\Measures\\\\# Workdays\"\n );\n \n \n // Create measures showing how many workdays passed as a %\n \n _WorkdaysDax = @\"IF (\n HASONEVALUE ('Date'[Calendar Month Year (ie Jan 21)]),\n MROUND (\n DIVIDE ([# Workdays MTD], [# Workdays in Selected Month]),\n 0.01\n )\n )\";\n \n _date.AddMeasure(\n \"% Workdays MTD\",\n _WorkdaysDax,\n \"5. Weekday / Workday\\\\Measures\\\\# Workdays\"\n );\n \n _date.AddMeasure(\n \"% Workdays QTD\",\n _WorkdaysDax.Replace(\"MTD\", \"QTD\").Replace(\"'Date'[Calendar Month Year (ie Jan 21)]\", \"'Date'[Calendar Quarter Year (ie Q1 2021)]\").Replace(\"Month\", \"Quarter\"),\n \"5. Weekday / Workday\\\\Measures\\\\# Workdays\"\n );\n \n _date.AddMeasure(\n \"% Workdays YTD\",\n _WorkdaysDax.Replace(\"MTD\", \"YTD\").Replace(\"'Date'[Calendar Month Year (ie Jan 21)]\", \"'Date'[Calendar Year (ie 2021)]\").Replace(\"Month\", \"Year\"),\n \"5. Weekday / Workday\\\\Measures\\\\# Workdays\"\n );\n \n \n //-------------------------------------------------------------------------------------------//\n \n \n // Move the reference measure to the newly created 'Date' table.\n _RefDateMeasure.Delete();\n _RefDateMeasure = Model.Tables[\"Date\"].AddMeasure(\n \"RefDate\",\n \"CALCULATE ( MAX ( \" + _LatestDate + \" ), REMOVEFILTERS ( ) )\",\n \"0. Measures\"\n );\n \n _RefDateMeasure.IsHidden = true;\n \n Info ( \"Created a new, organized 'Date' table based on the template in the C# Script.\\nThe Earliest Date is taken from \" + _EarliestDate + \"\\nThe Latest Date is taken from \" + _LatestDate );\n \n }\n catch\n {\n Error( \"Latest column not selected! Ending script without making changes.\" );\n }\n}\ncatch\n{\n Error( \"Earliest column not selected! Ending script without making changes.\" );\n}\n\n", "Tooltip": "", "ValidContexts": "Model" }, { - "Id": 7, - "Name": "Modelling\\Create new M parameter", + "Id": 4, + "Name": "Modelling\\Advanced\\Create New M Parameter and Add it to Existing M Partitions", "Enabled": "true", "Execute": "// This script creates a new M Parameter as a 'Shared Expression'.\n// It will also find the default value in all M partitions and replace them with the parameter object name.\n//#r \"System.Drawing\"\n\nusing System.Drawing;\nusing System.Text.RegularExpressions;\nusing System.Windows.Forms;\n\n// Hide the 'Running Macro' spinbox\nScriptHelper.WaitFormVisible = false;\n\n// Initialize variables\nstring _ParameterName = \"New Parameter\";\nstring _ParameterValue = \"ParameterValue\";\n\n// WinForms prompt to get Parameter Name / Value input\nusing (Form prompt = new Form())\n{\n Font formFont = new Font(\"Segoe UI\", 11); \n\n // Prompt config\n prompt.AutoSize = true;\n prompt.MinimumSize = new Size(380, 120);\n prompt.Text = \"Create New M Parameter\";\n prompt.StartPosition = FormStartPosition.CenterScreen;\n\n // Find: label\n Label parameterNameLabel = new Label() { Text = \"Enter Name:\" };\n parameterNameLabel.Location = new Point(20, 20);\n parameterNameLabel.AutoSize = true;\n parameterNameLabel.Font = formFont;\n\n // Textbox for inputing the substring text\n TextBox parameterNameBox = new TextBox();\n parameterNameBox.Width = 200;\n parameterNameBox.Location = new Point(parameterNameLabel.Location.X + parameterNameLabel.Width + 20, parameterNameLabel.Location.Y - 4);\n parameterNameBox.SelectedText = \"New Parameter\";\n parameterNameBox.Font = formFont;\n\n // Replace: label\n Label parameterValueLabel = new Label() { Text = \"Enter Value:\" };\n parameterValueLabel.Location = new Point(parameterNameLabel.Location.X, parameterNameLabel.Location.Y + parameterNameLabel.Height + 20);\n parameterValueLabel.AutoSize = true;\n parameterValueLabel.Font = formFont;\n\n // Textbox for inputting the substring text\n TextBox parameterValueBox = new TextBox() { Left = parameterValueLabel.Right + 20, Top = parameterValueLabel.Location.Y - 4, Width = parameterNameBox.Width };\n parameterValueBox.SelectedText = \"Parameter Value\";\n parameterValueBox.Font = formFont;\n\n // OK Button\n Button okButton = new Button() { Text = \"Create\", Left = 20, Width = 75, Top = parameterValueBox.Location.Y + parameterValueBox.Height + 20 };\n okButton.MinimumSize = new Size(75, 25);\n okButton.AutoSize = true;\n okButton.Font = formFont;\n\n // Cancel Button\n Button cancelButton = new Button() { Text = \"Cancel\", Left = okButton.Location.X + okButton.Width + 10, Top = okButton.Location.Y };\n cancelButton.MinimumSize = new Size(75, 25);\n cancelButton.AutoSize = true;\n cancelButton.Font = formFont;\n\n // Button actions\n okButton.Click += (sender, e) => { _ParameterName = parameterNameBox.Text; _ParameterValue = parameterValueBox.Text; prompt.DialogResult = DialogResult.OK; };\n cancelButton.Click += (sender, e) => { prompt.DialogResult = DialogResult.Cancel; };\n\n prompt.AcceptButton = okButton;\n prompt.CancelButton = cancelButton;\n\n prompt.Controls.Add(parameterNameLabel);\n prompt.Controls.Add(parameterNameBox);\n prompt.Controls.Add(parameterValueLabel);\n prompt.Controls.Add(parameterValueBox);\n prompt.Controls.Add(okButton);\n prompt.Controls.Add(cancelButton);\n\n // The user clicked OK, so perform the find-and-replace logic\n if (prompt.ShowDialog() == DialogResult.OK)\n {\n\n // Creates the parameter\n Model.AddExpression( \n _ParameterName, \n @\"\n \"\"\" + _ParameterValue +\n @\"\"\" meta\n [\n IsParameterQuery = true,\n IsParameterQueryRequired = true,\n Type = type text\n ]\"\n );\n \n \n // Informs the user that the parameter was successfully created\n Info ( \n \"Successfully created a new parameter: \" + @\"\"\"\" +\n _ParameterName + @\"\"\"\" +\n \"\\nDefault value: \" + @\"\"\"\" +\n _ParameterValue + @\"\"\"\");\n \n \n // Finds the parameter default value in M Partitions & replaces with the parameter name\n string _Find = @\"\"\"\" + _ParameterValue + @\"\"\"\";\n string _Replace = @\"#\"\"\" + _ParameterName + @\"\"\"\";\n \n int _NrMPartitions = 0;\n int _NrReplacements = 0;\n var _ReplacementsList = new List();\n \n foreach ( var _Tables in Model.Tables )\n {\n foreach ( var _p in _Tables.Partitions )\n {\n if ( _p.SourceType == PartitionSourceType.M )\n {\n if ( _p.Expression != _p.Expression.Replace( _Find, _Replace ) )\n {\n _p.Expression = _p.Expression.Replace( _Find, _Replace );\n \n // Tracks which M partitions were replaced (and how many)\n _NrReplacements = _NrReplacements + 1;\n _ReplacementsList.Add( _p.Name );\n }\n \n // Counts the total # M Partitions\n _NrMPartitions = _NrMPartitions + 1;\n }\n }\n }\n \n \n // Makes a bulleted list of all the M partitions that were replaced\n string _ReplacedPartitions = \" • \" + String.Join(\"\\n • \", _ReplacementsList );\n \n \n // Informs \n // - Whether the Find & Replace was successful\n // - How many M partitions were replaced\n // - Which M partitions had the Find & Replace done\n Info (\n \"Successfully replaced\\n\\n \" +\n _Find + \n \"\\n\\n with: \\n\\n\" + \n _Replace + \n \"\\n\\n in \" + \n Convert.ToString(_NrReplacements) +\n \" of \" +\n Convert.ToString(_NrMPartitions) + \n \" M Partitions:\\n\" +\n _ReplacedPartitions\n );\n\n }\n else\n {\n Error ( \"Cancelled input! Ended script without changes.\");\n }\n}\n", "Tooltip": "", "ValidContexts": "Model" }, { - "Id": 8, - "Name": "Modelling\\Create table groups", + "Id": 5, + "Name": "Modelling\\Beginner\\Create Table Groups", "Enabled": "true", - "Execute": "// Loop through all tables: \nforeach(var table in Model.Tables) \n{ \n if (table is CalculationGroupTable) \n { \n table.TableGroup = \"Calculation Groups\"; \n } \n else if (!table.UsedInRelationships.Any()) \n { \n // Tables without any relationships: \n table.TableGroup = \"Parameter Tables\"; \n } \n else if (table.IsHidden && table.Measures.Any(m => m.IsVisible)) \n { \n // Hidden tables containing visible measures: \n table.TableGroup = \"Measure Groups\"; \n } \n else if (table.UsedInRelationships.All(r => r.FromTable == table)) \n { \n // Tables exclusively on the \"many\" side of relationships: \n table.TableGroup = \"Facts\"; \n } \n else if (table.UsedInRelationships.Any(r => r.ToTable == table)) \n { \n // Tables on the \"one\" side of relationships: \n table.TableGroup = \"Dimensions\"; \n } \n else \n { \n // All other tables: \n table.TableGroup = \"Misc\"; \n } \n} ", + "Execute": "// Loop through all tables:\nforeach(var table in Model.Tables)\n{\n if (table is CalculationGroupTable)\n {\n table.TableGroup = \"Calculation Groups\";\n }\n else if (!table.UsedInRelationships.Any() && table.Measures.Any(m => m.IsVisible))\n {\n // Tables containing visible measures, but no relationships to other tables\n table.TableGroup = \"Measure Groups\";\n }\n else if (table.UsedInRelationships.All(r => r.FromTable == table) && table.UsedInRelationships.Any())\n {\n // Tables exclusively on the \"many\" side of relationships:\n table.TableGroup = \"Facts\";\n }\n else if (!table.UsedInRelationships.Any() && table is CalculatedTable && !table.Measures.Any())\n {\n // Tables without any relationships, that are Calculated Tables and do not have measures:\n table.TableGroup = \"Parameter Tables\";\n }\n else if (table.UsedInRelationships.Any(r => r.ToTable == table))\n {\n // Tables on the \"one\" side of relationships:\n table.TableGroup = \"Dimensions\";\n }\n else\n {\n // All other tables:\n table.TableGroup = \"Misc\";\n }\n}\n", "Tooltip": "", "ValidContexts": "Model" }, { - "Id": 17, - "Name": "Modelling\\Create measure table", + "Id": 6, + "Name": "Modelling\\Beginner\\Create Measure Table", "Enabled": "true", - "Execute": "// Create a calculated table with a single column which is hidden:\nvar table = Model.AddCalculatedTable(\"Model Measures\", \"{0}\");\nvar column = table.AddCalculatedTableColumn(\"Value\", \"[Value]\", \"\", DataType.Int64);\ncolumn.IsHidden = true;", + "Execute": "// Create a calculated table with a single column which is hidden:\nvar table = Model.AddCalculatedTable(\"Model Measures\", \"{0}\");\ntable.Columns[0].IsHidden = true;\n\n", "Tooltip": "", "ValidContexts": "Model" }, { - "Id": 19, + "Id": 7, "Name": "Modelling\\Power BI\\Create Field Parameter table", "Enabled": "true", "Execute": "// Before running the script, select the measures or columns that you\n// would like to use as field parameters (hold down CTRL to select multiple\n// objects). Also, you may change the name of the field parameter table\n// below. NOTE: If used against Power BI Desktop, you must enable unsupported\n// features under File > Preferences (TE2) or Tools > Preferences (TE3).\nvar name = \"Parameter\";\n\nif(Selected.Columns.Count == 0 && Selected.Measures.Count == 0) throw new Exception(\"No columns or measures selected!\");\n\n// Construct the DAX for the calculated table based on the current selection:\nvar objects = Selected.Columns.Any() ? Selected.Columns.Cast() : Selected.Measures;\nvar dax = \"{\\n \" + string.Join(\",\\n \", objects.Select((c,i) => string.Format(\"(\\\"{0}\\\", NAMEOF('{1}'[{0}]), {2})\", c.Name, c.Table.Name, i))) + \"\\n}\";\n\n// Add the calculated table to the model:\nvar table = Model.AddCalculatedTable(name, dax);\n\n// In TE2 columns are not created automatically from a DAX expression, so \n// we will have to add them manually:\nvar te2 = table.Columns.Count == 0;\nvar nameColumn = te2 ? table.AddCalculatedTableColumn(name, \"[Value1]\") : table.Columns[\"Value1\"] as CalculatedTableColumn;\nvar fieldColumn = te2 ? table.AddCalculatedTableColumn(name + \" Fields\", \"[Value2]\") : table.Columns[\"Value2\"] as CalculatedTableColumn;\nvar orderColumn = te2 ? table.AddCalculatedTableColumn(name + \" Order\", \"[Value3]\") : table.Columns[\"Value3\"] as CalculatedTableColumn;\n\nif(!te2) {\n // Rename the columns that were added automatically in TE3:\n nameColumn.IsNameInferred = false;\n nameColumn.Name = name;\n fieldColumn.IsNameInferred = false;\n fieldColumn.Name = name + \" Fields\";\n orderColumn.IsNameInferred = false;\n orderColumn.Name = name + \" Order\";\n}\n// Set remaining properties for field parameters to work\n// See: https://twitter.com/markbdi/status/1526558841172893696\nnameColumn.SortByColumn = orderColumn;\nnameColumn.GroupByColumns.Add(fieldColumn);\nfieldColumn.SortByColumn = orderColumn;\nfieldColumn.SetExtendedProperty(\"ParameterMetadata\", \"{\\\"version\\\":3,\\\"kind\\\":2}\", ExtendedPropertyType.Json);\nfieldColumn.IsHidden = true;\norderColumn.IsHidden = true;", @@ -57,12 +57,52 @@ "ValidContexts": "Measure, Column" }, { - "Id": 20, - "Name": "Analysis\\Count rows in table", + "Id": 8, + "Name": "Analysis\\Beginner\\Count Rows in the Selected Table", "Enabled": "true", "Execute": "// Get table name\nstring _TableName = \n Selected.Table.DaxObjectFullName;\n\n// Count table rows\nstring _dax = \n \"{ FORMAT( COUNTROWS (\" + _TableName + \"), \\\"#,##0\\\" ) }\";\n\n// Evaluate DAX\nstring _TableRows = \n Convert.ToString(EvaluateDax( _dax ));\n\n// Return output in pop-up\nInfo ( \"Number of rows in \" + _TableName + \": \" + _TableRows);", "Tooltip": "", "ValidContexts": "Table" + }, + { + "Id": 9, + "Name": "Analysis\\Beginner\\Count the number of model objects by Type", + "Enabled": "true", + "Execute": "// This script counts objects in your model and displays them in a pop-up info box.\n// It does not write any changes to this model.\n//\n// Use this script when you open a new model and need a 'helicopter view' on the contents.\n//\n// Count calculation groups & calculation items\nint _calcgroups = 0;\nint _calcitems = 0;\nforeach ( var _calcgroup in Model.CalculationGroups )\n{\n _calcgroups = _calcgroups + 1;\n  foreach ( var _item in _calcgroup.CalculationItems )\n  {\n  _calcitems = _calcitems + 1;\n   }\n}\n\n// Count partitions and DAX parameters\nint _partitions = 0;\nint _whatifparameters = 0;\nint _fieldparameters = 0;\nforeach ( var _table in Model.Tables )\n{\n  foreach ( var _partition in _table.Partitions )\n  {\n string _type = Convert.ToString(_partition.SourceType);\n string _exp = Convert.ToString(_partition.Expression);\n if ( _type == \"M\" )\n  {\n _partitions = _partitions + 1;\n }\n else if ( _type == \"Calculated\" && _exp.Contains(\"NAMEOF\") )\n {\n _fieldparameters = _fieldparameters + 1;\n }\n else if ( _type == \"Calculated\" && _exp.Contains(\"GENERATESERIES\") )\n {\n _whatifparameters = _whatifparameters + 1;\n }\n \n   }\n}\n\n// Average measure length\ndecimal _numLines = 0;\ndecimal _numChars = 0;\nint _measures = Model.AllMeasures.Count();\nforeach ( var _measure in Model.AllMeasures )\n{\n _numLines = _numLines + _measure.Expression.Split(\"\\n\").Length;\n _numChars = _numChars + _measure.Expression.Length;\n}\n_numLines = Math.Round(_numLines / _measures, 1);\n_numChars = Math.Round(_numChars / _measures, 1);\n\n\n// Return the pop-up\nInfo ( \"In the model, we see the below objects:\\n\\n\"\n\n + \"-----------------------------------------\\n\"\n + \"Data Objects\\n\"\n + \"-----------------------------------------\\n\"\n + \" ├─ PQ Expressions: \" + Convert.ToString(Model.Expressions.Count()) + \"\\n\"\n + \" │\\n\"\n + \" └─ Tables: \" + Convert.ToString(Model.Tables.Count()) + \"\\n\"\n + \" ├─ Incremental Refresh Tables: \" + \n Convert.ToString(Model.Tables.Where(\n _ir => \n Convert.ToString(_ir.EnableRefreshPolicy) \n == \n \"True\").Count()) + \"\\n\"\n \n + \" │\\n\"\n + \" ├─ Calculated Tables: \" + \n Convert.ToString(\n Model.Tables.Where(\n _tables => \n Convert.ToString(_tables.Columns[0].Type) \n == \n \"CalculatedTableColumn\").Count()) + \"\\n\"\n\n + \" │ ├─ What if parameters: \" + \n Convert.ToString(_whatifparameters) + \"\\n\"\n + \" │ └─ Field parameters: \" + \n Convert.ToString(_fieldparameters) + \"\\n\"\n + \" │\\n\"\n + \" ├─ M Partitions: \" + \n Convert.ToString(_partitions) + \"\\n\"\n + \" │\\n\"\n + \" └─ Total Table Columns: \" + \n Convert.ToString(Model.AllColumns.Count()) + \"\\n\\n\"\n\n + \"-----------------------------------------\\n\"\n + \"DAX Objects\\n\"\n + \"-----------------------------------------\\n\"\n + \" ├─ Relationships: \" + \n Convert.ToString(Model.Relationships.Count()) + \"\\n\"\n + \" │ ├─ Bi-directional: \" + \n Convert.ToString(Model.Relationships.Where(\n _relationships => \n Convert.ToString(_relationships.CrossFilteringBehavior) \n == \n \"BothDirections\").Count()) + \"\\n\"\n\n + \" │ ├─ Many-to-Many: \" + \n Convert.ToString(Model.Relationships.Where(\n _relationships => \n Convert.ToString(_relationships.FromCardinality) \n == \n \"Many\" \n && \n Convert.ToString(_relationships.ToCardinality) \n == \n \"Many\").Count()) + \"\\n\"\n\n + \" │ ├─ One-to-One: \" + \n Convert.ToString(Model.Relationships.Where(\n _relationships => \n Convert.ToString(_relationships.FromCardinality) \n == \n \"One\" \n && \n Convert.ToString(_relationships.ToCardinality) \n == \n \"One\").Count()) + \"\\n\"\n\n + \" │ └─ Inactive: \" + \n Convert.ToString(Model.Relationships.Where(\n _relationships => \n Convert.ToString(_relationships.IsActive) \n == \n \"False\").Count()) + \"\\n\"\n\n + \" │\\n\"\n + \" ├─ Calculation Groups: \" + \n Convert.ToString(_calcgroups) + \"\\n\"\n + \" │ └─ Calculation Items: \" + \n Convert.ToString(_calcitems) + \"\\n\" \n + \" │\\n\"\n + \" ├─ Calculated Columns: \" + \n Convert.ToString(Model.AllColumns.Where(\n _columns => \n Convert.ToString(_columns.Type) \n == \n \"Calculated\").Count()) + \"\\n\"\n\n + \" │\\n\"\n + \" └─ Measures: \" + \n Convert.ToString(_measures) + \"\\n\" \n + \" └─ Avg. Lines of DAX: \" + \n Convert.ToString(_numLines) + \" Lines \\n\" \n + \" └─ Avg. Chars of DAX: \" + \n Convert.ToString(_numChars) + \" Characters \\n\\n\" \n \n + \"-----------------------------------------\\n\"\n + \"Other Objects\\n\"\n + \"-----------------------------------------\\n\"\n + \" ├─ Data Security Roles: \" + \n Convert.ToString(Model.Roles.Count()) + \"\\n\"\n + \" ├─ Explicit Data Sources: \" + \n Convert.ToString(Model.DataSources.Count()) + \"\\n\"\n + \" ├─ Perspectives: \" + \n Convert.ToString(Model.Perspectives.Count()) + \"\\n\"\n + \" └─ Translations: \" + \n Convert.ToString(Model.Cultures.Count()));", + "Tooltip": "", + "ValidContexts": "Model" + }, + { + "Id": 10, + "Name": "Formating\\Advanced\\Format Power Query", + "Enabled": "true", + "Execute": "// This script formats the Power Query (M Code) of any selected M Partition (not Shared Expression or Source Expression).\n// It will send an HTTPS POST request of the expression to the Power Query Formatter API and replace the code with the result.\n//\nusing System.Net.Http;\nusing System.Net.Http.Headers;\nusing System.Text;\nusing Newtonsoft.Json;\nusing Newtonsoft.Json.Linq;\n\n// URL of the powerqueryformatter.com API\nstring powerqueryformatterAPI = \"https://m-formatter.azurewebsites.net/api/v2\";\n\n// HttpClient method to initiate the API call POST method for the URL\nHttpClient client = new HttpClient();\nHttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, powerqueryformatterAPI);\n\n// Get the M Expression of the selected partition\nstring partitionExpression = Selected.Partition.Expression;\n\n// Serialize the request body as a JSON object\nvar requestBody = JsonConvert.SerializeObject(\n new { \n code = partitionExpression, \n resultType = \"text\", \n lineWidth = 40, \n alignLineCommentsToPosition = true, \n includeComments = true\n });\n\n// Set the \"Content-Type\" header of the request to \"application/json\" and the encoding to UTF-8\nvar content = new StringContent(requestBody, Encoding.UTF8, \"application/json\");\ncontent.Headers.ContentType = new MediaTypeHeaderValue(\"application/json\");\n\n// Retrieve the response\nvar response = client.PostAsync(powerqueryformatterAPI, content).Result;\n\n// If the response is successful\nif (response.IsSuccessStatusCode)\n{\n // Get the result of the response\n var result = response.Content.ReadAsStringAsync().Result;\n\n // Parse the response JSON object from the string\n JObject data = JObject.Parse(result.ToString());\n\n // Get the formatted Power Query response\n string formattedPowerQuery = (string)data[\"result\"];\n\n ///////////////////////////////////////////////////////////////////////\n // OPTIONAL MANUAL FORMATTING\n // Manually add a new line and comment to each step\n var replace = new Dictionary \n { \n { \" //\", \"\\n\\n//\" }, \n { \"\\n #\", \"\\n\\n // Step\\n #\" }, \n { \"\\n Source\", \"\\n\\n // Data Source\\n Source\" }, \n { \"\\n Dataflow\", \"\\n\\n // Dataflow Connection Info\\n Dataflow\" }, \n {\"\\n Data =\", \"\\n\\n // Step\\n Data =\"}, \n {\"\\n Navigation =\", \"\\n\\n // Step\\n Navigation =\"}, \n {\"in\\n\\n // Step\\n #\", \"in\\n #\"}, \n {\"\\nin\", \"\\n\\n// Result\\nin\"} \n };\n\n // Replace the first string in the dictionary with the second\n var manuallyformattedPowerQuery = replace.Aggregate(\n formattedPowerQuery, \n (before, after) => before.Replace(after.Key, after.Value));\n\n // Replace the auto-formatted code with the manually formatted version\n formattedPowerQuery = manuallyformattedPowerQuery;\n ////////////////////////////////////////////////////////////////////////\n\n // Replace the unformatted M expression with the formatted expression\n Selected.Partition.Expression = formattedPowerQuery;\n\n // Pop-up to inform of completion\n Info(\"Formatted \" + Selected.Partition.Name);\n}\n\n// Otherwise return an error message\nelse\n{\nInfo(\n \"API call unsuccessful.\" +\n \"\\nCheck that you are selecting a partition with a valid M Expression.\"\n );\n}", + "Tooltip": "", + "ValidContexts": "Partition" + }, + { + "Id": 11, + "Name": "Modelling\\Beginner\\Create a new M Partition", + "Enabled": "true", + "Execute": "// This script creates a new M parameter in the 'Shared Expressions' of a model.\n//\n// Create a new shared expression called \"New Parameter\"\nModel.AddExpression( \n \"New Parameter\", \n @\"\n\"\"Parameter Text\"\" meta\n[\n\tIsParameterQuery = true,\n\tIsParameterQueryRequired = true,\n\tType = type text\n]\"\n);\n\n// Provides an output informing how to configure and use the parameter\nInfo ( \n \"Created a new Shared Expression called 'New Parameter', which is an M Parameter template.\" + \n \"\\n------------------------------------------------------\\n\" + \n \"To configure:\" +\n \"\\n------------------------------------------------------\\n \" + \n \"1. Replace the text 'New Parameter' with the desired parameter value\\n \" +\n \"2. Set the data type appropriately\\n \" +\n \"3. Replace any values found in the M partitions with the parameter reference.\" );", + "Tooltip": "", + "ValidContexts": "Model" + }, + { + "Id": 12, + "Name": "Modelling\\Beginner\\Edit Hidden Partitions", + "Enabled": "true", + "Execute": "Selected.Table.Partitions[0].Output();", + "Tooltip": "", + "ValidContexts": "Table" + }, + { + "Id": 13, + "Name": "Measures\\Beginner\\Find & Replace Substring in Measures", + "Enabled": "true", + "Execute": "#r \"System.Drawing\"\n\nusing System.Drawing;\nusing System.Text.RegularExpressions;\nusing System.Windows.Forms;\n\n// Hide the 'Running Macro' spinbox\nScriptHelper.WaitFormVisible = false;\n\n// Replace Selected.Measures with Model.AllMeasures to scan all measures\nvar _measures = Model.AllMeasures;\n // Optional: Replace _m.Expression with _m.Name to find & replace in names.\n\n// Initialize _find and _replace string variables\nstring _find = \"Find\";\nstring _replace = \"Replace\";\n\n// WinForms prompt to get Find & Replace input\nusing (Form prompt = new Form())\n{\n Font formFont = new Font(\"Segoe UI\", 11); \n\n // Prompt config\n prompt.AutoSize = true;\n prompt.MinimumSize = new Size(350, 120);\n prompt.Text = \"Find and Replace Dialog\";\n prompt.StartPosition = FormStartPosition.CenterScreen;\n\n // Set the AutoScaleMode property to Dpi\n prompt.AutoScaleMode = AutoScaleMode.Dpi;\n\n // Find: label\n Label findLabel = new Label() { Text = \"Find:\" };\n findLabel.Location = new Point(20, 20);\n findLabel.Width = 80;\n findLabel.Font = formFont;\n\n // Textbox for inputing the substring text\n TextBox findBox = new TextBox();\n findBox.Width = 200;\n findBox.Location = new Point(findLabel.Location.X + findLabel.Width + 20, findLabel.Location.Y - 4);\n findBox.SelectedText = \"Find this Text\";\n findBox.Font = formFont;\n\n // Replace: label\n Label replaceLabel = new Label() { Left = 20, Top = 60, Text = \"Replace:\" };\n replaceLabel.Width = 80;\n replaceLabel.Font = formFont;\n\n // Textbox for inputting the substring text\n TextBox replaceBox = new TextBox() { Left = replaceLabel.Right + 20, Top = replaceLabel.Location.Y - 4, Width = findBox.Width };\n replaceBox.SelectedText = \"Replace with this Text\";\n replaceBox.Font = formFont;\n\n // OK Button\n Button okButton = new Button() { Text = \"OK\", Left = 20, Width = 75, Top = replaceBox.Location.Y + replaceBox.Height + 20 };\n okButton.MinimumSize = new Size(75, 25);\n okButton.AutoSize = true;\n okButton.Font = formFont;\n\n // Cancel Button\n Button cancelButton = new Button() { Text = \"Cancel\", Left = okButton.Location.X + okButton.Width + 10, Top = okButton.Location.Y };\n cancelButton.MinimumSize = new Size(75, 25);\n cancelButton.AutoSize = true;\n cancelButton.Font = formFont;\n\n // Button actions\n okButton.Click += (sender, e) => { _find = findBox.Text; _replace = replaceBox.Text; prompt.DialogResult = DialogResult.OK; };\n cancelButton.Click += (sender, e) => { prompt.DialogResult = DialogResult.Cancel; };\n\n prompt.AcceptButton = okButton;\n prompt.CancelButton = cancelButton;\n\n prompt.Controls.Add(findLabel);\n prompt.Controls.Add(findBox);\n prompt.Controls.Add(replaceLabel);\n prompt.Controls.Add(replaceBox);\n prompt.Controls.Add(okButton);\n prompt.Controls.Add(cancelButton);\n\n // The user clicked OK, so perform the find-and-replace logic\n if (prompt.ShowDialog() == DialogResult.OK)\n {\n \n int _occurrences = 0;\n var _ReplacedList = new List();\n \n foreach (var _m in _measures)\n {\n if (_m.Expression != _m.Expression.Replace(_find, _replace))\n {\n try\n {\n // Count number of occurrences of _find substring in the string\n string _pattern = Regex.Escape(_find);\n _occurrences = Regex.Matches(_m.Expression, _pattern).Count;\n }\n catch\n {\n // If it's not found there are 0 occurrences\n _occurrences = 0;\n }\n \n // Perform the Find/Replace\n _m.Expression = _m.Expression.Replace(_find, _replace);\n _ReplacedList.Add(_m.DaxObjectName);\n }\n }\n \n // Create a list of all the measures replaced\n string _Replaced = _ReplacedList.Count > 0\n ? \"\\n\\nMeasures with Replacements:\\n • \" + string.Join(\"\\n • \", _ReplacedList)\n : \"\";\n \n // Return a success Info box pop-up\n Info(\n \"Replaced \" + \n _occurrences + \n \" occurrences of '\" + \n _find + \n \"' with '\" + \n _replace + \n \"'\" + \n _Replaced);\n }\n else\n {\n Error(\"Find/Replace cancelled!\");\n }\n}", + "Tooltip": "", + "ValidContexts": "Model" } ] } \ No newline at end of file diff --git a/common/CSharpScripts/Advanced/script-remove-measures-with-error.md b/common/CSharpScripts/Advanced/script-remove-measures-with-error.md index 1f4210f3..53b510fd 100644 --- a/common/CSharpScripts/Advanced/script-remove-measures-with-error.md +++ b/common/CSharpScripts/Advanced/script-remove-measures-with-error.md @@ -18,14 +18,16 @@ If you want to see all the measures that have errors and have the option to dele ```csharp // This script scans the model and shows all measures with errors, giving the option to remove them. // -// .GetSemantics(...) method is only available in TE3 +// .GetCachedSemantics(...) method is only available in TE3 using System.Windows.Forms; // Hide the 'Running Macro' spinbox ScriptHelper.WaitFormVisible = false; // Get all the measures that have errors -var measuresWithError = Model.AllMeasures.Where(m => m.GetSemantics(ExpressionProperty.Expression).HasError).ToList(); +var measuresWithError = Model.AllMeasures.Where(m => m.GetCachedSemantics(ExpressionProperty.Expression).HasError).ToList(); +//Prior to Tabular Editor 3.12.0 the GetSemantics method must be used. +//var measuresWithError = Model.AllMeasures.Where(m => m.GetSemantics(ExpressionProperty.Expression).HasError).ToList(); // If no measures with errors, end script with error. if ( measuresWithError.Count == 0 ) @@ -95,6 +97,7 @@ var _ToDelete = SelectObjects(measuresWithError, measuresWithError, "Select meas Info ( "No measure selected." ); } } + ``` ### Explanation This snippet gets all the measures that have errors according to the Tabular Editor Semantic Analysis. It then will display them in an output box where you can manually browse them or make changes. Thereafter, measures can be selected for removal. The removed measures can be saved as a back-up .tsv file in case you want to import them, later. diff --git a/common/CSharpScripts/Beginner/script-count-things.md b/common/CSharpScripts/Beginner/script-count-things.md index af963e6b..6d4d0aa4 100644 --- a/common/CSharpScripts/Beginner/script-count-things.md +++ b/common/CSharpScripts/Beginner/script-count-things.md @@ -189,7 +189,7 @@ Info ( "In the model, we see the below objects:\n\n" ``` ### Explanation This snippet goes through the model and counts the different object types, displaying them in a hierarchical "node and tree" format that is manually constructed. -You can comment out +You can comment out the parts that you do not need for your purposes. ## Example Output diff --git a/common/CSharpScripts/Beginner/script-create-measure-table.md b/common/CSharpScripts/Beginner/script-create-measure-table.md new file mode 100644 index 00000000..44d9b2ca --- /dev/null +++ b/common/CSharpScripts/Beginner/script-create-measure-table.md @@ -0,0 +1,24 @@ +--- +uid: script-create-measure-table +title: Create Measure Table +author: Morten Lønskov +updated: 2023-11-29 +applies_to: + versions: + - version: 2.x + - version: 3.x +--- +# Create Measure Table + +## Script Purpose +The scripts creates a hidden measure table containing one hidden column + + +## Script + +### Create Measure Table +```csharp +// Create a calculated table with a single column which is hidden: +var table = Model.AddCalculatedTable("Model Measures", "{0}"); +table.Columns[0].IsHidden = true; +``` \ No newline at end of file diff --git a/common/CSharpScripts/Beginner/create-sum-measures-from-columns.md b/common/CSharpScripts/Beginner/script-create-sum-measures-from-columns.md similarity index 97% rename from common/CSharpScripts/Beginner/create-sum-measures-from-columns.md rename to common/CSharpScripts/Beginner/script-create-sum-measures-from-columns.md index 66050a7e..736db555 100644 --- a/common/CSharpScripts/Beginner/create-sum-measures-from-columns.md +++ b/common/CSharpScripts/Beginner/script-create-sum-measures-from-columns.md @@ -1,5 +1,5 @@ --- -uid: create-sum-measures-from-columns +uid: script-create-sum-measures-from-columns title: Create SUM Measure from Column author: Morten Lønskov updated: 2023-02-22 diff --git a/common/CSharpScripts/Beginner/script-create-table-groups.md b/common/CSharpScripts/Beginner/script-create-table-groups.md new file mode 100644 index 00000000..3a20ebc3 --- /dev/null +++ b/common/CSharpScripts/Beginner/script-create-table-groups.md @@ -0,0 +1,55 @@ +--- +uid: script-create-table-groups +title: Create Table Groups +author: Morten Lønskov +updated: 2023-11-29 +applies_to: + versions: + - version: 3.x +--- +# Create Table Groups + +## Script Purpose +This script creates default table groups within Tabular Editor 3. + +## Script + +### Script Title +```csharp +// Loop through all tables: +foreach(var table in Model.Tables) +{ + if (table is CalculationGroupTable) + { + table.TableGroup = "Calculation Groups"; + } + else if (!table.UsedInRelationships.Any() && table.Measures.Any(m => m.IsVisible)) + { + // Tables containing visible measures, but no relationships to other tables + table.TableGroup = "Measure Groups"; + } + else if (table.UsedInRelationships.All(r => r.FromTable == table) && table.UsedInRelationships.Any()) + { + // Tables exclusively on the "many" side of relationships: + table.TableGroup = "Facts"; + } + else if (!table.UsedInRelationships.Any() && table is CalculatedTable && !table.Measures.Any()) + { + // Tables without any relationships, that are Calculated Tables and do not have measures: + table.TableGroup = "Parameter Tables"; + } + else if (table.UsedInRelationships.Any(r => r.ToTable == table)) + { + // Tables on the "one" side of relationships: + table.TableGroup = "Dimensions"; + } + else + { + // All other tables: + table.TableGroup = "Misc"; + } +} +``` +### Explanation +The scripts loops through all tables in the model assigning a table group according to specific properties. + diff --git a/common/CSharpScripts/Beginner/script-format-numeric-measures.md b/common/CSharpScripts/Beginner/script-format-numeric-measures.md new file mode 100644 index 00000000..55455974 --- /dev/null +++ b/common/CSharpScripts/Beginner/script-format-numeric-measures.md @@ -0,0 +1,44 @@ +--- +uid: script-format-numeric-measures +title: Format Numeric Measures +author: Morten Lønskov +updated: 2023-11-29 +applies_to: + versions: + - version: 2.x + - version: 3.x +--- +# Format Numeric Measures + +## Script Purpose +Allows you to quickly set default format strings on the measures selected. + +

+> [!NOTE] +> The script uses certain naming standards so you might wish to adjust it to suit yours. +

+ +## Script + +### Script Title +```csharp +// This script is meant to format all measures with a default formatstring +foreach (var ms in Selected.Measures) { +//Don't set format string on hidden measures + if (ms.IsHidden) continue; +// If the format string is empty continue. + if (!string.IsNullOrWhiteSpace(ms.FormatString)) continue; +//If the data type is int set a whole number format string + if (ms.DataType == DataType.Int64) ms.FormatString = "#,##0"; +//If the datatype is double or decimal + if (ms.DataType == DataType.Double || ms.DataType == DataType.Decimal) { + //and the name contains # or QTY then set the format string to a whole number + if (ms.Name.Contains("#") + || ms.Name.IndexOf("QTY", StringComparison.OrdinalIgnoreCase) >= 0) ms.FormatString = "#,##0"; + //otherwise set it a decimal format string. + else ms.FormatString = "#,##0.00"; + } +} +``` +### Explanation +The script takes each of the selected measures and loops through them to set a default format string according to various conditions. \ No newline at end of file diff --git a/common/toc.md b/common/toc.md index 1835f936..bd1145e8 100644 --- a/common/toc.md +++ b/common/toc.md @@ -18,17 +18,21 @@ ### @script-library-beginner #### @script-count-rows -#### @script-count-things #### @script-edit-hidden-partitions #### @script-create-m-parameter -#### @create-sum-measures-from-columns -#### @script-find-replace +#### @script-create-sum-measures-from-columns +#### @script-create-measure-table +#### @script-format-numeric-measures +#### @script-create-table-groups + ### @script-library-advanced +#### @script-count-things #### @script-create-date-table #### @script-create-and-replace-parameter #### @script-format-power-query #### @script-implement-incremental-refresh #### @script-remove-measures-with-error +#### @script-find-replace ## @script-helper-methods \ No newline at end of file diff --git a/te3/features/supported-files.md b/te3/features/supported-files.md index 69ea7f2b..416fca4d 100644 --- a/te3/features/supported-files.md +++ b/te3/features/supported-files.md @@ -189,6 +189,7 @@ It can be helpful to share these files across a team so that all developers have > A windows native way of syncing a version controlled file into the "%localappdata%\TabularEditor3" folder is to use [SymLink](https://www.howtogeek.com/16226/complete-guide-to-symbolic-links-symlinks-on-windows-or-linux/). > > Store the required files in Git or OneDrive and create a Symlink to the "%localappdata%\TabularEditor3" folder, but be aware that this could end up with synchronization issues, if multiple users update the same file version. +> However, this is not supported by Tabular Editor directly, so implement it at your own discretion. ### MacroActions.json diff --git a/te3/features/table-groups.md b/te3/features/table-groups.md index b884c6e0..56b3a083 100644 --- a/te3/features/table-groups.md +++ b/te3/features/table-groups.md @@ -39,21 +39,21 @@ foreach(var table in Model.Tables) { table.TableGroup = "Calculation Groups"; } - else if (!table.UsedInRelationships.Any()) + else if (!table.UsedInRelationships.Any() && table.Measures.Any(m => m.IsVisible)) { - // Tables without any relationships: - table.TableGroup = "Parameter Tables"; - } - else if (table.IsHidden && table.Measures.Any(m => m.IsVisible)) - { - // Hidden tables containing visible measures: + // Tables containing visible measures, but no relationships to other tables table.TableGroup = "Measure Groups"; } - else if (table.UsedInRelationships.All(r => r.FromTable == table)) + else if (table.UsedInRelationships.All(r => r.FromTable == table) && table.UsedInRelationships.Any()) { // Tables exclusively on the "many" side of relationships: table.TableGroup = "Facts"; } + else if (!table.UsedInRelationships.Any() && table is CalculatedTable && !table.Measures.Any()) + { + // Tables without any relationships, that are Calculated Tables and do not have measures: + table.TableGroup = "Parameter Tables"; + } else if (table.UsedInRelationships.Any(r => r.ToTable == table)) { // Tables on the "one" side of relationships: