├── README.md ├── package-icon.png ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ └── ci.yml ├── src ├── Dax.Template │ ├── Syntax │ │ ├── DaxBase.cs │ │ ├── IGlobalScope.cs │ │ ├── VarScope.cs │ │ ├── VarRow.cs │ │ ├── IDaxComment.cs │ │ ├── IDaxName.cs │ │ ├── VarGlobal.cs │ │ ├── IDependencies.cs │ │ ├── DaxStep.cs │ │ ├── Var.cs │ │ └── DaxElement.cs │ ├── Model │ │ ├── DateColumn.cs │ │ ├── Level.cs │ │ ├── EntityBase.cs │ │ ├── Hierarchy.cs │ │ ├── Measure.cs │ │ └── Column.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Enums │ │ ├── AutoNamingEnum.cs │ │ └── AutoScanEnum.cs │ ├── Constants │ │ ├── Prefixes.cs │ │ └── Attributes.cs │ ├── Exceptions │ │ ├── ExistingTableException.cs │ │ ├── InvalidAttributeException.cs │ │ ├── InvalidVariableReferenceException.cs │ │ ├── CircularDependencyException.cs │ │ ├── InvalidConfigurationException.cs │ │ ├── InvalidMacroReferenceException.cs │ │ └── TemplateException.cs │ ├── Interfaces │ │ ├── ICustomTableConfig.cs │ │ ├── ILocalization.cs │ │ ├── IHolidaysConfig.cs │ │ ├── IDateTemplateConfig.cs │ │ ├── IScanConfig.cs │ │ ├── IMeasureTemplateConfig.cs │ │ └── ITemplates.cs │ ├── AssemblyInfo.cs │ ├── GlobalSuppressions.cs │ ├── Tables │ │ ├── Dates │ │ │ ├── HolidaysConfig.cs │ │ │ ├── CustomDateTable.cs │ │ │ ├── HolidaysDefinitionTable.cs │ │ │ └── SimpleDateTable.cs │ │ ├── ReferenceCalculatedTable.cs │ │ └── TemplateConfiguration.cs │ ├── Extensions │ │ ├── GetDependencies.cs │ │ ├── StringExtensions.cs │ │ ├── ReflectionHelper.cs │ │ ├── ComputeDependencies.cs │ │ ├── GetScanColumns.cs │ │ └── TSort.cs │ ├── Dax.Template.csproj │ ├── CustomTemplateDefinition.cs │ ├── Translations.cs │ └── Package.cs ├── Dax.Template.TestUI │ ├── Properties │ │ └── launchSettings.json │ ├── Templates │ │ ├── HolidaysConfig.json │ │ ├── Config-99 - Date.template.json │ │ ├── Config-98 - DateHolidays.template.json │ │ ├── Config-03 - Monthly Gregorian.template.json │ │ ├── Config-04 - Monthly Fiscal.template.json │ │ ├── Config-05 - Custom Gregorian.template.json │ │ ├── Config-06 - Custom Fiscal.template.json │ │ ├── Config-95 - Standard dates no text.template.json │ │ ├── Config-01 - Standard Gregorian.template.json │ │ ├── Config-02 - Standard Fiscal.template.json │ │ ├── Config-07 - Weekly.template.json │ │ ├── DateTemplate-03.json │ │ ├── DateTemplate-01.json │ │ ├── DateTemplate-95.json │ │ ├── DateTemplate-02.json │ │ ├── DateTemplate-04.json │ │ ├── DateTemplate-05.json │ │ └── DateTemplate-06.json │ ├── Dax.Template.TestUI.csproj │ ├── Program.cs │ └── ApplyDaxTemplate.resx ├── Dax.Template.Tests │ ├── Dax.Template.Tests.csproj │ ├── PackageTests.cs │ └── _data │ │ └── Templates │ │ ├── Config-01 - Standard.template.json │ │ └── DateTemplate-01.json └── Dax.Template.sln ├── global.json ├── nuget.config ├── LICENSE.md ├── .gitattributes ├── .azure └── pipelines │ └── ci.yml ├── .gitignore └── .editorconfig /README.md: -------------------------------------------------------------------------------- 1 | # DaxTemplate 2 | Tabular and DAX template engine 3 | -------------------------------------------------------------------------------- /package-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sql-bi/DaxTemplate/HEAD/package-icon.png -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @marcosqlbi 2 | 3 | /.azure/ @albertospelta 4 | /.github/ @albertospelta -------------------------------------------------------------------------------- /src/Dax.Template/Syntax/DaxBase.cs: -------------------------------------------------------------------------------- 1 | namespace Dax.Template.Syntax 2 | { 3 | public abstract class DaxBase 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/Dax.Template/Model/DateColumn.cs: -------------------------------------------------------------------------------- 1 | namespace Dax.Template.Model 2 | { 3 | public class DateColumn : Column 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "8.0.400", 4 | "allowPrerelease": false, 5 | "rollForward": "latestFeature" 6 | } 7 | } -------------------------------------------------------------------------------- /src/Dax.Template/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Dax.Template": { 4 | "commandName": "Project" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /src/Dax.Template/Syntax/IGlobalScope.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | namespace Dax.Template.Syntax 3 | { 4 | internal interface IGlobalScope 5 | { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Dax.Template/Syntax/VarScope.cs: -------------------------------------------------------------------------------- 1 | namespace Dax.Template.Syntax 2 | { 3 | public enum VarScope 4 | { 5 | Global, 6 | Row 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Dax.Template/Enums/AutoNamingEnum.cs: -------------------------------------------------------------------------------- 1 | namespace Dax.Template.Enums 2 | { 3 | public enum AutoNamingEnum 4 | { 5 | Suffix = 0, 6 | Prefix 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Dax.Template/Syntax/VarRow.cs: -------------------------------------------------------------------------------- 1 | namespace Dax.Template.Syntax 2 | { 3 | public class VarRow : Var 4 | { 5 | public VarRow() { Scope = VarScope.Row; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Dax.Template/Syntax/IDaxComment.cs: -------------------------------------------------------------------------------- 1 | namespace Dax.Template.Syntax 2 | { 3 | public interface IDaxComment 4 | { 5 | public string[]? Comments { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Dax.Template/Syntax/IDaxName.cs: -------------------------------------------------------------------------------- 1 | namespace Dax.Template.Syntax 2 | { 3 | public interface IDaxName : IDependencies 4 | { 5 | public string DaxName { get; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Dax.Template/Constants/Prefixes.cs: -------------------------------------------------------------------------------- 1 | namespace Dax.Template.Constants 2 | { 3 | public static class Prefixes 4 | { 5 | public const string CONFLICT_RENAME_PREFIX = "_old"; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/Dax.Template/Exceptions/ExistingTableException.cs: -------------------------------------------------------------------------------- 1 | namespace Dax.Template.Exceptions 2 | { 3 | public class ExistingTableException : TemplateException 4 | { 5 | public ExistingTableException(string message) : base(message) { } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Dax.Template/Syntax/VarGlobal.cs: -------------------------------------------------------------------------------- 1 | namespace Dax.Template.Syntax 2 | { 3 | public class VarGlobal : Var, IGlobalScope 4 | { 5 | public VarGlobal() { Scope = VarScope.Global; } 6 | public bool IsConfigurable { get; set; } = false; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Dax.Template.TestUI/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Dax.Template.TestUI": { 4 | "commandName": "Project", 5 | "commandLineArgs": "--server=\"localhost:57766\" --database=\"0aac490b-4197-498b-8488-95d89c44970c\"" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /src/Dax.Template.TestUI/Templates/HolidaysConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsoCountry": "US", 3 | "InLieuOfPrefix": "(in lieu of ", 4 | "InLieuOfSuffix": ")", 5 | "HolidayDefinitionTable": "HolidaysDefinition", 6 | "FirstYear": 2000, 7 | "LastYear": 2022, 8 | "WorkingDays": "{ 2, 3, 4, 5, 6 }" 9 | } -------------------------------------------------------------------------------- /src/Dax.Template/Interfaces/ICustomTableConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Dax.Template.Interfaces 4 | { 5 | public interface ICustomTableConfig: IScanConfig 6 | { 7 | public Dictionary DefaultVariables { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Dax.Template/Interfaces/ILocalization.cs: -------------------------------------------------------------------------------- 1 | namespace Dax.Template.Interfaces 2 | { 3 | public interface ILocalization 4 | { 5 | public string? IsoTranslation { get; set; } 6 | public string? IsoFormat { get; set; } 7 | public string[]? LocalizationFiles { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Dax.Template/Exceptions/InvalidAttributeException.cs: -------------------------------------------------------------------------------- 1 | namespace Dax.Template.Exceptions 2 | { 3 | public class InvalidAttributeException : TemplateException 4 | { 5 | public InvalidAttributeException(string attributeValue, string entitymessage) 6 | : base($"Invalid attribute type {attributeValue} in entity {entitymessage}") { } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Dax.Template/Exceptions/InvalidVariableReferenceException.cs: -------------------------------------------------------------------------------- 1 | namespace Dax.Template.Exceptions 2 | { 3 | public class InvalidVariableReferenceException : TemplateException 4 | { 5 | public InvalidVariableReferenceException(string variableName, string daxExpressionmessage) 6 | : base( $"Invalid variable reference {variableName} in DAX expression: {daxExpressionmessage}") { } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "nuget" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | open-pull-requests-limit: 10 8 | assignees: 9 | - "albertospelta" 10 | 11 | - package-ecosystem: "github-actions" 12 | directory: "/" 13 | schedule: 14 | interval: "weekly" 15 | open-pull-requests-limit: 10 16 | assignees: 17 | - "albertospelta" -------------------------------------------------------------------------------- /src/Dax.Template/Model/Level.cs: -------------------------------------------------------------------------------- 1 | using TabularLevel = Microsoft.AnalysisServices.Tabular.Level; 2 | 3 | namespace Dax.Template.Model 4 | { 5 | public class Level : EntityBase 6 | { 7 | public Column Column { get; init; } = default!; 8 | internal TabularLevel? TabularLevel { get; set; } 9 | public override void Reset() 10 | { 11 | TabularLevel = null; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Dax.Template/Syntax/IDependencies.cs: -------------------------------------------------------------------------------- 1 | namespace Dax.Template.Syntax 2 | { 3 | public interface IDependencies where T : DaxBase 4 | { 5 | public bool AddLevel { get; init; } 6 | public bool IgnoreAutoDependency { get; init; } 7 | public IDependencies[]? Dependencies { get; set; } 8 | public string? Expression { get; set; } 9 | 10 | public string GetDebugInfo(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Dax.Template/Exceptions/CircularDependencyException.cs: -------------------------------------------------------------------------------- 1 | namespace Dax.Template.Exceptions 2 | { 3 | public class CircularDependencyException : TemplateException 4 | { 5 | public CircularDependencyException(string? variableName, string? daxExpressionmessage) 6 | : base($"Circulare dependency in variable definition {variableName??"[undefined]"} with DAX expression: {daxExpressionmessage??"[undefined]"}") { } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Dax.Template/Interfaces/IHolidaysConfig.cs: -------------------------------------------------------------------------------- 1 | namespace Dax.Template.Interfaces 2 | { 3 | public interface IHolidaysConfig: IDateTemplateConfig 4 | { 5 | public string? IsoCountry { get; set; } 6 | public string? InLieuOfPrefix { get; set; } 7 | public string? InLieuOfSuffix { get; set; } 8 | public string? HolidaysDefinitionTable { get; set; } 9 | public string? WorkingDays { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Dax.Template/Interfaces/IDateTemplateConfig.cs: -------------------------------------------------------------------------------- 1 | namespace Dax.Template.Interfaces 2 | { 3 | public interface IDateTemplateConfig : ICustomTableConfig 4 | { 5 | public int? FirstYearMin { get; set; } 6 | public int? FirstYearMax { get; set; } 7 | public int? LastYearMin { get; set; } 8 | public int? LastYearMax { get; set; } 9 | 10 | public Tables.Dates.HolidaysConfig? HolidaysReference { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Dax.Template/Interfaces/IScanConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using Dax.Template.Enums; 3 | 4 | namespace Dax.Template.Interfaces 5 | { 6 | public interface IScanConfig 7 | { 8 | public string[]? OnlyTablesColumns { get; set; } 9 | public string[]? ExceptTablesColumns { get; set; } 10 | 11 | [JsonConverter(typeof(JsonStringEnumConverter))] 12 | public AutoScanEnum? AutoScan { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Dax.Template/Exceptions/InvalidConfigurationException.cs: -------------------------------------------------------------------------------- 1 | namespace Dax.Template.Exceptions 2 | { 3 | public class InvalidConfigurationException : TemplateException 4 | { 5 | public InvalidConfigurationException(string message) 6 | : base(message) { } 7 | 8 | public InvalidConfigurationException(string variableName, string value) 9 | : base($"Global variable {variableName} not found to assign the default value {value}") { } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Dax.Template/Model/EntityBase.cs: -------------------------------------------------------------------------------- 1 | namespace Dax.Template.Model 2 | { 3 | public abstract class EntityBase 4 | { 5 | public string Name { get; init; } = default!; 6 | public string? Description { get; set; } 7 | 8 | /// 9 | /// Reset internal state for Tabular objects 10 | /// 11 | public abstract void Reset(); 12 | public override string ToString() 13 | { 14 | return $"{GetType().Name} : {Name}"; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Dax.Template.TestUI/Templates/Config-99 - Date.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "Description": "Standard calendar", 3 | "Templates": [ 4 | { 5 | "Class": "CustomDateTable", 6 | "Table": "Date", 7 | "ReferenceTable": "DateTemplate", 8 | "Template": "DateTemplate-01.json", 9 | "LocalizationFiles": [ 10 | ] 11 | } 12 | ], 13 | "FirstYear": null, 14 | "LastYear": null, 15 | "AutoScan": "Full", 16 | "OnlyTablesColumns": [], 17 | "ExceptTablesColumns": [], 18 | "DefaultVariables": { 19 | "__FirstFiscalMonth": "4", 20 | "__FirstDayOfWeek": "0" 21 | } 22 | } -------------------------------------------------------------------------------- /src/Dax.Template/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | 4 | #if SIGNED 5 | [assembly: InternalsVisibleTo("Dax.Template.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010039665474541e22abe13943b14b31bc8af889efb4e8f4c121b3972aece355b441903e0f40cd077752e6843385806db630730d03a4d6127f5ca13e5b2770989f5da7e66ba79d02212c6cd647e40eff4a72aa51def4fcc40c8170779f5d028531a9982d62adf5448b300f19d756a7222a9154abde0339b47e48211a4499112016c9")] 6 | #else 7 | [assembly: InternalsVisibleTo("Dax.Template.Tests")] 8 | #endif 9 | 10 | [assembly: CLSCompliant(true)] -------------------------------------------------------------------------------- /src/Dax.Template/Model/Hierarchy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using TabularHierarchy = Microsoft.AnalysisServices.Tabular.Hierarchy; 3 | 4 | namespace Dax.Template.Model 5 | { 6 | public class Hierarchy : EntityBase 7 | { 8 | public string? DisplayFolder { get; init; } 9 | public bool IsHidden { get; init; } = false; 10 | public Level[] Levels { get; init; } = Array.Empty(); 11 | internal TabularHierarchy? TabularHierarchy { get; set; } 12 | public override void Reset() 13 | { 14 | TabularHierarchy = null; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Dax.Template.TestUI/Dax.Template.TestUI.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WinExe 5 | net8.0-windows 6 | enable 7 | true 8 | enable 9 | false 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Dax.Template/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | using System.Diagnostics.CodeAnalysis; 7 | 8 | [assembly: SuppressMessage("Style", "IDE0045:Convert to conditional expression", Justification = "", Scope = "member", Target = "~M:Dax.Template.Package.ReadDefinition``1(System.String)~``0")] 9 | [assembly: SuppressMessage("Style", "IDE0007:Use implicit type", Justification = "", Scope = "member", Target = "~M:Dax.Template.Package.ReadDefinition``1(System.String)~``0")] 10 | -------------------------------------------------------------------------------- /src/Dax.Template/Tables/Dates/HolidaysConfig.cs: -------------------------------------------------------------------------------- 1 | namespace Dax.Template.Tables.Dates 2 | { 3 | using System.Text.Json.Serialization; 4 | 5 | public class HolidaysConfig 6 | { 7 | [JsonIgnore] 8 | public bool IsEnabled { get; set; } = true; 9 | public string? TableName { get; set; } 10 | public string? DateColumnName { get; set; } 11 | public string? HolidayColumnName { get; set; } 12 | public static bool HasHolidays( HolidaysConfig? holidaysConfig) 13 | { 14 | return (holidaysConfig?.IsEnabled == true) && (holidaysConfig?.TableName != null) && (holidaysConfig?.DateColumnName != null) && (holidaysConfig.HolidayColumnName != null); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Dax.Template/Constants/Attributes.cs: -------------------------------------------------------------------------------- 1 | namespace Dax.Template.Constants 2 | { 3 | public static class Attributes 4 | { 5 | public const string SQLBI_TEMPLATE_ATTRIBUTE = "SQLBI_Template"; 6 | public const string SQLBI_TEMPLATETABLE_ATTRIBUTE = "SQLBI_TemplateTable"; 7 | public const string SQLBI_TEMPLATE_DATES = "Dates"; 8 | public const string SQLBI_TEMPLATE_HOLIDAYS = "Holidays"; 9 | public const string SQLBI_TEMPLATETABLE_DATE = "Date"; 10 | public const string SQLBI_TEMPLATETABLE_DATEAUTOTEMPLATE = "DateAutoTemplate"; 11 | public const string SQLBI_TEMPLATETABLE_HOLIDAYS = "Holidays"; 12 | public const string SQLBI_TEMPLATETABLE_HOLIDAYSDEFINITION = "HolidaysDefinition"; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Dax.Template/Interfaces/IMeasureTemplateConfig.cs: -------------------------------------------------------------------------------- 1 | using Dax.Template.Enums; 2 | using System.Collections.Generic; 3 | 4 | namespace Dax.Template.Interfaces 5 | { 6 | public interface IMeasureTemplateConfig : IScanConfig 7 | { 8 | public class TargetMeasure 9 | { 10 | public string? Name { get; set; } 11 | } 12 | public AutoNamingEnum? AutoNaming { get; set; } 13 | public string? AutoNamingSeparator { get; set; } 14 | // public IScanConfig DateColumns { get; set; } = new(); 15 | public TargetMeasure[]? TargetMeasures { get; set; } 16 | public string? TableSingleInstanceMeasures { get; set; } 17 | 18 | Dictionary DefaultVariables { get; set; } 19 | } 20 | } -------------------------------------------------------------------------------- /src/Dax.Template/Enums/AutoScanEnum.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Dax.Template.Enums 4 | { 5 | [Flags] 6 | public enum AutoScanEnum : short 7 | { 8 | /// 9 | /// Does not scan data to find min/max date 10 | /// 11 | Disabled = 0, 12 | /// 13 | /// Scan OnlyTablesColumns excluding ExceptTablesColumns 14 | /// 15 | SelectedTablesColumns = 1, 16 | /// 17 | /// Scan active relationships connected to the Date table 18 | /// 19 | ScanActiveRelationships = 2, 20 | /// 21 | /// Scan inactive relationships connected to the Date table 22 | /// 23 | ScanInactiveRelationships = 4, 24 | Full = 127 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Dax.Template/Syntax/DaxStep.cs: -------------------------------------------------------------------------------- 1 | namespace Dax.Template.Syntax 2 | { 3 | /// 4 | /// Explicit calculation step that can be included in the dependencies list. 5 | /// It is usually a table expression assigned to the variable specified in the Name property. 6 | /// The last DaxStep added to the list of dependencies is considered as a default reference for 7 | /// the following automatix DaxElement created to embed columns. 8 | /// 9 | public class DaxStep : DaxElement, IDaxName, IDaxComment 10 | { 11 | public string Name { get; init; } = default!; 12 | public string DaxName { get { return Name; } } 13 | public string[]? Comments { get; set; } 14 | 15 | public override string ToString() 16 | { 17 | return $"{GetType().Name} : {DaxName}"; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Dax.Template/Syntax/Var.cs: -------------------------------------------------------------------------------- 1 | namespace Dax.Template.Syntax 2 | { 3 | public abstract class Var : DaxBase, IDependencies, IDaxName, IDaxComment 4 | { 5 | bool IDependencies.AddLevel { get; init; } = false; 6 | public bool IgnoreAutoDependency { get; init; } = false; 7 | 8 | public VarScope Scope { get; init; } 9 | public string Name { get; init; } = default!; 10 | public string? Expression { get; set; } 11 | public string[]? Comments { get; set; } 12 | public string DaxName { get { return Name; } } 13 | 14 | public IDependencies[]? Dependencies { get; set; } 15 | public string GetDebugInfo() { return $"VAR {Name}: {Expression}"; } 16 | public override string ToString() 17 | { 18 | return $"{GetType().Name} : {Name}"; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Dax.Template/Extensions/GetDependencies.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Dax.Template.Syntax; 3 | 4 | namespace Dax.Template.Extensions 5 | { 6 | public static partial class Extensions 7 | { 8 | public static IEnumerable> GetDependencies(this IEnumerable> listDependencies, bool includeSelf = true) 9 | { 10 | var result = new List>(); 11 | foreach (var dep in listDependencies) 12 | { 13 | if (includeSelf) 14 | { 15 | result.Add(dep); 16 | } 17 | if (dep.Dependencies != null) 18 | { 19 | result.AddRange(dep.Dependencies); 20 | } 21 | } 22 | return result; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Dax.Template/Exceptions/InvalidMacroReferenceException.cs: -------------------------------------------------------------------------------- 1 | namespace Dax.Template.Exceptions 2 | { 3 | public class InvalidMacroReferenceException : TemplateException 4 | { 5 | public InvalidMacroReferenceException(string macro, string daxExpressionmessage) 6 | : base($"Invalid macro reference {macro} in DAX expression: {daxExpressionmessage}") { } 7 | 8 | public InvalidMacroReferenceException(string macro, string daxExpressionmessage, string additionalMessage) 9 | : base($"{additionalMessage} Invalid macro reference {macro} in DAX expression: {daxExpressionmessage}") { } 10 | 11 | public InvalidMacroReferenceException(string macro, string[] multipleMatches, string daxExpressionmessage) 12 | : base($"Multiple results ({string.Join(", ", multipleMatches)}) for macro reference {macro} in DAX expression: {daxExpressionmessage}") { } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Dax.Template/Interfaces/ITemplates.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Dax.Template.Interfaces 5 | { 6 | public interface ITemplates 7 | { 8 | public class TemplateEntry 9 | { 10 | public string? Class { get; set; } 11 | public string? Table { get; set; } 12 | public string? Template { get; set; } 13 | public string? ReferenceTable { get; set; } 14 | public string[] LocalizationFiles { get; set; } = Array.Empty(); 15 | public IMeasureTemplateConfig.TargetMeasure[] TargetMeasures { get; set; } = Array.Empty(); 16 | public bool IsHidden { get; set; } = false; 17 | public bool IsEnabled { get; set; } = true; 18 | public Dictionary Properties { get; set; } = new(); 19 | } 20 | 21 | public TemplateEntry[]? Templates { get; set; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Dax.Template/Model/Measure.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.AnalysisServices.Tabular; 4 | using TabularColumn = Microsoft.AnalysisServices.Tabular.Column; 5 | using TabularMeasure = Microsoft.AnalysisServices.Tabular.Measure; 6 | 7 | namespace Dax.Template.Model 8 | { 9 | public class Measure : EntityBase, Syntax.IDaxComment 10 | { 11 | public virtual string? Expression { get; set; } 12 | string DaxReference { get { return $"[{Name}]"; } } 13 | public string[]? Comments { get; set; } = Array.Empty(); 14 | public string? DisplayFolder { get; set; } 15 | public string? FormatString { get; set; } 16 | public bool IsHidden { get; set; } = false; 17 | public IEnumerable>? Annotations { get; set; } 18 | public override void Reset() 19 | { 20 | // Implement reset of references to Tabular entities 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build-and-test: 14 | name: build-and-test--${{ matrix.os-version }} 15 | runs-on: ${{ matrix.os-version }} 16 | strategy: 17 | matrix: 18 | os-version: [windows-latest] #, ubuntu-latest, macos-latest] 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: actions/setup-dotnet@v4 22 | with: 23 | dotnet-version: | 24 | 6.0.x 25 | global-json-file: global.json 26 | - name: restore 27 | run: dotnet restore ./src 28 | - name: build 29 | run: dotnet build ./src/Dax.Template.sln --configuration Release --no-restore 30 | - name: test 31 | run: dotnet test ./src/Dax.Template.Tests/Dax.Template.Tests.csproj --configuration Release --no-build --verbosity normal 32 | - name: pack 33 | run: dotnet pack ./src/Dax.Template/Dax.Template.csproj --configuration Release --no-build --no-restore --verbosity normal -------------------------------------------------------------------------------- /src/Dax.Template.TestUI/Program.cs: -------------------------------------------------------------------------------- 1 | namespace Dax.Template.TestUI 2 | { 3 | internal static class Program 4 | { 5 | /// 6 | /// The main entry point for the application. 7 | /// 8 | [STAThread] 9 | static void Main() 10 | { 11 | // To customize application configuration such as set high DPI settings or default font, 12 | // see https://aka.ms/applicationconfiguration. 13 | ApplicationConfiguration.Initialize(); 14 | Application.Run(new ApplyDaxTemplate()); 15 | } 16 | } 17 | } 18 | 19 | /* 20 | TODO 21 | 22 | combine different time intelligence into the same date table 23 | wildcard for measure selection 24 | Cascading match for IsoTranslation (support region only for translations files?) 25 | Add flag to expose parameters to Bravo UI 26 | 27 | Include multiple measure template files (calendar+fiscal) with different DisplayFolderRule 28 | API: 29 | - List of available templates 30 | - Include config for each model 31 | - Localization of description - it should be a reference to UI names 32 | */ -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 SQLBI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Dax.Template/Syntax/DaxElement.cs: -------------------------------------------------------------------------------- 1 | namespace Dax.Template.Syntax 2 | { 3 | using System.Xml.Linq; 4 | 5 | /// 6 | /// Internal use to create automatic DAX code in templates 7 | /// This could be partial code, it has no name because it is assigned internally 8 | /// to variables or to other DAX syntaxes. 9 | /// For example, it is used to create the GENERATE / ADDCOLUMNS functions to embed columns. 10 | /// Adding a DaxElement in the list of dependencies would replace the default DaxElement created 11 | /// for the level depending on the presence of variables. However, usually it is not used 12 | /// in the list of dependencies, using a DaxStep instead. 13 | /// 14 | public class DaxElement : DaxBase, IDependencies 15 | { 16 | bool IDependencies.AddLevel { get; init; } = true; 17 | public bool IgnoreAutoDependency { get; init; } = false; 18 | public string? Expression { get; set; } 19 | 20 | public IDependencies[]? Dependencies { get; set; } 21 | public string GetDebugInfo() { return $"{GetType().Name}: {Expression}"; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Dax.Template.TestUI/Templates/Config-98 - DateHolidays.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "Description": "Standard calendar with holidays", 3 | "Templates": [ 4 | { 5 | "Class": "HolidaysDefinitionTable", 6 | "Table": "HolidaysDefinition", 7 | "Template": "HolidaysDefinition.json", 8 | "IsHidden": true 9 | }, 10 | { 11 | "Class": "HolidaysTable", 12 | "Table": "Holidays", 13 | "Template": null 14 | }, 15 | { 16 | "Class": "CustomDateTable", 17 | "Table": "Date", 18 | "ReferenceTable": "DateTemplate", 19 | "Template": "DateTemplate-01.json" 20 | } 21 | ], 22 | "FirstYear": 0, 23 | "LastYear": 9999, 24 | 25 | "AutoScan": "Full", 26 | "OnlyTablesColumns": [], 27 | "ExceptTablesColumns": [], 28 | 29 | "IsoCountry": "US", 30 | "InLieuOfPrefix": "(in lieu of ", 31 | "InLieuOfSuffix": ")", 32 | "WorkingDays": "{ 2, 3, 4, 5, 6 }", 33 | "HolidaysDefinitionTable": "HolidaysDefinition", 34 | "HolidaysReference": { 35 | "TableName": "Holidays", 36 | "DateColumnName": "Holiday Date", 37 | "HolidayColumnName": "Holiday Name" 38 | }, 39 | 40 | "DefaultVariables": { 41 | "__FirstFiscalMonth": "4", 42 | "__FirstDayOfWeek": "0" 43 | } 44 | } -------------------------------------------------------------------------------- /src/Dax.Template/Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Dax.Template.Extensions 2 | { 3 | using System; 4 | 5 | internal static class StringExtensions 6 | { 7 | public static bool IsNullOrEmpty(this string? value) 8 | { 9 | return string.IsNullOrEmpty(value); 10 | } 11 | 12 | public static bool EqualsI(this string? current, string? value) 13 | { 14 | return current?.Equals(value, StringComparison.OrdinalIgnoreCase) ?? false; 15 | } 16 | 17 | public static string? GetDaxTableName(this string? name) 18 | { 19 | return name?.Replace("'", "''"); 20 | } 21 | 22 | public static string? GetDaxColumnName(this string? name) 23 | { 24 | return name?.Replace("]", "]]"); 25 | } 26 | 27 | /// 28 | /// Replace all occurrences of CRLF with LF since this is the default EOL character in SSAS 29 | /// 30 | public static string? ToASEol(this string? value) 31 | { 32 | if (value?.Length > 0) 33 | { 34 | value = value.Replace("\r\n", "\n"); 35 | } 36 | 37 | return value; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Dax.Template.Tests/Dax.Template.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0;net8.0 5 | 12.0 6 | enable 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | all 16 | 17 | 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | all 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | Always 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/Dax.Template/Exceptions/TemplateException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Dax.Template.Exceptions 4 | { 5 | public class TemplateException : Exception 6 | { 7 | public TemplateException() 8 | : base() 9 | { 10 | } 11 | 12 | public TemplateException(string message) 13 | : base(message) 14 | { 15 | } 16 | 17 | public TemplateException(string message, Exception innerException) 18 | : base(message, innerException) 19 | { 20 | } 21 | } 22 | 23 | public class TemplateConfigurationException : TemplateException 24 | { 25 | public TemplateConfigurationException(string message) 26 | : base(message) 27 | { 28 | } 29 | 30 | public TemplateConfigurationException(string message, Exception innerException) 31 | : base(message, innerException) 32 | { 33 | } 34 | } 35 | 36 | public class TemplateUnexpectedException : Exception 37 | { 38 | public TemplateUnexpectedException(string message) 39 | : base(message) 40 | { 41 | } 42 | 43 | public TemplateUnexpectedException(string message, Exception innerException) 44 | : base(message, innerException) 45 | { 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Dax.Template/Tables/ReferenceCalculatedTable.cs: -------------------------------------------------------------------------------- 1 | using Dax.Template.Model; 2 | using System.Threading; 3 | 4 | namespace Dax.Template.Tables 5 | { 6 | public abstract class ReferenceCalculatedTable : CalculatedTableTemplateBase 7 | { 8 | public string? HiddenTable { get; init; } 9 | 10 | public override string? GetDaxTableExpression(Microsoft.AnalysisServices.Tabular.Model? model, CancellationToken cancellationToken = default) 11 | { 12 | return QuotedHiddenTable ?? base.GetDaxTableExpression(model, cancellationToken); 13 | } 14 | 15 | private string? QuotedHiddenTable { get 16 | { 17 | // TODO: there could be a bug in TOM or SSAS because when we use a quoted identifier 18 | // in Source Column, the column is considered "invalid" if we try to deploy a change 19 | // to hierarchies or relationships that reference the column that uses this identifier 20 | // Therefore, we keep the "unquoted" table name in the Source Column Name property 21 | // so it works with table names that don't require quotes in the name. 22 | // 23 | //if (HiddenTable == null) 24 | //{ 25 | // return null; 26 | //} 27 | //else 28 | //{ 29 | // return $"'{HiddenTable}'"; 30 | //} 31 | return HiddenTable; 32 | } 33 | } 34 | 35 | protected override string GetSourceColumnName(Column column) 36 | { 37 | return $"{QuotedHiddenTable ?? string.Empty}[{column.Name}]"; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Dax.Template.TestUI/Templates/Config-03 - Monthly Gregorian.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "Description": "Monthly calendar and monthly time intelligence", 3 | "Templates": [ 4 | { 5 | "Class": "CustomDateTable", 6 | "Table": "Date", 7 | "ReferenceTable": "DateAutoTemplate", 8 | "Template": "DateTemplate-03.json", 9 | "LocalizationFiles": [ 10 | ] 11 | }, 12 | { 13 | "Class": "MeasuresTemplate", 14 | "Table": null, 15 | "Template": "TimeIntelligence-03.json", 16 | "Properties": { 17 | "__DisplayFolderRule": "Time intelligence\\@_MEASURE_@\\@_TEMPLATEFOLDER_@", 18 | "_DisplayFolderRule": "Time intelligence\\@_TEMPLATEFOLDER_@\\@_TEMPLATE_@", 19 | "___DisplayFolderRule": "Time intelligence\\@_TEMPLATEFOLDER_@\\@_MEASURE_@", 20 | "DisplayFolderRule": "Time intelligence\\@_TEMPLATEFOLDER_@\\@_MEASUREFOLDER_@\\@_MEASURE_@", 21 | "DisplayFolderRuleSingleInstanceMeasures": "Hidden Time Intelligence" 22 | }, 23 | "_comment": "TargetMeasures can override the default setting" 24 | } 25 | ], 26 | "IsoTranslation": "en-US", 27 | "IsoFormat": "en-US", 28 | "LocalizationFiles": [ 29 | "DateLocalization-03.json" 30 | ], 31 | "OnlyTablesColumns": [ 32 | "Sales", 33 | "Orders" 34 | ], 35 | "ExceptTablesColumns": [], 36 | "FirstYearMin": null, 37 | "FirstYearMax": null, 38 | "LastYearMin": null, 39 | "LastYearMax": null, 40 | "AutoScan": "Full", 41 | "DefaultVariables": { 42 | "__MonthsInYear": "12" 43 | }, 44 | "IsoCountry": "US", 45 | 46 | "AutoNaming": "Prefix", 47 | "TargetMeasures": [ 48 | { 49 | "Name": "Sales Amount" 50 | }, 51 | { 52 | "Name": "Total Cost" 53 | } 54 | ] 55 | } -------------------------------------------------------------------------------- /src/Dax.Template.TestUI/Templates/Config-04 - Monthly Fiscal.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "Description": "Monthly fiscal calendar and monthly fiscal time intelligence", 3 | "Templates": [ 4 | { 5 | "Class": "CustomDateTable", 6 | "Table": "Date", 7 | "ReferenceTable": "DateAutoTemplate", 8 | "Template": "DateTemplate-04.json", 9 | "LocalizationFiles": [ 10 | ] 11 | }, 12 | { 13 | "Class": "MeasuresTemplate", 14 | "Table": null, 15 | "Template": "TimeIntelligence-04.json", 16 | "Properties": { 17 | "__DisplayFolderRule": "Time intelligence\\@_MEASURE_@\\@_TEMPLATEFOLDER_@", 18 | "_DisplayFolderRule": "Time intelligence\\@_TEMPLATEFOLDER_@\\@_TEMPLATE_@", 19 | "___DisplayFolderRule": "Time intelligence\\@_TEMPLATEFOLDER_@\\@_MEASURE_@", 20 | "DisplayFolderRule": "Time intelligence\\@_TEMPLATEFOLDER_@\\@_MEASUREFOLDER_@\\@_MEASURE_@", 21 | "DisplayFolderRuleSingleInstanceMeasures": "Hidden Time Intelligence" 22 | }, 23 | "_comment": "TargetMeasures can override the default setting" 24 | } 25 | ], 26 | "IsoTranslation": "en-US", 27 | "IsoFormat": "en-US", 28 | "LocalizationFiles": [ 29 | "DateLocalization-04.json" 30 | ], 31 | "OnlyTablesColumns": [ 32 | "Sales", 33 | "Orders" 34 | ], 35 | "ExceptTablesColumns": [], 36 | "FirstYearMin": null, 37 | "FirstYearMax": null, 38 | "LastYearMin": null, 39 | "LastYearMax": null, 40 | "AutoScan": "Full", 41 | "DefaultVariables": { 42 | "__FirstFiscalMonth": "4", 43 | "__MonthsInYear": "12" 44 | }, 45 | "IsoCountry": "US", 46 | 47 | "AutoNaming": "Prefix", 48 | "TargetMeasures": [ 49 | { 50 | "Name": "Sales Amount" 51 | }, 52 | { 53 | "Name": "Total Cost" 54 | } 55 | ] 56 | } -------------------------------------------------------------------------------- /src/Dax.Template/Model/Column.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Microsoft.AnalysisServices.Tabular; 3 | using TabularColumn = Microsoft.AnalysisServices.Tabular.Column; 4 | using AttributeType = Microsoft.AnalysisServices.AttributeType; 5 | 6 | namespace Dax.Template.Model 7 | { 8 | public class Column : EntityBase, Syntax.IDependencies, Syntax.IDaxName, Syntax.IDaxComment 9 | { 10 | bool Syntax.IDependencies.AddLevel { get; init; } = true; 11 | public bool IgnoreAutoDependency { get; init; } = false; 12 | public string? Expression { get; set; } 13 | public DataType DataType { get; init; } 14 | public string? DataCategory { get; set; } 15 | public string? FormatString { get; set; } 16 | public string? DisplayFolder { get; set; } 17 | public bool IsHidden { get; set; } = false; 18 | public bool IsTemporary { get; set; } = false; 19 | public bool IsKey { get; set; } = false; 20 | public Syntax.IDependencies[]? Dependencies { get; set; } 21 | string Syntax.IDaxName.DaxName { get { return $"[{Name}]"; } } 22 | public string[]? Comments { get; set; } 23 | public Column? SortByColumn { get; set; } 24 | internal TabularColumn? TabularColumn { get; set; } 25 | public Dictionary Annotations { get; set; } = new(); 26 | /// 27 | /// This attribute is applied as a SQLBI_AttributeTypes annotation 28 | /// 29 | public AttributeType[]? AttributeType { get; set; } 30 | 31 | public override void Reset() 32 | { 33 | SortByColumn = null; 34 | TabularColumn = null; 35 | } 36 | public string GetDebugInfo() { return $"Column {Name} : {Expression}"; } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Dax.Template.Tests/PackageTests.cs: -------------------------------------------------------------------------------- 1 | namespace Dax.Template.Tests 2 | { 3 | using System; 4 | using System.IO; 5 | using Xunit; 6 | 7 | public class PackageTests 8 | { 9 | private const string StandardTemplatePath = @".\_data\Templates\Config-01 - Standard.template.json"; 10 | private const string TemplatePath = @".\_data\Templates"; 11 | 12 | [Fact] 13 | public void FindTemplateFiles_NotEmptyTest() 14 | { 15 | var templates = Package.FindTemplateFiles(TemplatePath); 16 | 17 | Assert.NotEmpty(templates); 18 | } 19 | 20 | [Fact] 21 | public void FindTemplateFiles_FileExtensionTest() 22 | { 23 | var templates = Package.FindTemplateFiles(TemplatePath); 24 | 25 | foreach (var template in templates) 26 | { 27 | Assert.EndsWith(Package.TEMPLATE_FILE_EXTENSION, template); 28 | } 29 | } 30 | 31 | [Fact] 32 | public void LoadFromFile_ConfigurationNotNullTest() 33 | { 34 | var package = Package.LoadFromFile(StandardTemplatePath); 35 | 36 | Assert.NotNull(package.Configuration); 37 | } 38 | 39 | [Fact] 40 | public void LoadFromFile_ConfigurationNameTest() 41 | { 42 | var package = Package.LoadFromFile(StandardTemplatePath); 43 | 44 | var expected = Path.GetFileName(StandardTemplatePath.Remove(StandardTemplatePath.Length - Package.TEMPLATE_FILE_EXTENSION.Length)); 45 | var actual = package.Configuration.Name; 46 | 47 | Assert.Equal(expected, actual); 48 | } 49 | 50 | [Fact] 51 | public void LoadFromFile_TemplateUriPathTest() 52 | { 53 | var package = Package.LoadFromFile(StandardTemplatePath); 54 | 55 | Assert.NotNull(package.Configuration.TemplateUri); 56 | 57 | var expected = Path.GetFullPath(StandardTemplatePath); 58 | var actual = (new Uri(package.Configuration.TemplateUri!)).LocalPath; 59 | 60 | Assert.Equal(expected, actual); 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/Dax.Template.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.31911.260 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dax.Template", "Dax.Template\Dax.Template.csproj", "{41718D2F-3E76-4CC9-80BD-A15442CFF5D8}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dax.Template.TestUI", "Dax.Template.TestUI\Dax.Template.TestUI.csproj", "{59CB9130-6643-4C25-8A4D-E808F1271AAE}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dax.Template.Tests", "Dax.Template.Tests\Dax.Template.Tests.csproj", "{7429EC2C-81D9-42E2-8D69-D5CD729BD503}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {41718D2F-3E76-4CC9-80BD-A15442CFF5D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {41718D2F-3E76-4CC9-80BD-A15442CFF5D8}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {41718D2F-3E76-4CC9-80BD-A15442CFF5D8}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {41718D2F-3E76-4CC9-80BD-A15442CFF5D8}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {59CB9130-6643-4C25-8A4D-E808F1271AAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {59CB9130-6643-4C25-8A4D-E808F1271AAE}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {59CB9130-6643-4C25-8A4D-E808F1271AAE}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {59CB9130-6643-4C25-8A4D-E808F1271AAE}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {7429EC2C-81D9-42E2-8D69-D5CD729BD503}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {7429EC2C-81D9-42E2-8D69-D5CD729BD503}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {7429EC2C-81D9-42E2-8D69-D5CD729BD503}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {7429EC2C-81D9-42E2-8D69-D5CD729BD503}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {19828D02-B5CB-4269-89B0-543FBDEFD6AF} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /src/Dax.Template/Extensions/ReflectionHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Dax.Template.Extensions 9 | { 10 | public static class ReflectionHelper 11 | { 12 | private static PropertyInfo? GetPropertyInfo(Type? type, string propertyName) 13 | { 14 | PropertyInfo? propInfo; 15 | do 16 | { 17 | propInfo = type?.GetProperty( 18 | propertyName, 19 | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); 20 | type = type?.BaseType; 21 | } 22 | while (propInfo == null && type != null); 23 | return propInfo; 24 | } 25 | 26 | public static object? GetPropertyValue(this object obj, string propertyName, bool errorIfNotFound = true) 27 | { 28 | if (obj == null) 29 | { 30 | throw new ArgumentNullException(nameof(obj)); 31 | } 32 | Type objType = obj.GetType(); 33 | PropertyInfo? propInfo = GetPropertyInfo(objType, propertyName); 34 | if (propInfo == null && errorIfNotFound) 35 | { 36 | throw new ArgumentOutOfRangeException( 37 | nameof(propertyName), 38 | string.Format("Couldn't find property {0} in type {1}", propertyName, objType.FullName)); 39 | } 40 | return propInfo?.GetValue(obj, null); 41 | } 42 | 43 | public static void SetPropertyValue(this object obj, string propertyName, object val) 44 | { 45 | if (obj == null) 46 | { 47 | throw new ArgumentNullException(nameof(obj)); 48 | } 49 | Type objType = obj.GetType(); 50 | PropertyInfo? propInfo = GetPropertyInfo(objType, propertyName); 51 | if (propInfo == null) 52 | { 53 | throw new ArgumentOutOfRangeException( 54 | nameof(propertyName), 55 | string.Format("Couldn't find property {0} in type {1}", propertyName, objType.FullName)); 56 | } 57 | propInfo.SetValue(obj, val, null); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Dax.Template.TestUI/Templates/Config-05 - Custom Gregorian.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "Description": "Custom calendar based on months with custom time intelligence", 3 | "Templates": [ 4 | { 5 | "Class": "HolidaysDefinitionTable", 6 | "Table": "HolidaysDefinition", 7 | "Template": "HolidaysDefinition.json", 8 | "IsHidden": true 9 | }, 10 | { 11 | "Class": "HolidaysTable", 12 | "Table": "Holidays", 13 | "Template": null, 14 | "IsHidden": true 15 | }, 16 | { 17 | "Class": "CustomDateTable", 18 | "Table": "Date", 19 | "ReferenceTable": "DateAutoTemplate", 20 | "Template": "DateTemplate-05.json", 21 | "LocalizationFiles": [ 22 | ] 23 | }, 24 | { 25 | "Class": "MeasuresTemplate", 26 | "Table": null, 27 | "Template": "TimeIntelligence-05.json", 28 | "Properties": { 29 | "__DisplayFolderRule": "Time intelligence\\@_MEASURE_@\\@_TEMPLATEFOLDER_@", 30 | "_DisplayFolderRule": "Time intelligence\\@_TEMPLATEFOLDER_@\\@_TEMPLATE_@", 31 | "___DisplayFolderRule": "Time intelligence\\@_TEMPLATEFOLDER_@\\@_MEASURE_@", 32 | "DisplayFolderRule": "Time intelligence\\@_TEMPLATEFOLDER_@\\@_MEASUREFOLDER_@\\@_MEASURE_@", 33 | "DisplayFolderRuleSingleInstanceMeasures": "Hidden Time Intelligence" 34 | }, 35 | "_comment": "TargetMeasures can override the default setting" 36 | } 37 | ], 38 | "IsoTranslation": "en-US", 39 | "IsoFormat": "en-US", 40 | "LocalizationFiles": [ 41 | "DateLocalization-05.json" 42 | ], 43 | "OnlyTablesColumns": [ 44 | "Sales", 45 | "Orders" 46 | ], 47 | "ExceptTablesColumns": [], 48 | "FirstYearMin": null, 49 | "FirstYearMax": null, 50 | "LastYearMin": null, 51 | "LastYearMax": null, 52 | "AutoScan": "Full", 53 | "DefaultVariables": { 54 | "__FirstDayOfWeek": "0" 55 | }, 56 | "IsoCountry": "US", 57 | "InLieuOfPrefix": "(in lieu of ", 58 | "InLieuOfSuffix": ")", 59 | "WorkingDays": "{ 2, 3, 4, 5, 6 }", 60 | "HolidaysDefinitionTable": "HolidaysDefinition", 61 | 62 | "HolidaysReference": { 63 | "TableName": "Holidays", 64 | "DateColumnName": "Holiday Date", 65 | "HolidayColumnName": "Holiday Name" 66 | }, 67 | "AutoNaming": "Prefix", 68 | "TargetMeasures": [ 69 | { 70 | "Name": "Sales Amount" 71 | }, 72 | { 73 | "Name": "Total Cost" 74 | } 75 | ] 76 | } -------------------------------------------------------------------------------- /src/Dax.Template/Extensions/ComputeDependencies.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Collections.Generic; 3 | using Dax.Template.Syntax; 4 | using System.Text.RegularExpressions; 5 | using Dax.Template.Exceptions; 6 | 7 | namespace Dax.Template.Extensions 8 | { 9 | public static partial class Extensions 10 | { 11 | public static void AddDependenciesFromExpression(this IEnumerable daxElements) 12 | { 13 | daxElements.AddDependenciesFromExpression(daxElements); 14 | } 15 | 16 | public static void AddDependenciesFromExpression(this IEnumerable> items, IEnumerable daxElements) 17 | { 18 | items 19 | .Where(item => !item.IgnoreAutoDependency && !string.IsNullOrEmpty(item.Expression)) 20 | .ToList() 21 | .ForEach(item => item.InternalAdd(FindDaxReferences, daxElements)); 22 | } 23 | 24 | //private readonly static Regex FindVariables = new(@"__(\w*)", RegexOptions.Compiled); 25 | //private readonly static Regex FindColumns = new(@"(?<=[^']|^)\[(.*?)\]", RegexOptions.Compiled); 26 | private readonly static Regex FindDaxReferences = new(@"__(\w*)|(?<=[^']|^)\[(.*?)\]", RegexOptions.Compiled); 27 | private static void InternalAdd(this IDependencies item, Regex findTokenRegex, IEnumerable daxElements) 28 | { 29 | if (item.Expression == null) return; 30 | 31 | var findTokens = findTokenRegex.Matches(item.Expression); 32 | 33 | var tokens = 34 | from token in findTokens 35 | select token.Value; 36 | 37 | var invalidReferences = 38 | from token in tokens 39 | where !daxElements.Any(v => v.DaxName == token) 40 | select token; 41 | 42 | if (invalidReferences.Any()) 43 | { 44 | throw new InvalidVariableReferenceException(invalidReferences.First(), item.Expression); 45 | } 46 | 47 | var dependenciesToken = 48 | from var in daxElements 49 | where tokens.Contains(var.DaxName) 50 | && !(item.Dependencies?.Contains(var) == true) 51 | select var; 52 | 53 | item.Dependencies = item.Dependencies?.Union(dependenciesToken).ToArray() ?? dependenciesToken.ToArray(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Dax.Template.TestUI/Templates/Config-06 - Custom Fiscal.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "Description": "Custom fiscal calendar based on months with custom time intelligence", 3 | "Templates": [ 4 | { 5 | "Class": "HolidaysDefinitionTable", 6 | "Table": "HolidaysDefinition", 7 | "Template": "HolidaysDefinition.json", 8 | "IsHidden": true 9 | }, 10 | { 11 | "Class": "HolidaysTable", 12 | "Table": "Holidays", 13 | "Template": null, 14 | "IsHidden": true 15 | }, 16 | { 17 | "Class": "CustomDateTable", 18 | "Table": "Date", 19 | "ReferenceTable": "DateAutoTemplate", 20 | "Template": "DateTemplate-06.json", 21 | "LocalizationFiles": [ 22 | ] 23 | }, 24 | { 25 | "Class": "MeasuresTemplate", 26 | "Table": null, 27 | "Template": "TimeIntelligence-06.json", 28 | "Properties": { 29 | "__DisplayFolderRule": "Time intelligence\\@_MEASURE_@\\@_TEMPLATEFOLDER_@", 30 | "_DisplayFolderRule": "Time intelligence\\@_TEMPLATEFOLDER_@\\@_TEMPLATE_@", 31 | "___DisplayFolderRule": "Time intelligence\\@_TEMPLATEFOLDER_@\\@_MEASURE_@", 32 | "DisplayFolderRule": "Time intelligence\\@_TEMPLATEFOLDER_@\\@_MEASUREFOLDER_@\\@_MEASURE_@", 33 | "DisplayFolderRuleSingleInstanceMeasures": "Hidden Time Intelligence" 34 | }, 35 | "_comment": "TargetMeasures can override the default setting" 36 | } 37 | ], 38 | "IsoTranslation": "en-US", 39 | "IsoFormat": "en-US", 40 | "LocalizationFiles": [ 41 | "DateLocalization-06.json" 42 | ], 43 | "OnlyTablesColumns": [ 44 | "Sales", 45 | "Orders" 46 | ], 47 | "ExceptTablesColumns": [], 48 | "FirstYearMin": null, 49 | "FirstYearMax": null, 50 | "LastYearMin": null, 51 | "LastYearMax": null, 52 | "AutoScan": "Full", 53 | "DefaultVariables": { 54 | "__FirstFiscalMonth": "4", 55 | "__FirstDayOfWeek": "0" 56 | }, 57 | "IsoCountry": "US", 58 | "InLieuOfPrefix": "(in lieu of ", 59 | "InLieuOfSuffix": ")", 60 | "WorkingDays": "{ 2, 3, 4, 5, 6 }", 61 | "HolidaysDefinitionTable": "HolidaysDefinition", 62 | 63 | "HolidaysReference": { 64 | "TableName": "Holidays", 65 | "DateColumnName": "Holiday Date", 66 | "HolidayColumnName": "Holiday Name" 67 | }, 68 | "AutoNaming": "Prefix", 69 | "TargetMeasures": [ 70 | { 71 | "Name": "Sales Amount" 72 | }, 73 | { 74 | "Name": "Total Cost" 75 | } 76 | ] 77 | } -------------------------------------------------------------------------------- /src/Dax.Template/Dax.Template.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0;net8.0 5 | 12.0 6 | enable 7 | en-US 8 | true 9 | 10 | 11 | 1.0.0.0 12 | 1.0.0 13 | 1.0.0 14 | dev 15 | 16 | Dax.Template 17 | SQLBI 18 | 19 | Marco Russo 20 | Tabular and DAX template engine 21 | Engine that creates DAX columns, measures, tables and calculation groups based on JSON templates 22 | Engine that creates DAX columns, measures, tables and calculation groups based on JSON templates 23 | Dax.Template 24 | DAX;TEMPLATE;SQLBI;TABULAR;SSAS 25 | package-icon.png 26 | README.md 27 | MIT 28 | https://github.com/sql-bi/DaxTemplate 29 | https://github.com/sql-bi/DaxTemplate 30 | git 31 | main 32 | true 33 | true 34 | embedded 35 | 36 | 37 | 38 | true 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | $(AdditionalConstants) 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/Dax.Template.TestUI/Templates/Config-95 - Standard dates no text.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "Description": "Standard calendar with holidays and standard time intelligence", 3 | "Templates": [ 4 | { 5 | "Class": "HolidaysDefinitionTable", 6 | "Table": "HolidaysDefinition", 7 | "Template": "HolidaysDefinition.json", 8 | "IsHidden": true 9 | }, 10 | { 11 | "Class": "HolidaysTable", 12 | "Table": "Holidays", 13 | "Template": null 14 | }, 15 | { 16 | "Class": "CustomDateTable", 17 | "Table": "Date", 18 | "ReferenceTable": "DateAutoTemplate", 19 | "Template": "DateTemplate-95.json", 20 | "LocalizationFiles": [ 21 | ] 22 | }, 23 | { 24 | "Class": "MeasuresTemplate", 25 | "Table": null, 26 | "Template": "TimeIntelligence-01.json", 27 | "Properties": { 28 | "__DisplayFolderRule": "Time intelligence\\@_MEASURE_@\\@_TEMPLATEFOLDER_@", 29 | "_DisplayFolderRule": "Time intelligence\\@_TEMPLATEFOLDER_@\\@_TEMPLATE_@", 30 | "___DisplayFolderRule": "Time intelligence\\@_TEMPLATEFOLDER_@\\@_MEASURE_@", 31 | "DisplayFolderRule": "Time intelligence\\@_TEMPLATEFOLDER_@\\@_MEASUREFOLDER_@\\@_MEASURE_@", 32 | "DisplayFolderRuleSingleInstanceMeasures": "Hidden Time Intelligence" 33 | }, 34 | "_comment": "TargetMeasures can override the default setting" 35 | } 36 | ], 37 | "IsoTranslation": "en-US", 38 | "IsoFormat": "en-US", 39 | "LocalizationFiles": [ 40 | ], 41 | "OnlyTablesColumns": [ 42 | "Sales", 43 | "Orders" 44 | ], 45 | "ExceptTablesColumns": [], 46 | "FirstYearMin": null, 47 | "FirstYearMax": null, 48 | "LastYearMin": null, 49 | "LastYearMax": null, 50 | "AutoScan": "Full", 51 | "DefaultVariables": { 52 | "__FirstFiscalMonth": "4", 53 | "__FirstDayOfWeek": "0" 54 | }, 55 | "IsoCountry": "US", 56 | "InLieuOfPrefix": "(in lieu of ", 57 | "InLieuOfSuffix": ")", 58 | "WorkingDays": "{ 2, 3, 4, 5, 6 }", 59 | "HolidaysDefinitionTable": "HolidaysDefinition", 60 | 61 | "HolidaysReference": { 62 | "TableName": "Holidays", 63 | "DateColumnName": "Holiday Date", 64 | "HolidayColumnName": "Holiday Name" 65 | }, 66 | "TableSingleInstanceMeasures": "Sales", 67 | "AutoNaming": "Prefix", 68 | "TargetMeasures": [ 69 | { 70 | "Name": "Sales Amount" 71 | }, 72 | { 73 | "Name": "Total Cost" 74 | }, 75 | { 76 | "Name": "Margin" 77 | }, 78 | { 79 | "Name": "Margin %" 80 | } 81 | ] 82 | } -------------------------------------------------------------------------------- /src/Dax.Template.TestUI/Templates/Config-01 - Standard Gregorian.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "Description": "Standard calendar with holidays and standard time intelligence", 3 | "Templates": [ 4 | { 5 | "Class": "HolidaysDefinitionTable", 6 | "Table": "HolidaysDefinition", 7 | "Template": "HolidaysDefinition.json", 8 | "IsHidden": true 9 | }, 10 | { 11 | "Class": "HolidaysTable", 12 | "Table": "Holidays", 13 | "Template": null, 14 | "IsHidden": true 15 | }, 16 | { 17 | "Class": "CustomDateTable", 18 | "Table": "Date", 19 | "ReferenceTable": "DateAutoTemplate", 20 | "Template": "DateTemplate-01.json", 21 | "LocalizationFiles": [ 22 | ] 23 | }, 24 | { 25 | "Class": "MeasuresTemplate", 26 | "Table": null, 27 | "Template": "TimeIntelligence-01.json", 28 | "Properties": { 29 | "__DisplayFolderRule": "Time intelligence\\@_MEASURE_@\\@_TEMPLATEFOLDER_@", 30 | "_DisplayFolderRule": "Time intelligence\\@_TEMPLATEFOLDER_@\\@_TEMPLATE_@", 31 | "___DisplayFolderRule": "Time intelligence\\@_TEMPLATEFOLDER_@\\@_MEASURE_@", 32 | "DisplayFolderRule": "Time intelligence\\@_TEMPLATEFOLDER_@\\@_MEASUREFOLDER_@\\@_MEASURE_@", 33 | "DisplayFolderRuleSingleInstanceMeasures": "Hidden Time Intelligence" 34 | }, 35 | "_comment": "TargetMeasures can override the default setting" 36 | } 37 | ], 38 | "IsoTranslation": "en-US", 39 | "IsoFormat": "en-US", 40 | "LocalizationFiles": [ 41 | "DateLocalization-01.json" 42 | ], 43 | "OnlyTablesColumns": [ 44 | "Sales", 45 | "Orders" 46 | ], 47 | "ExceptTablesColumns": [], 48 | "FirstYearMin": null, 49 | "FirstYearMax": null, 50 | "LastYearMin": null, 51 | "LastYearMax": null, 52 | "AutoScan": "Full", 53 | "DefaultVariables": { 54 | "__FirstDayOfWeek": "0" 55 | }, 56 | "IsoCountry": "US", 57 | "InLieuOfPrefix": "(in lieu of ", 58 | "InLieuOfSuffix": ")", 59 | "WorkingDays": "{ 2, 3, 4, 5, 6 }", 60 | "HolidaysDefinitionTable": "HolidaysDefinition", 61 | 62 | "HolidaysReference": { 63 | "TableName": "Holidays", 64 | "DateColumnName": "Holiday Date", 65 | "HolidayColumnName": "Holiday Name" 66 | }, 67 | "TableSingleInstanceMeasures": "Sales", 68 | "AutoNaming": "Prefix", 69 | "TargetMeasures": [ 70 | { 71 | "Name": "Sales Amount" 72 | }, 73 | { 74 | "Name": "Total Cost" 75 | }, 76 | { 77 | "Name": "Margin" 78 | }, 79 | { 80 | "Name": "Margin %" 81 | } 82 | ] 83 | } -------------------------------------------------------------------------------- /src/Dax.Template.Tests/_data/Templates/Config-01 - Standard.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "Description": "Standard calendar with holidays and standard time intelligence", 3 | "Templates": [ 4 | { 5 | "Class": "HolidaysDefinitionTable", 6 | "Table": "HolidaysDefinition", 7 | "Template": "HolidaysDefinition.json", 8 | "IsHidden": true 9 | }, 10 | { 11 | "Class": "HolidaysTable", 12 | "Table": "Holidays", 13 | "Template": null, 14 | "IsHidden": true 15 | }, 16 | { 17 | "Class": "CustomDateTable", 18 | "Table": "Date", 19 | "ReferenceTable": "DateAutoTemplate", 20 | "Template": "DateTemplate-01.json", 21 | "LocalizationFiles": [ 22 | ] 23 | }, 24 | { 25 | "Class": "MeasuresTemplate", 26 | "Table": null, 27 | "Template": "TimeIntelligence-01.json", 28 | "Properties": { 29 | "__DisplayFolderRule": "Time intelligence\\@_MEASURE_@\\@_TEMPLATEFOLDER_@", 30 | "_DisplayFolderRule": "Time intelligence\\@_TEMPLATEFOLDER_@\\@_TEMPLATE_@", 31 | "___DisplayFolderRule": "Time intelligence\\@_TEMPLATEFOLDER_@\\@_MEASURE_@", 32 | "DisplayFolderRule": "Time intelligence\\@_TEMPLATEFOLDER_@\\@_MEASUREFOLDER_@\\@_MEASURE_@", 33 | "DisplayFolderRuleSingleInstanceMeasures": "Hidden Time Intelligence" 34 | }, 35 | "_comment": "TargetMeasures can override the default setting" 36 | } 37 | ], 38 | "IsoTranslation": "en-US", 39 | "IsoFormat": "en-US", 40 | "LocalizationFiles": [ 41 | "DateLocalization-01.json" 42 | ], 43 | "OnlyTablesColumns": [ 44 | "Sales", 45 | "Orders" 46 | ], 47 | "ExceptTablesColumns": [], 48 | "FirstYearMin": null, 49 | "FirstYearMax": null, 50 | "LastYearMin": null, 51 | "LastYearMax": null, 52 | "AutoScan": "Full", 53 | "DefaultVariables": { 54 | "__FirstFiscalMonth": "4", 55 | "__FirstDayOfWeek": "0" 56 | }, 57 | "IsoCountry": "US", 58 | "InLieuOfPrefix": "(in lieu of ", 59 | "InLieuOfSuffix": ")", 60 | "WorkingDays": "{ 2, 3, 4, 5, 6 }", 61 | "HolidaysDefinitionTable": "HolidaysDefinition", 62 | 63 | "HolidaysReference": { 64 | "TableName": "Holidays", 65 | "DateColumnName": "Holiday Date", 66 | "HolidayColumnName": "Holiday Name" 67 | }, 68 | "TableSingleInstanceMeasures": "Sales", 69 | "AutoNaming": "Prefix", 70 | "TargetMeasures": [ 71 | { 72 | "Name": "Sales Amount" 73 | }, 74 | { 75 | "Name": "Total Cost" 76 | }, 77 | { 78 | "Name": "Margin" 79 | }, 80 | { 81 | "Name": "Margin %" 82 | } 83 | ] 84 | } -------------------------------------------------------------------------------- /src/Dax.Template.TestUI/Templates/Config-02 - Standard Fiscal.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "Description": "Standard fiscal calendar with holidays and standard fiscal time intelligence", 3 | "Templates": [ 4 | { 5 | "Class": "HolidaysDefinitionTable", 6 | "Table": "HolidaysDefinition", 7 | "Template": "HolidaysDefinition.json", 8 | "IsHidden": true 9 | }, 10 | { 11 | "Class": "HolidaysTable", 12 | "Table": "Holidays", 13 | "Template": null, 14 | "IsHidden": true 15 | }, 16 | { 17 | "Class": "CustomDateTable", 18 | "Table": "Date", 19 | "ReferenceTable": "DateAutoTemplate", 20 | "Template": "DateTemplate-02.json", 21 | "LocalizationFiles": [ 22 | ] 23 | }, 24 | { 25 | "Class": "MeasuresTemplate", 26 | "Table": null, 27 | "Template": "TimeIntelligence-02.json", 28 | "Properties": { 29 | "__DisplayFolderRule": "Time intelligence\\@_MEASURE_@\\@_TEMPLATEFOLDER_@", 30 | "_DisplayFolderRule": "Time intelligence\\@_TEMPLATEFOLDER_@\\@_TEMPLATE_@", 31 | "___DisplayFolderRule": "Time intelligence\\@_TEMPLATEFOLDER_@\\@_MEASURE_@", 32 | "DisplayFolderRule": "Time intelligence\\@_TEMPLATEFOLDER_@\\@_MEASUREFOLDER_@\\@_MEASURE_@", 33 | "DisplayFolderRuleSingleInstanceMeasures": "Hidden Time Intelligence" 34 | }, 35 | "_comment": "TargetMeasures can override the default setting" 36 | } 37 | ], 38 | "IsoTranslation": "en-US", 39 | "IsoFormat": "en-US", 40 | "LocalizationFiles": [ 41 | "DateLocalization-02.json" 42 | ], 43 | "OnlyTablesColumns": [ 44 | "Sales", 45 | "Orders" 46 | ], 47 | "ExceptTablesColumns": [], 48 | "FirstYearMin": null, 49 | "FirstYearMax": null, 50 | "LastYearMin": null, 51 | "LastYearMax": null, 52 | "AutoScan": "Full", 53 | "DefaultVariables": { 54 | "__FirstFiscalMonth": "7", 55 | "__FirstDayOfWeek": "0" 56 | }, 57 | "IsoCountry": "US", 58 | "InLieuOfPrefix": "(in lieu of ", 59 | "InLieuOfSuffix": ")", 60 | "WorkingDays": "{ 2, 3, 4, 5, 6 }", 61 | "HolidaysDefinitionTable": "HolidaysDefinition", 62 | 63 | "HolidaysReference": { 64 | "TableName": "Holidays", 65 | "DateColumnName": "Holiday Date", 66 | "HolidayColumnName": "Holiday Name" 67 | }, 68 | "TableSingleInstanceMeasures": "Sales", 69 | "AutoNaming": "Prefix", 70 | "TargetMeasures": [ 71 | { 72 | "Name": "Sales Amount" 73 | }, 74 | { 75 | "Name": "Total Cost" 76 | }, 77 | { 78 | "Name": "Margin" 79 | }, 80 | { 81 | "Name": "Margin %" 82 | } 83 | ] 84 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /src/Dax.Template.TestUI/Templates/Config-07 - Weekly.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "Description": "Weekly calendars with weekly-based time intelligence", 3 | "Templates": [ 4 | { 5 | "Class": "HolidaysDefinitionTable", 6 | "Table": "HolidaysDefinition", 7 | "Template": "HolidaysDefinition.json", 8 | "IsHidden": true 9 | }, 10 | { 11 | "Class": "HolidaysTable", 12 | "Table": "Holidays", 13 | "Template": null, 14 | "IsHidden": true 15 | }, 16 | { 17 | "Class": "CustomDateTable", 18 | "Table": "Date", 19 | "ReferenceTable": "DateAutoTemplate", 20 | "Template": "DateTemplate-07.json", 21 | "LocalizationFiles": [ 22 | "DateLocalization-07.json" 23 | ] 24 | }, 25 | { 26 | "Class": "MeasuresTemplate", 27 | "Table": null, 28 | "Template": "TimeIntelligence-07.json", 29 | "Properties": { 30 | "__DisplayFolderRule": "Time intelligence\\@_MEASURE_@\\@_TEMPLATEFOLDER_@", 31 | "_DisplayFolderRule": "Time intelligence\\@_TEMPLATEFOLDER_@\\@_TEMPLATE_@", 32 | "___DisplayFolderRule": "Time intelligence\\@_TEMPLATEFOLDER_@\\@_MEASURE_@", 33 | "DisplayFolderRule": "Time intelligence\\@_TEMPLATEFOLDER_@\\@_MEASUREFOLDER_@\\@_MEASURE_@", 34 | "DisplayFolderRuleSingleInstanceMeasures": "Hidden Time Intelligence" 35 | }, 36 | "_comment": "TargetMeasures can override the default setting", 37 | "TargetMeasures": [ 38 | { 39 | "Name": "Sales Amount" 40 | }, 41 | { 42 | "Name": "Total Cost" 43 | } 44 | ] 45 | } 46 | ], 47 | "IsoTranslation": "en-US", 48 | "IsoFormat": "en-US", 49 | "LocalizationFiles": [ 50 | "DateLocalization-07.json" 51 | ], 52 | "OnlyTablesColumns": [ 53 | "Sales", 54 | "Orders" 55 | ], 56 | "ExceptTablesColumns": [], 57 | "FirstYearMin": null, 58 | "FirstYearMax": null, 59 | "LastYearMin": null, 60 | "LastYearMax": null, 61 | "AutoScan": "Full", 62 | "DefaultVariables": { 63 | "__FirstFiscalMonth": "4", 64 | "__FirstDayOfWeek": "0", 65 | "__TypeStartFiscalYear": "1", 66 | "__QuarterWeekType": "\"445\"", 67 | "__WeeklyType": "\"Last\"", 68 | "__WorkingDayType": "\"Working day\"", 69 | "__NonWorkingDayType": "\"Non-working day\"", 70 | "__OffsetYears": "1" 71 | }, 72 | 73 | "IsoCountry": "US", 74 | "InLieuOfPrefix": "(in lieu of ", 75 | "InLieuOfSuffix": ")", 76 | "WorkingDays": "{ 2, 3, 4, 5, 6 }", 77 | "HolidaysDefinitionTable": "HolidaysDefinition", 78 | 79 | "HolidaysReference": { 80 | "TableName": "Holidays", 81 | "DateColumnName": "Holiday Date", 82 | "HolidayColumnName": "Holiday Name" 83 | }, 84 | "TargetMeasures": [ 85 | { 86 | "Name": "Sales Amount" 87 | }, 88 | { 89 | "Name": "Total Cost" 90 | } 91 | ] 92 | } -------------------------------------------------------------------------------- /src/Dax.Template.TestUI/ApplyDaxTemplate.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | text/microsoft-resx 50 | 51 | 52 | 2.0 53 | 54 | 55 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 56 | 57 | 58 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 59 | 60 | 61 | 17, 17 62 | 63 | -------------------------------------------------------------------------------- /src/Dax.Template/Tables/Dates/CustomDateTable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Microsoft.AnalysisServices.Tabular; 4 | using TabularModel = Microsoft.AnalysisServices.Tabular.Model; 5 | using Column = Dax.Template.Model.Column; 6 | using Dax.Template.Exceptions; 7 | using Dax.Template.Interfaces; 8 | using Dax.Template.Constants; 9 | 10 | namespace Dax.Template.Tables.Dates 11 | { 12 | public class CustomDateTemplateDefinition : CustomTemplateDefinition 13 | { 14 | /// 15 | /// Define the calendar type for time intelligence calculations 16 | /// 17 | public string? CalendarType { get; set; } 18 | public string[]? CalendarTypes { get; set; } 19 | } 20 | public class CustomDateTable : BaseDateTemplate 21 | { 22 | // TODO: this could be localized (as other column names) 23 | const string DATE_COLUMN_NAME = "Date"; 24 | 25 | public CustomDateTable(IDateTemplateConfig config, CustomDateTemplateDefinition template, TabularModel? model, string? referenceTable = null) 26 | : base(config, template, model) 27 | { 28 | HiddenTable = referenceTable; 29 | Annotations.Add(Attributes.SQLBI_TEMPLATE_ATTRIBUTE, Attributes.SQLBI_TEMPLATE_DATES); 30 | Annotations.Add( 31 | Attributes.SQLBI_TEMPLATETABLE_ATTRIBUTE, 32 | (referenceTable == null) ? Attributes.SQLBI_TEMPLATETABLE_DATEAUTOTEMPLATE : Attributes.SQLBI_TEMPLATETABLE_DATE ); 33 | 34 | if (!string.IsNullOrWhiteSpace(template.CalendarType)) { 35 | CalendarType = new string[] { template.CalendarType }; 36 | } 37 | else 38 | { 39 | CalendarType = template.CalendarTypes; 40 | } 41 | } 42 | protected override void InitTemplate(IDateTemplateConfig config, CustomTemplateDefinition template, Predicate skipColumn, TabularModel? model) 43 | { 44 | bool hasHolidays = HolidaysConfig.HasHolidays(config.HolidaysReference); 45 | if (hasHolidays) 46 | { 47 | if (model?.Tables.FirstOrDefault(t => t.Name == config.HolidaysReference?.TableName) == null) 48 | { 49 | throw new TemplateException($"Holidays table '{config.HolidaysReference?.TableName}' not found."); 50 | } 51 | } 52 | base.InitTemplate( 53 | config, 54 | template, 55 | // Skip columns related to holidays if no holidays configuration available 56 | ((columnDefinition) => columnDefinition.RequiresHolidays && !hasHolidays), 57 | model); 58 | } 59 | protected override Column CreateColumn(string name, DataType dataType) 60 | { 61 | if (name == DATE_COLUMN_NAME) 62 | { 63 | return new Model.DateColumn() 64 | { 65 | Name = name, 66 | DataType = dataType 67 | }; 68 | } 69 | else return base.CreateColumn(name, dataType); 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /.azure/pipelines/ci.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: verbosity 3 | displayName: Verbosity 4 | type: string 5 | default: minimal 6 | values: 7 | - minimal 8 | - normal 9 | - detailed 10 | - diagnostic 11 | 12 | trigger: none 13 | 14 | pool: 15 | vmImage: 'windows-latest' 16 | 17 | variables: 18 | configuration: 'Release' 19 | assemblyVersion: '$(AppVersionMajor).0.0.0' 20 | semanticVersion: '$(AppVersionMajor).$(AppVersionMinor).$(AppVersionPatch)' 21 | 22 | steps: 23 | - task: PowerShell@2 24 | displayName: 'Set variables' 25 | inputs: 26 | targetType: 'inline' 27 | script: | 28 | $versionSuffix = "$(AppVersionSuffix)" 29 | if ($versionSuffix -match "\S") { $versionSuffix = "-$versionSuffix" } 30 | Write-Host "##vso[task.setvariable variable=versionSuffix;]$versionSuffix" 31 | Write-Host "VersionSuffix $versionSuffix" 32 | $packageVersion = "$(semanticVersion)$versionSuffix" 33 | if ("$(isReleaseBuild)" -ne "true") { $packageVersion += "-CI-$(Build.BuildNumber)" } 34 | Write-Host "Set PackageVersion and BuildNumber to '$packageVersion'" 35 | Write-Host "##vso[task.setvariable variable=packageVersion;]$packageVersion" 36 | Write-Host "##vso[build.updatebuildnumber]$packageVersion" 37 | - task: DownloadSecureFile@1 38 | name: signKey 39 | displayName: 'Download sign key' 40 | inputs: 41 | secureFile: 'DaxTemplate.snk' 42 | - task: UseDotNet@2 43 | displayName: 'Install .NET SDK' 44 | inputs: 45 | packageType: sdk 46 | useGlobalJson: true 47 | - task: UseDotNet@2 48 | displayName: 'Install .NET 6.0 runtime' 49 | inputs: 50 | packageType: runtime 51 | version: '6.0.x' 52 | - task: DotNetCoreCLI@2 53 | displayName: '.NET restore' 54 | inputs: 55 | command: 'restore' 56 | projects: 'src/**/*.csproj' 57 | feedsToUse: 'select' 58 | verbosityRestore: '${{ parameters.verbosity }}' 59 | - task: DotNetCoreCLI@2 60 | displayName: '.NET build' 61 | inputs: 62 | command: 'build' 63 | projects: 'src/**/*.csproj' 64 | arguments: '--configuration "$(configuration)" --no-restore --verbosity ${{ parameters.verbosity }} /p:AssemblyVersion="$(assemblyVersion)" /p:FileVersion="$(semanticVersion)" /p:VersionPrefix="$(semanticVersion)" /p:VersionSuffix="$(versionSuffix)" /p:ContinuousIntegrationBuild="true" /p:AdditionalConstants="SIGNED" /p:SignAssembly="true" /p:AssemblyOriginatorKeyFile="$(signKey.secureFilePath)" /m' 65 | - task: DotNetCoreCLI@2 66 | displayName: '.NET test' 67 | inputs: 68 | command: 'test' 69 | projects: 'src/*Tests/*.csproj' 70 | arguments: '--no-restore --no-build --verbosity ${{ parameters.verbosity }} --logger "trx;LogFilePrefix=testResults" --collect "Code coverage"' 71 | - task: DotNetCoreCLI@2 72 | displayName: '.NET pack' 73 | inputs: 74 | command: 'pack' 75 | packagesToPack: 'src/Dax.Template/Dax.Template.csproj' 76 | nobuild: true 77 | versioningScheme: 'byEnvVar' 78 | versionEnvVar: 'packageVersion' 79 | verbosityPack: '${{ parameters.verbosity }}' 80 | - task: PublishPipelineArtifact@1 81 | displayName: 'Publish artifacts' 82 | inputs: 83 | targetPath: '$(Build.ArtifactStagingDirectory)' 84 | artifact: 'drop' 85 | publishLocation: 'pipeline' -------------------------------------------------------------------------------- /src/Dax.Template/Tables/TemplateConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.Json.Serialization; 4 | using Dax.Template.Interfaces; 5 | using Dax.Template.Tables.Dates; 6 | using Dax.Template.Enums; 7 | using System.IO; 8 | 9 | namespace Dax.Template.Tables 10 | { 11 | 12 | public class TemplateConfiguration: IScanConfig, IDateTemplateConfig, IMeasureTemplateConfig, IHolidaysConfig, ICustomTableConfig, ITemplates, ILocalization 13 | { 14 | public string? TemplateUri { get; set; } 15 | public string? Name { get; set; } 16 | public string? Description { get; set; } 17 | 18 | // ITemplates implementation 19 | public ITemplates.TemplateEntry[]? Templates { get; set; } 20 | 21 | // ILocalization implementation 22 | public string? IsoTranslation { get; set; } 23 | /// 24 | /// If IsoFormat is null, there is not localization in FORMAT functions and the model language is used. 25 | /// If IsoFormat is not null, it corresponds to the third argument of FORMAT functions used to generate formatted strings. 26 | /// 27 | public string? IsoFormat { get; set; } 28 | public string[]? LocalizationFiles { get; set; } 29 | 30 | // IScanConfig implementation 31 | public string[]? OnlyTablesColumns { get; set; } 32 | public string[]? ExceptTablesColumns { get; set; } 33 | 34 | [JsonConverter(typeof(JsonStringEnumConverter))] 35 | public AutoScanEnum? AutoScan { get; set; } 36 | 37 | // IDateTemplateConfig implementation 38 | public int? FirstYearMin { get; set; } 39 | public int? FirstYearMax { get; set; } 40 | public int? LastYearMin { get; set; } 41 | public int? LastYearMax { get; set; } 42 | public int? FirstYear { get; set; } 43 | public int? LastYear { get; set; } 44 | 45 | // ICustomTableConfig implementation 46 | public Dictionary DefaultVariables { get; set; } = new(); 47 | 48 | // IHolidaysConfig implementation 49 | public string? IsoCountry { get; set; } 50 | public string? InLieuOfPrefix { get; set; } 51 | public string? InLieuOfSuffix { get; set; } 52 | public string? HolidaysDefinitionTable { get; set; } 53 | public string? WorkingDays { get; set; } 54 | 55 | public HolidaysConfig? HolidaysReference { get; set; } 56 | 57 | // IMeasureTemplateConfig implementation 58 | [JsonConverter(typeof(JsonStringEnumConverter))] 59 | public AutoNamingEnum? AutoNaming { get; set; } 60 | public string? AutoNamingSeparator { get; set; } 61 | public IMeasureTemplateConfig.TargetMeasure[]? TargetMeasures { get; set; } 62 | public string? TableSingleInstanceMeasures { get; set; } 63 | 64 | public string? DisplayFolderRule { get; set; } 65 | } 66 | 67 | public static class TemplateConfigurationExtensions 68 | { 69 | public static string ToTemplateUri(this FileInfo file) 70 | { 71 | var uriBuilder = new UriBuilder(file.FullName) 72 | { 73 | Scheme = Uri.UriSchemeFile 74 | }; 75 | 76 | return uriBuilder.Uri.AbsoluteUri; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Dax.Template/CustomTemplateDefinition.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Collections.Generic; 4 | 5 | namespace Dax.Template 6 | { 7 | public class CustomTemplateDefinition 8 | { 9 | public class DaxExpression 10 | { 11 | public string? Name { get; set; } 12 | public string? Expression { get; set; } 13 | public string[]? MultiLineExpression { get; set; } 14 | public string? Comment { get; set; } 15 | public string[]? MultiLineComment { get; set; } 16 | public string[]? GetComments() 17 | { 18 | return (MultiLineComment != null && MultiLineComment.Length > 0) 19 | ? MultiLineComment 20 | : (!string.IsNullOrWhiteSpace(Comment) ? new string[] { Comment } : null); 21 | } 22 | public string? GetExpression(string? padding = null) 23 | { 24 | return (string.IsNullOrEmpty(Expression) && MultiLineExpression != null) 25 | ? string.Join("", MultiLineExpression.Select(line => $"\r\n{padding}{line}")) 26 | : Expression; 27 | } 28 | } 29 | public class Step : DaxExpression 30 | { 31 | } 32 | public abstract class Variable : DaxExpression 33 | { 34 | } 35 | public class GlobalVariable : DaxExpression 36 | { 37 | public bool IsConfigurable { get; set; } = false; 38 | } 39 | public class RowVariable : DaxExpression 40 | { 41 | } 42 | public class Column : DaxExpression 43 | { 44 | public string? DataType { get; set; } 45 | public string? FormatString { get; set; } 46 | public bool IsHidden { get; set; } = false; 47 | public bool IsTemporary { get; set; } = false; 48 | public bool RequiresHolidays { get; set; } = false; 49 | public string? SortByColumn { get; set; } 50 | public string? DisplayFolder { get; set; } 51 | public string? DataCategory { get; set; } 52 | public string? Description { get; set; } 53 | public string? Step { get; set; } 54 | public string? AttributeType { get; set; } 55 | public string[]? AttributeTypes { get; set; } 56 | public Dictionary Annotations { get; set; } = new(); 57 | } 58 | public class HierarchyLevel 59 | { 60 | public string? Name { get; set; } 61 | public string? Column { get; set; } 62 | public string? Description { get; set; } 63 | } 64 | public class Hierarchy 65 | { 66 | public string? Name { get; set; } 67 | public string? Description { get; set; } 68 | public HierarchyLevel[] Levels { get; set; } = Array.Empty(); 69 | } 70 | public string[] FormatPrefixes { get; set; } = Array.Empty(); 71 | public Step[] Steps { get; set; } = Array.Empty(); 72 | public GlobalVariable[] GlobalVariables { get; set; } = Array.Empty(); 73 | public RowVariable[] RowVariables { get; set; } = Array.Empty(); 74 | public Column[] Columns { get; set; } = Array.Empty(); 75 | public Hierarchy[] Hierarchies { get; set; } = Array.Empty(); 76 | public Dictionary Annotations { get; set; } = new(); 77 | /// 78 | /// Define the calendar type for time intelligence calculations 79 | /// 80 | // public string? CalendarType { get; set; } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Dax.Template/Translations.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Dax.Template 8 | { 9 | public class Translations 10 | { 11 | #region Internal translation entities definition 12 | public class Entity 13 | { 14 | public string? OriginalName { get; set; } 15 | public string? Name { get; set; } 16 | public string? Description { get; set; } 17 | } 18 | 19 | public class EntityDisplayFolder : Entity 20 | { 21 | public string? DisplayFolders { get; set; } 22 | } 23 | public class EntityFormat : EntityDisplayFolder 24 | { 25 | public string? FormatString { get; set; } 26 | } 27 | public class Level : Entity { } 28 | public class Hierarchy : EntityDisplayFolder 29 | { 30 | public Level[] Levels { get; set; } = Array.Empty(); 31 | } 32 | public class Measure : EntityFormat { } 33 | public class Column : EntityFormat { } 34 | public class Table : Entity { } 35 | #endregion 36 | 37 | public class Language 38 | { 39 | public string? Iso { get; set; } 40 | public Table? Table { get; set; } 41 | public Measure[] Measures { get; set; } = Array.Empty(); 42 | public Column[] Columns { get; set; } = Array.Empty(); 43 | public Hierarchy[] Hierarchies { get; set; } = Array.Empty(); 44 | } 45 | 46 | public class Definitions 47 | { 48 | public Language[] Translations { get; set; } = Array.Empty(); 49 | } 50 | 51 | protected Definitions LanguageDefinitions; 52 | 53 | /// 54 | /// Define the Iso translation to use as default name, ignoring translations 55 | /// 56 | public string? DefaultIso { get; set; } 57 | /// 58 | /// List of ISO translations to apply 59 | /// 60 | public string[] ApplyIso { get; set; } = Array.Empty(); 61 | /// 62 | /// TRUE if all the ISO translations available should be applied as translations 63 | /// 64 | public bool ApplyAllIso { get; set; } = false; 65 | public Translations(Definitions definitions) 66 | { 67 | LanguageDefinitions = definitions; 68 | } 69 | 70 | public Language? GetTranslationIso( string iso ) 71 | { 72 | // First, search for perfect match ("it-IT" must be "it-IT", "it" must be "it") 73 | var matchingTranslation = LanguageDefinitions.Translations.FirstOrDefault(t => t.Iso == iso); 74 | if (matchingTranslation == null) 75 | { 76 | // Second, search for generic match ("it" instead of "it-IT") 77 | var genericIsoLanguage = iso[..2]; 78 | matchingTranslation = LanguageDefinitions.Translations.FirstOrDefault(t => t.Iso == genericIsoLanguage); 79 | if (matchingTranslation == null) 80 | { 81 | // Third, search for the first compatible match ("it-IT" instead of "it-CH" or "it") 82 | matchingTranslation = LanguageDefinitions.Translations.FirstOrDefault(t => t.Iso?[..2] == genericIsoLanguage); 83 | } 84 | } 85 | return matchingTranslation; 86 | } 87 | 88 | public IEnumerable GetTranslations() 89 | { 90 | return LanguageDefinitions.Translations; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Dax.Template/Package.cs: -------------------------------------------------------------------------------- 1 | using Dax.Template.Exceptions; 2 | using Dax.Template.Extensions; 3 | using Dax.Template.Tables; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Text.Encodings.Web; 8 | using System.Text.Json; 9 | 10 | namespace Dax.Template 11 | { 12 | public class Package 13 | { 14 | public const string TEMPLATE_FILE_EXTENSION = ".template.json"; 15 | public const string PACKAGE_CONFIG = "Config"; 16 | 17 | private readonly string _path; 18 | private readonly JsonDocument _document; 19 | private readonly TemplateConfiguration _configuration; 20 | private readonly string _directoryName; 21 | 22 | /// 23 | /// Load a from a template file 24 | /// 25 | /// Full path to the template file 26 | public static Package LoadFromFile(string path) 27 | { 28 | var packageFile = new FileInfo(path); 29 | var packageText = File.ReadAllText(path); 30 | var packageDocument = JsonDocument.Parse(packageText); 31 | 32 | string configurationText; 33 | 34 | if (packageDocument.RootElement.TryGetProperty(PACKAGE_CONFIG, out var configurationElement)) 35 | { 36 | if (configurationElement.ValueKind != JsonValueKind.Object) 37 | throw new TemplateConfigurationException($"Invalid json value kind [{ PACKAGE_CONFIG }]"); 38 | 39 | // File is a packaged template which contains the config and all referenced templates as embeded objects 40 | configurationText = configurationElement.GetRawText(); 41 | } 42 | else 43 | { 44 | // File is an unpackaged template which only contains the config object, all referenced templates are mapped as external files 45 | configurationText = packageText; 46 | } 47 | 48 | var templateConfiguration = JsonSerializer.Deserialize(configurationText) ?? throw new TemplateUnexpectedException("Deserialized configurationText is null"); 49 | { 50 | templateConfiguration.TemplateUri = packageFile.ToTemplateUri(); 51 | 52 | if (templateConfiguration.Name.IsNullOrEmpty()) 53 | templateConfiguration.Name = Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(packageFile.Name)); 54 | } 55 | 56 | var package = new Package(packageFile, packageDocument, templateConfiguration); 57 | return package; 58 | } 59 | 60 | /// 61 | /// Search for existing templates within local path 62 | /// 63 | /// The relative or absolute path to the directory to search 64 | public static IEnumerable FindTemplateFiles(string path) 65 | { 66 | var templateFiles = Directory.EnumerateFiles(path, searchPattern: $"*{ TEMPLATE_FILE_EXTENSION }"); 67 | return templateFiles; 68 | } 69 | 70 | private Package(FileInfo file, JsonDocument document, TemplateConfiguration configuration) 71 | { 72 | _path = file.FullName; 73 | _document = document; 74 | _configuration = configuration; 75 | 76 | _directoryName = file.DirectoryName ?? throw new TemplateUnexpectedException($"DirectoryName is null"); 77 | } 78 | 79 | public TemplateConfiguration Configuration => _configuration; 80 | 81 | internal T ReadDefinition(string name) 82 | { 83 | string definitionName = Path.GetExtension(name).EqualsI(".json") ? Path.GetFileNameWithoutExtension(name) : name; 84 | string definitionText; 85 | 86 | if (_document.RootElement.TryGetProperty(definitionName, out var element)) 87 | { 88 | definitionText = element.GetRawText(); 89 | } 90 | else 91 | { 92 | definitionText = File.ReadAllText(path: Path.Combine(_directoryName, name)); 93 | } 94 | 95 | return JsonSerializer.Deserialize(definitionText) ?? throw new TemplateUnexpectedException($"Deserialized definition is null [{ definitionName }]"); 96 | } 97 | 98 | public void SaveTo(string path) 99 | { 100 | Dictionary package = new(); 101 | package.Add(PACKAGE_CONFIG, Configuration); 102 | 103 | var fileNames = 104 | from t in Configuration.Templates 105 | where !string.IsNullOrEmpty(t.Template) 106 | select t.Template; 107 | 108 | fileNames = fileNames.Union( 109 | from t in Configuration.Templates 110 | from l in t.LocalizationFiles 111 | where !string.IsNullOrEmpty(l) 112 | select l).Distinct(); 113 | 114 | foreach (var fileName in fileNames) 115 | { 116 | var filePath = Path.Combine(_directoryName, fileName); 117 | var fileText = File.ReadAllText(filePath); 118 | 119 | var content = JsonSerializer.Deserialize(fileText); 120 | var name = Path.GetFileNameWithoutExtension(fileName); 121 | 122 | package.Add(name, content); 123 | } 124 | 125 | var options = new JsonSerializerOptions 126 | { 127 | Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, 128 | WriteIndented = true 129 | }; 130 | 131 | var packageText = JsonSerializer.Serialize(package, options); 132 | 133 | File.WriteAllText(path, packageText); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Dax.Template.TestUI/Templates/DateTemplate-03.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/sql-bi/Bravo/main/src/Assets/ManageDates/Schemas/date-template.schema.json", 3 | "_comment": "Reference Monthly Gregorian Calendar", 4 | "CalendarTypes": [ "Calendar" ], 5 | "FormatPrefixes": [ "M", "Q" ], 6 | "Steps": [ 7 | { 8 | "Name": "__Calendar", 9 | "Expression": "@@GETCALENDAR()" 10 | } 11 | ], 12 | "GlobalVariables": [ 13 | { 14 | "Name": "__MonthsInYear", 15 | "Expression": "12", 16 | "IsConfigurable": true 17 | }, 18 | { 19 | "Name": "__OffsetFiscalMonthNumber", 20 | "Expression": "__MonthsInYear + 1 - (__MonthsInYear - 12)" 21 | } 22 | ], 23 | "RowVariables": [ 24 | { 25 | "Name": "__Date", 26 | "Expression": "[Date]" 27 | }, 28 | { 29 | "Name": "__YearMonthNumber", 30 | "Expression": "YEAR ( __Date ) * __MonthsInYear + MONTH ( __Date ) - 1" 31 | }, 32 | { 33 | "Name": "__MonthNumber", 34 | "Expression": "MOD ( __YearMonthNumber, __MonthsInYear ) + 1" 35 | }, 36 | { 37 | "Name": "__YearNumber", 38 | "Expression": "QUOTIENT ( __YearMonthNumber, __MonthsInYear )" 39 | }, 40 | 41 | { 42 | "Name": "__YearMonthKey", 43 | "Expression": "__YearNumber * 100 + __MonthNumber" 44 | }, 45 | { 46 | "Name": "__MonthDate", 47 | "Expression": "DATE ( __YearNumber, __MonthNumber, 1 )" 48 | }, 49 | { 50 | "Name": "__MonthInQuarterNumber", 51 | "Expression": "MOD ( __MonthNumber - 1, 3 ) + 1 + 3 * ( __MonthNumber > 12 )" 52 | }, 53 | { 54 | "Name": "__QuarterNumber", 55 | "Expression": "MIN ( ROUNDUP ( __MonthNumber / 3, 0 ), 4 )" 56 | }, 57 | { 58 | "Name": "__YearQuarterNumber", 59 | "Expression": "__YearNumber * 4 + __QuarterNumber - 1" 60 | }, 61 | { 62 | "Name": "__IsStandardLocale", 63 | "Expression": "IF ( FORMAT( DATE( 2000, 1, 1 ), \"oooo\"@@GETISO() ) = \"oooo\", TRUE, FALSE )" 64 | }, 65 | { 66 | "Name": "__MonthFormatString", 67 | "Expression": "IF( __IsStandardLocale, \"mmm\", \"ooo\" )" 68 | } 69 | ], 70 | "Columns": [ 71 | { 72 | "Name": "Date", 73 | "DataType": "DateTime", 74 | "FormatString": null, 75 | "Step": "__Calendar", 76 | "DataCategory": "PaddedDateTableDates", 77 | "AttributeTypes": [ 78 | "Date" 79 | ] 80 | }, 81 | { 82 | "Name": "Year Month Key", 83 | "Expression": "__YearNumber * 100 + __MonthNumber", 84 | "DataType": "Int64", 85 | "IsHidden": true 86 | }, 87 | { 88 | "Name": "Month", 89 | "MultiLineExpression": [ 90 | "IF (", 91 | " __MonthNumber > 12,", 92 | " FORMAT ( __MonthNumber, \"@_M_@00\"@@GETISO() ),", 93 | " FORMAT ( __MonthDate, __MonthFormatString@@GETISO() )", 94 | ")" 95 | ], 96 | "DataType": "String", 97 | "SortByColumn": "Month Number", 98 | "DataCategory": "MonthOfYear" 99 | }, 100 | { 101 | "Name": "Month Number", 102 | "Expression": "__MonthNumber", 103 | "DataType": "Int64", 104 | "IsHidden": true, 105 | "AttributeType": "MonthOfYear", 106 | "DataCategory": "MonthOfYear" 107 | }, 108 | { 109 | "Name": "Year Month", 110 | "MultiLineExpression": [ 111 | "IF (", 112 | " __MonthNumber > 12,", 113 | " FORMAT ( __MonthNumber, \"@_M_@00\"@@GETISO() ) & FORMAT ( __YearNumber, \" 0000\"@@GETISO() ),", 114 | " FORMAT ( __Date, __MonthFormatString & \" yyyy\"@@GETISO() )", 115 | ")" 116 | ], 117 | "DataType": "String", 118 | "SortByColumn": "Year Month Number", 119 | "": "Months" 120 | }, 121 | { 122 | "Name": "Year Month Number", 123 | "Expression": "__YearMonthNumber", 124 | "DataType": "Int64", 125 | "IsHidden": true, 126 | "DataCategory": "Months", 127 | "AttributeTypes": [ 128 | "Months", 129 | "FiscalMonths" 130 | ] 131 | }, 132 | { 133 | "Name": "Month In Quarter Number", 134 | "Expression": "__MonthInQuarterNumber", 135 | "DataType": "Int64", 136 | "IsHidden": true, 137 | "AttributeType": "MonthOfQuarter", 138 | "DataCategory": "MonthOfQuarter" 139 | }, 140 | { 141 | "Name": "Quarter", 142 | "Expression": "FORMAT( __Date, \"@_Q_@Q\"@@GETISO() )", 143 | "DataType": "String", 144 | "DataCategory": "QuarterOfYear" 145 | }, 146 | { 147 | "Name": "Year Quarter", 148 | "Expression": "FORMAT ( __QuarterNumber, \"@_Q_@0\"@@GETISO() ) & \"-\" & FORMAT ( __YearNumber, \"0000\"@@GETISO() )", 149 | "DataType": "String", 150 | "SortByColumn": "Year Quarter Number", 151 | "DataCategory": "Quarters" 152 | }, 153 | { 154 | "Name": "Year Quarter Number", 155 | "Expression": "__YearQuarterNumber", 156 | "DataType": "Int64", 157 | "IsHidden": true, 158 | "AttributeType": "Quarters", 159 | "DataCategory": "Quarters" 160 | }, 161 | { 162 | "Name": "Year", 163 | "Expression": "__YearNumber", 164 | "DataType": "Int64", 165 | "AttributeType": "Years", 166 | "DataCategory": "Years" 167 | } 168 | ], 169 | "Hierarchies": [ 170 | { 171 | "Name": "Year-Quarter", 172 | "Levels": [ 173 | { 174 | "Name": "Year", 175 | "Column": "Year" 176 | }, 177 | { 178 | "Name": "Quarter", 179 | "Column": "Year Quarter" 180 | }, 181 | { 182 | "Name": "Month", 183 | "Column": "Year Month" 184 | } 185 | ] 186 | }, 187 | { 188 | "Name": "Year-Month", 189 | "Levels": [ 190 | { 191 | "Name": "Year", 192 | "Column": "Year" 193 | }, 194 | { 195 | "Name": "Month", 196 | "Column": "Year Month" 197 | } 198 | ] 199 | } 200 | ] 201 | } -------------------------------------------------------------------------------- /src/Dax.Template/Extensions/GetScanColumns.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Collections.Generic; 3 | using Microsoft.AnalysisServices.Tabular; 4 | using Column = Dax.Template.Model.Column; 5 | using TabularModel = Microsoft.AnalysisServices.Tabular.Model; 6 | using TabularColumn = Microsoft.AnalysisServices.Tabular.Column; 7 | using System.Text.RegularExpressions; 8 | using Dax.Template.Interfaces; 9 | using Dax.Template.Enums; 10 | 11 | namespace Dax.Template.Extensions 12 | { 13 | public static partial class Extensions 14 | { 15 | private static readonly Regex regexTable = new(@".+(?=\[)", RegexOptions.Compiled); 16 | private static readonly Regex regexColumn = new(@"(\[.+\])", RegexOptions.Compiled); 17 | 18 | public static (string? tableName, string? columnName) SplitDaxIdentifier(string daxIdentifier) 19 | { 20 | var matchTable = regexTable.Matches(daxIdentifier); 21 | var matchColumn = regexColumn.Matches(daxIdentifier); 22 | 23 | var columnName = matchColumn.FirstOrDefault()?.Value; 24 | var tableName = matchTable.FirstOrDefault()?.Value; 25 | if (tableName == null && columnName == null) tableName = daxIdentifier; 26 | 27 | // Remove quoted identifier for table name 28 | if (tableName?[0] == '\'') 29 | tableName = tableName[1..^1]; 30 | 31 | // Remove bracket identifier for column name 32 | if (columnName?[0] == '[') 33 | columnName = columnName[1..^1]; 34 | 35 | return (tableName, columnName); 36 | } 37 | 38 | public static IEnumerable? GetScanColumns(this TabularModel model, IScanConfig Config, string? dataCategory = null) 39 | { 40 | IEnumerable? scanColumns = null; 41 | var scanTargets = 42 | from item in Config.OnlyTablesColumns 43 | select SplitDaxIdentifier(item); 44 | var exceptTargets = 45 | from item in Config.ExceptTablesColumns 46 | select SplitDaxIdentifier(item); 47 | var exceptTables = exceptTargets.Where(x => string.IsNullOrEmpty(x.columnName)).ToList(); 48 | var exceptColumns = exceptTargets.Where(x => !string.IsNullOrEmpty(x.columnName)).ToList(); 49 | 50 | if ((Config.AutoScan & AutoScanEnum.SelectedTablesColumns) == AutoScanEnum.SelectedTablesColumns) 51 | { 52 | List columnsToScan = new(); 53 | bool scanAll = (Config.OnlyTablesColumns == null) || Config.OnlyTablesColumns.Length == 0; 54 | 55 | var onlyTables = scanTargets.Where(x => string.IsNullOrEmpty(x.columnName)).ToList(); 56 | var onlyColumns = scanTargets.Where(x => !string.IsNullOrEmpty(x.columnName)).ToList(); 57 | 58 | scanColumns = 59 | from t in model.Tables 60 | from c in t.Columns 61 | where c.DataType == DataType.DateTime 62 | && ( 63 | scanAll 64 | || onlyTables.Any(o => o.tableName == t.Name) 65 | || onlyColumns.Any(o => o.columnName == c.Name && o.tableName == t.Name) 66 | ) 67 | && !exceptTables.Any(o => o.tableName == t.Name) 68 | && !exceptColumns.Any(o => o.columnName == c.Name && o.tableName == t.Name) 69 | && (dataCategory == null || t.DataCategory != dataCategory) // DATACATEGORY_TIME 70 | select c; 71 | } 72 | bool checkInactive = (Config.AutoScan & AutoScanEnum.ScanInactiveRelationships) == AutoScanEnum.ScanInactiveRelationships; 73 | bool checkActive = (Config.AutoScan & AutoScanEnum.ScanActiveRelationships) == AutoScanEnum.ScanActiveRelationships || checkInactive; 74 | if ( checkInactive || checkActive ) 75 | { 76 | var scanRelationshipsFrom = 77 | from r in ( 78 | from r in model.Relationships 79 | where r is SingleColumnRelationship 80 | select r as SingleColumnRelationship) 81 | where r.FromColumn.DataType == DataType.DateTime 82 | && r.FromCardinality == RelationshipEndCardinality.Many 83 | && (dataCategory == null || r.FromTable.DataCategory != dataCategory) // DATACATEGORY_TIME 84 | && ((checkActive && r.IsActive) || (checkInactive && !r.IsActive)) 85 | && !exceptTables.Any(o => o.tableName == r.FromTable.Name) 86 | && !exceptColumns.Any(o => o.columnName == r.FromColumn.Name && o.tableName == r.FromTable.Name) 87 | select r.FromColumn; 88 | var scanRelationshipsTo = 89 | from r in ( 90 | from r in model.Relationships 91 | where r is SingleColumnRelationship 92 | select r as SingleColumnRelationship) 93 | where r.ToColumn.DataType == DataType.DateTime 94 | && r.ToCardinality == RelationshipEndCardinality.Many 95 | && (dataCategory == null || r.ToTable.DataCategory != dataCategory) // DATACATEGORY_TIME 96 | && ((checkActive && r.IsActive) || (checkInactive && !r.IsActive)) 97 | && !exceptTables.Any(o => o.tableName == r.ToTable.Name) 98 | && !exceptColumns.Any(o => o.columnName == r.ToColumn.Name && o.tableName == r.ToTable.Name) 99 | select r.ToColumn; 100 | var scanRelationships = scanRelationshipsFrom.Union(scanRelationshipsTo); 101 | scanColumns = (scanColumns == null) ? scanRelationships : scanColumns.Union(scanRelationships); 102 | } 103 | 104 | // Remove columns that are marked as Time 105 | scanColumns = scanColumns?.Where(c => !(c.Annotations.FirstOrDefault(a => a.Name == "UnderlyingDateTimeDataType")?.Value == "Time")); 106 | 107 | return scanColumns; 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Dax.Template.TestUI/Templates/DateTemplate-01.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/sql-bi/Bravo/main/src/Assets/ManageDates/Schemas/date-template.schema.json", 3 | "_comment": "Reference Q prefix format in DAX expression using the syntax @_Q_@", 4 | "CalendarType": "Calendar", 5 | "FormatPrefixes": [ "Q" ], 6 | "Steps": [ 7 | { 8 | "Name": "__Calendar", 9 | "Expression": "@@GETCALENDAR()" 10 | } 11 | ], 12 | "GlobalVariables": [ 13 | { 14 | "Name": "__FirstDayOfWeek", 15 | "Expression": "0", 16 | "IsConfigurable": true 17 | }, 18 | { 19 | "Name": "__WeekDayCalculationType", 20 | "Expression": "IF ( __FirstDayOfWeek = 0, 7, __FirstDayOfWeek ) + 10" 21 | }, 22 | { 23 | "Name": "__WorkingDays", 24 | "Expression": "@@GETCONFIG( WorkingDays )", 25 | "IsConfigurable": true 26 | } 27 | ], 28 | "RowVariables": [ 29 | { 30 | "Name": "__Date", 31 | "Expression": "[Date]" 32 | }, 33 | { 34 | "Name": "__YearNumber", 35 | "Expression": "YEAR ( __Date )" 36 | }, 37 | { 38 | "Name": "__MonthNumber", 39 | "Expression": "MONTH ( __Date )" 40 | }, 41 | { 42 | "Name": "__QuarterNumber", 43 | "Expression": "QUARTER ( __Date )" 44 | }, 45 | { 46 | "Name": "__MonthNumberQ", 47 | "Expression": "__MonthNumber - 3 * (__QuarterNumber - 1)" 48 | }, 49 | { 50 | "Name": "__YearQuarterNumber", 51 | "Expression": "CONVERT ( __YearNumber * 4 + __QuarterNumber - 1, INTEGER )" 52 | }, 53 | { 54 | "Name": "__WeekDayNumber", 55 | "Expression": "WEEKDAY ( __Date, __WeekDayCalculationType )" 56 | }, 57 | { 58 | "Name": "__WeekDay", 59 | "Expression": "FORMAT ( __Date, __DayFormatString@@GETISO() )" 60 | }, 61 | { 62 | "Name": "__HolidayName", 63 | "Expression": "@@GETHOLIDAYNAME( __Date )" 64 | }, 65 | { 66 | "Name": "__IsWorkingDay", 67 | "Expression": "WEEKDAY ( __Date, 1 ) IN __WorkingDays && ISBLANK ( __HolidayName )" 68 | }, 69 | { 70 | "Name": "__LastTransactionDate", 71 | "Expression": "@@GETMAXDATE()" 72 | }, 73 | { 74 | "Name": "__IsStandardLocale", 75 | "Expression": "IF ( FORMAT( DATE( 2000, 1, 1 ), \"oooo\"@@GETISO() ) = \"oooo\", TRUE, FALSE )" 76 | }, 77 | { 78 | "Name": "__DayFormatString", 79 | "Expression": "IF( __IsStandardLocale, \"ddd\", \"aaa\" )" 80 | }, 81 | { 82 | "Name": "__MonthFormatString", 83 | "Expression": "IF( __IsStandardLocale, \"mmm\", \"ooo\" )" 84 | } 85 | ], 86 | "Columns": [ 87 | { 88 | "Name": "Date", 89 | "DataType": "DateTime", 90 | "FormatString": null, 91 | "Step": "__Calendar", 92 | "DataCategory": "PaddedDateTableDates", 93 | "AttributeTypes": [ 94 | "Date" 95 | ] 96 | }, 97 | { 98 | "Name": "Year", 99 | "Expression": "__YearNumber", 100 | "DataType": "Int64", 101 | "DataCategory": "Years" 102 | }, 103 | { 104 | "Name": "Year Quarter Number", 105 | "Expression": "__YearQuarterNumber", 106 | "DataType": "Int64", 107 | "IsHidden": true, 108 | "DataCategory": "Quarters" 109 | }, 110 | { 111 | "Name": "Year Quarter", 112 | "Expression": "FORMAT ( __QuarterNumber, \"@_Q_@0\"@@GETISO() ) & \"-\" & FORMAT ( __YearNumber, \"0000\"@@GETISO() )", 113 | "DataType": "String", 114 | "SortByColumn": "Year Quarter Number", 115 | "DataCategory": "Quarters" 116 | }, 117 | { 118 | "Name": "Quarter", 119 | "Expression": "FORMAT( __QuarterNumber, \"@_Q_@0\"@@GETISO() )", 120 | "DataType": "String", 121 | "DataCategory": "QuarterOfYear" 122 | }, 123 | { 124 | "Name": "Year Month", 125 | "Expression": "FORMAT ( __Date, __MonthFormatString & \" yyyy\"@@GETISO() )", 126 | "DataType": "String", 127 | "SortByColumn": "Year Month Number", 128 | "DataCategory": "Months" 129 | }, 130 | { 131 | "Name": "Year Month Number", 132 | "Expression": "__YearNumber * 12 + __MonthNumber - 1", 133 | "DataType": "Int64", 134 | "IsHidden": true, 135 | "DataCategory": "Months" 136 | }, 137 | { 138 | "Name": "Month", 139 | "Expression": "FORMAT ( __Date, __MonthFormatString@@GETISO() )", 140 | "DataType": "String", 141 | "SortByColumn": "Month Number", 142 | "DataCategory": "MonthOfYear" 143 | }, 144 | { 145 | "Name": "Month Number", 146 | "Expression": "__MonthNumber", 147 | "DataType": "Int64", 148 | "IsHidden": true, 149 | "DataCategory": "MonthOfYear" 150 | }, 151 | { 152 | "Name": "Day of Week Number", 153 | "Expression": "__WeekDayNumber", 154 | "DataType": "Int64", 155 | "IsHidden": true, 156 | "AttributeType": "DayOfWeek", 157 | "DataCategory": "DayOfWeek", 158 | "Annotations": { 159 | "SQLBI_FilterSafe": true 160 | } 161 | }, 162 | { 163 | "Name": "Day of Week", 164 | "Expression": "__WeekDay", 165 | "DataType": "String", 166 | "SortByColumn": "Day of Week Number", 167 | "DataCategory": "DayOfWeek", 168 | "Annotations": { 169 | "SQLBI_FilterSafe": true 170 | } 171 | }, 172 | { 173 | "Name": "IsWorking", 174 | "Expression": "__IsWorkingDay", 175 | "DataType": "Boolean", 176 | "IsHidden": true, 177 | "RequiresHolidays": true, 178 | "Annotations": { 179 | "SQLBI_FilterSafe": true 180 | } 181 | }, 182 | { 183 | "Name": "Working Day Value", 184 | "Expression": "IF ( __IsWorkingDay, 1 )", 185 | "DataType": "Int64", 186 | "IsHidden": true, 187 | "RequiresHolidays": true, 188 | "Annotations": { 189 | "SQLBI_FilterSafe": true 190 | } 191 | }, 192 | { 193 | "Name": "Holiday Name", 194 | "Expression": "__HolidayName", 195 | "DataType": "String", 196 | "RequiresHolidays": true, 197 | "Annotations": { 198 | "SQLBI_FilterSafe": true 199 | } 200 | }, 201 | { 202 | "Name": "DateWithTransactions", 203 | "Expression": "__Date <= __LastTransactionDate", 204 | "DataType": "Boolean", 205 | "IsHidden": true, 206 | "AttributeType": "DateDuration" 207 | } 208 | ], 209 | "Hierarchies": [ 210 | { 211 | "Name": "Calendar", 212 | "Levels": [ 213 | { 214 | "Name": "Year", 215 | "Column": "Year" 216 | }, 217 | { 218 | "Name": "Month", 219 | "Column": "Year Month" 220 | }, 221 | { 222 | "Name": "Date", 223 | "Column": "Date" 224 | } 225 | ] 226 | } 227 | ] 228 | } -------------------------------------------------------------------------------- /src/Dax.Template/Extensions/TSort.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Collections.Generic; 4 | using Dax.Template.Syntax; 5 | using Dax.Template.Exceptions; 6 | 7 | namespace Dax.Template.Extensions 8 | { 9 | 10 | public static partial class Extensions 11 | { 12 | 13 | public static IEnumerable<(T item, int level)> TSort(this IEnumerable source, Func?> dependencies, bool onlyAddLevel = true) where T : Syntax.IDependencies 14 | { 15 | var sorted = new List<(T item, int level)>(); 16 | var visited = new HashSet(); 17 | 18 | if (!source.Any()) 19 | { 20 | return sorted; 21 | } 22 | 23 | foreach (var item in source.Where(n => (!onlyAddLevel) || n.AddLevel)) 24 | { 25 | Visit(item, visited, sorted, dependencies); 26 | } 27 | 28 | if (!sorted.Any()) 29 | { 30 | return sorted; 31 | } 32 | 33 | // Add the dependencies required in each level (usually row variables) 34 | var result = new List<(T item, int level)>(); 35 | int min = sorted.Min(element => element.level); 36 | int max = sorted.Max(element => element.level); 37 | for (int currentLoopLevel = min; currentLoopLevel <= max; currentLoopLevel++) 38 | { 39 | result.AddRange(sorted.Where(element => element.level == currentLoopLevel && element.item.AddLevel)); 40 | var referencedVariables = 41 | from element in sorted 42 | where element.level == currentLoopLevel && element.item.AddLevel == false 43 | select element; 44 | var referencedDependencies = 45 | (from element in sorted 46 | where element.level == currentLoopLevel 47 | select element.item as Syntax.IDependencies 48 | ).GetDependencies(includeSelf: false); 49 | var previousVariables = referencedDependencies.Where(element => element.AddLevel == false).TSort(v => v.Dependencies, onlyAddLevel: false); 50 | if (result.Any(t => t.level == currentLoopLevel)) 51 | { 52 | var pos = result.FirstOrDefault(t => t.level == currentLoopLevel); 53 | result.InsertRange( 54 | result.IndexOf(pos), 55 | from element in sorted 56 | where element.level < currentLoopLevel 57 | && element.item.AddLevel == false 58 | && previousVariables.Any(item => object.ReferenceEquals(item.item, element.item)) 59 | && !result.Any(existingItem => object.ReferenceEquals(existingItem.item, element.item) && existingItem.level == currentLoopLevel) 60 | && element.item is not Syntax.IGlobalScope 61 | select (element.item, currentLoopLevel) 62 | ); 63 | result.InsertRange( 64 | result.IndexOf(pos), 65 | from element in referencedVariables 66 | select (element.item, element.level + 1) 67 | ); 68 | } 69 | else 70 | { 71 | result.AddRange( 72 | from element in sorted 73 | where element.level < currentLoopLevel 74 | && element.item.AddLevel == false 75 | && previousVariables.Any(item => object.ReferenceEquals(item.item, element.item)) 76 | && !result.Any(existingItem => object.ReferenceEquals(existingItem.item, element.item) && existingItem.level == currentLoopLevel) 77 | && element.item is not Syntax.IGlobalScope 78 | select (element.item, currentLoopLevel) 79 | ); 80 | result.AddRange( 81 | from element in referencedVariables 82 | select (element.item, element.level + 1) 83 | ); 84 | 85 | } 86 | } 87 | return result; 88 | } 89 | 90 | /// 91 | /// Maximum number of nested calls in VisitDependencies 92 | /// 93 | private const int MAX_NESTED_CALLS = 1000; 94 | 95 | private static void Visit(T item, HashSet visited, List<(T, int level)> sorted, Func?> dependencies) where T : Syntax.IDependencies 96 | { 97 | if (!visited.Contains(item)) 98 | { 99 | visited.Add(item); 100 | 101 | var allDependencies = dependencies(item); 102 | 103 | if (allDependencies != null) 104 | { 105 | foreach (var dep in allDependencies) 106 | { 107 | Visit(dep, visited, sorted, dependencies); 108 | } 109 | } 110 | 111 | int level = VisitDependencies(item, visited, sorted, dependencies); 112 | sorted.Add((item, level)); 113 | } 114 | } 115 | 116 | private static int VisitDependencies(T item, HashSet visited, List<(T, int level)> sorted, Func?> dependencies, int level = 0, int nestedCalls = 0) where T : Syntax.IDependencies 117 | { 118 | var allDependencies = dependencies(item); 119 | // var dependenciesListAddLevel = allDependencies?.Where(d => d.AddLevel == true); 120 | 121 | if (nestedCalls > MAX_NESTED_CALLS) 122 | { 123 | string? varName = (item as IDaxName)?.DaxName.ToString(); 124 | throw new CircularDependencyException(varName, "{STACK OVERFLOW: check complex dependencies}"); 125 | } 126 | 127 | if (allDependencies?.Contains(item) == true) 128 | { 129 | throw new CircularDependencyException((item as IDaxName)?.DaxName.ToString(), item.Expression); 130 | } 131 | 132 | level += item.AddLevel ? 1 : 0; 133 | int maxLevel = level; 134 | if (allDependencies != null) 135 | { 136 | foreach (var dep in allDependencies) 137 | { 138 | var nestedLevel = VisitDependencies(dep, visited, sorted, dependencies, level, ++nestedCalls); 139 | if (nestedLevel > maxLevel) 140 | { 141 | maxLevel = nestedLevel; 142 | } 143 | } 144 | } 145 | 146 | return maxLevel; 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/Dax.Template.TestUI/Templates/DateTemplate-95.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/sql-bi/Bravo/main/src/Assets/ManageDates/Schemas/date-template.schema.json", 3 | "_comment": "Reference FY prefix format in DAX expression using the syntax @_FY_@", 4 | "CalendarType": "Calendar", 5 | "FormatPrefixes": [ "Q", "FY", "FQ" ], 6 | "Steps": [ 7 | { 8 | "Name": "__Calendar", 9 | "Expression": "@@GETCALENDAR()" 10 | } 11 | ], 12 | "GlobalVariables": [ 13 | { 14 | "Name": "__FirstFiscalMonth", 15 | "Expression": "7", 16 | "IsConfigurable": true 17 | }, 18 | { 19 | "Name": "__FirstDayOfWeek", 20 | "Expression": "0", 21 | "IsConfigurable": true 22 | }, 23 | { 24 | "Name": "__WorkingDays", 25 | "Expression": "@@GETCONFIG( WorkingDays )", 26 | "IsConfigurable": true 27 | } 28 | ], 29 | "RowVariables": [ 30 | { 31 | "Name": "__Date", 32 | "Expression": "[Date]" 33 | }, 34 | { 35 | "Name": "__YearNumber", 36 | "Expression": "YEAR ( __Date )" 37 | }, 38 | { 39 | "Name": "__MonthNumber", 40 | "Expression": "MONTH ( __Date )" 41 | }, 42 | { 43 | "Name": "__QuarterNumber", 44 | "Expression": "QUARTER ( __Date )" 45 | }, 46 | { 47 | "Name": "__MonthNumberQ", 48 | "Expression": "__MonthNumber - 3 * (__QuarterNumber - 1)" 49 | }, 50 | { 51 | "Name": "__WeekdayNumber", 52 | "Expression": "WEEKDAY ( __Date, 1 ) - 1" 53 | }, 54 | { 55 | "Name": "__FiscalYearNumber", 56 | "Expression": "__YearNumber + 1 * ( __FirstFiscalMonth > 1 && __MonthNumber >= __FirstFiscalMonth )" 57 | }, 58 | { 59 | "Name": "__FiscalQuarterNumber", 60 | "Expression": "FORMAT ( EOMONTH ( __Date, 1 - __FirstFiscalMonth ), \"@_FQ_@Q\"@@GETISO() )" 61 | }, 62 | { 63 | "Name": "__HolidayName", 64 | "Expression": "@@GETHOLIDAYNAME( __Date )" 65 | }, 66 | { 67 | "Name": "__IsWorkingDay", 68 | "Expression": "WEEKDAY ( __Date, 1 ) IN __WorkingDays && ISBLANK ( __HolidayName )" 69 | }, 70 | { 71 | "Name": "__LastTransactionDate", 72 | "Expression": "@@GETMAXDATE()" 73 | } 74 | ], 75 | "Columns": [ 76 | { 77 | "Name": "Date", 78 | "DataType": "DateTime", 79 | "FormatString": "m/dd/yyyy", 80 | "Step": "__Calendar", 81 | "DataCategory": "PaddedDateTableDates", 82 | "AttributeTypes": [ 83 | "Date", 84 | "FiscalDate" 85 | ] 86 | }, 87 | { 88 | "Name": "Year", 89 | "Expression": "DATE(__YearNumber, 12, 31)", 90 | "DataType": "DateTime", 91 | "FormatString": "yyyy", 92 | "DataCategory": "Years" 93 | }, 94 | { 95 | "Name": "Year Quarter Date", 96 | "Expression": "EOMONTH( __Date, 3 - __MonthNumberQ )", 97 | "DataType": "DateTime", 98 | "FormatString": "m/dd/yyyy", 99 | "IsHidden": true, 100 | "DataCategory": "Quarters" 101 | }, 102 | { 103 | "Name": "Year Quarter", 104 | "_comment": "TODO Add FORMAT argument for localization", 105 | "Expression": "FORMAT( __Date, \"@_Q_@Q-YYYY\")", 106 | "DataType": "String", 107 | "SortByColumn": "Year Quarter Date" 108 | }, 109 | { 110 | "Name": "Quarter", 111 | "Expression": "FORMAT( __Date, \"@_Q_@Q\" )", 112 | "DataType": "String", 113 | "DataCategory": "QuarterOfYear" 114 | }, 115 | { 116 | "_comment": "Use this version for end-of-month", 117 | "_Name": "Year Month", 118 | "_Expression": "EOMONTH( _Date, 0 )", 119 | "_DataType": "DateTime", 120 | "_comment_1": "Use this version for beginning-of-month", 121 | "Name": "Year Month", 122 | "Expression": "EOMONTH( __Date, -1 ) + 1", 123 | "DataType": "DateTime", 124 | "FormatString": "mmm yyyy", 125 | "DataCategory": "Months" 126 | }, 127 | { 128 | "Name": "Month", 129 | "Expression": "DATE(1900, MONTH( __Date ), 1 )", 130 | "DataType": "DateTime", 131 | "FormatString": "mmm", 132 | "DataCategory": "MonthOfYear" 133 | }, 134 | { 135 | "Name": "Day of Week", 136 | "Expression": "DATE(1900, 1, 7 + __WeekdayNumber + (7 * (__WeekdayNumber < __FirstDayOfWeek)))", 137 | "DataType": "DateTime", 138 | "FormatString": "ddd", 139 | "DataCategory": "DayOfWeek", 140 | "Annotations": { 141 | "SQLBI_FilterSafe": true 142 | } 143 | }, 144 | { 145 | "Name": "Fiscal Year", 146 | "Expression": "DATE(__FiscalYearNumber + (__FirstFiscalMonth = 1), __FirstFiscalMonth, 1) - 1", 147 | "DataType": "DateTime", 148 | "FormatString": "@_FY_@ yyyy", 149 | "DisplayFolder": "Fiscal", 150 | "DataCategory": "FiscalYears" 151 | }, 152 | { 153 | "Name": "Fiscal Year Quarter", 154 | "Expression": "__FiscalQuarterNumber & \"-\" & __FiscalYearNumber", 155 | "DataType": "String", 156 | "SortByColumn": "Fiscal Year Quarter Date", 157 | "DisplayFolder": "Fiscal" 158 | }, 159 | { 160 | "Name": "Fiscal Year Quarter Date", 161 | "Expression": "EOMONTH( __Date, 3 - __MonthNumberQ )", 162 | "DataType": "DateTime", 163 | "FormatString": "m/dd/yyyy", 164 | "IsHidden": true, 165 | "DisplayFolder": "Fiscal", 166 | "DataCategory": "FiscalQuarters" 167 | }, 168 | { 169 | "Name": "Fiscal Quarter", 170 | "Expression": "__FiscalQuarterNumber", 171 | "DataType": "String", 172 | "DisplayFolder": "Fiscal", 173 | "DataCategory": "FiscalQuarterOfYear" 174 | }, 175 | { 176 | "Name": "IsWorking", 177 | "Expression": "__IsWorkingDay", 178 | "DataType": "Boolean", 179 | "IsHidden": true, 180 | "RequiresHolidays": true, 181 | "Annotations": { 182 | "SQLBI_FilterSafe": true 183 | } 184 | }, 185 | { 186 | "Name": "Working Day Value", 187 | "Expression": "IF ( __IsWorkingDay, 1 )", 188 | "DataType": "Int64", 189 | "IsHidden": true, 190 | "RequiresHolidays": true, 191 | "Annotations": { 192 | "SQLBI_FilterSafe": true 193 | } 194 | }, 195 | { 196 | "Name": "Holiday Name", 197 | "Expression": "__HolidayName", 198 | "DataType": "String", 199 | "RequiresHolidays": true, 200 | "Annotations": { 201 | "SQLBI_FilterSafe": true 202 | } 203 | }, 204 | { 205 | "Name": "DateWithTransactions", 206 | "Expression": "__Date <= __LastTransactionDate", 207 | "DataType": "Boolean", 208 | "IsHidden": true, 209 | "AttributeType": "DateDuration" 210 | } 211 | ], 212 | "Hierarchies": [ 213 | { 214 | "Name": "Calendar", 215 | "Levels": [ 216 | { 217 | "Name": "Year", 218 | "Column": "Year" 219 | }, 220 | { 221 | "Name": "Month", 222 | "Column": "Year Month" 223 | }, 224 | { 225 | "Name": "Date", 226 | "Column": "Date" 227 | } 228 | ] 229 | }, 230 | { 231 | "Name": "Fiscal", 232 | "Levels": [ 233 | { 234 | "Name": "Year", 235 | "Column": "Fiscal Year" 236 | }, 237 | { 238 | "Name": "Month", 239 | "Column": "Year Month" 240 | }, 241 | { 242 | "Name": "Date", 243 | "Column": "Date" 244 | } 245 | ] 246 | } 247 | ] 248 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd 364 | -------------------------------------------------------------------------------- /src/Dax.Template.TestUI/Templates/DateTemplate-02.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/sql-bi/Bravo/main/src/Assets/ManageDates/Schemas/date-template.schema.json", 3 | "_comment": "Reference FY prefix format in DAX expression using the syntax @_FY_@", 4 | "CalendarType": "Fiscal", 5 | "FormatPrefixes": [ "FY", "FQ" ], 6 | "Steps": [ 7 | { 8 | "Name": "__Calendar", 9 | "Expression": "@@GETCALENDAR()" 10 | } 11 | ], 12 | "GlobalVariables": [ 13 | { 14 | "Name": "__FirstFiscalMonth", 15 | "Expression": "7", 16 | "IsConfigurable": true 17 | }, 18 | { 19 | "Name": "__FirstDayOfWeek", 20 | "Expression": "0", 21 | "IsConfigurable": true 22 | }, 23 | { 24 | "Name": "__WeekDayCalculationType", 25 | "Expression": "IF ( __FirstDayOfWeek = 0, 7, __FirstDayOfWeek ) + 10" 26 | }, 27 | { 28 | "Name": "__WorkingDays", 29 | "Expression": "@@GETCONFIG( WorkingDays )", 30 | "IsConfigurable": true 31 | } 32 | ], 33 | "RowVariables": [ 34 | { 35 | "Name": "__Date", 36 | "Expression": "[Date]" 37 | }, 38 | { 39 | "Name": "__YearNumber", 40 | "Expression": "YEAR ( __Date )" 41 | }, 42 | { 43 | "Name": "__MonthNumber", 44 | "Expression": "MONTH ( __Date )" 45 | }, 46 | { 47 | "Name": "__QuarterNumber", 48 | "Expression": "QUARTER ( __Date )" 49 | }, 50 | { 51 | "Name": "__MonthNumberQ", 52 | "Expression": "__MonthNumber - 3 * (__QuarterNumber - 1)" 53 | }, 54 | { 55 | "Name": "__YearQuarterNumber", 56 | "Expression": "CONVERT ( __YearNumber * 4 + __QuarterNumber - 1, INTEGER )" 57 | }, 58 | { 59 | "Name": "__WeekDayNumber", 60 | "Expression": "WEEKDAY ( __Date, __WeekDayCalculationType )" 61 | }, 62 | { 63 | "Name": "__WeekDay", 64 | "Expression": "FORMAT ( __Date, __DayFormatString@@GETISO() )" 65 | }, 66 | { 67 | "Name": "__FiscalYearNumber", 68 | "Expression": "__YearNumber + 1 * ( __FirstFiscalMonth > 1 && __MonthNumber >= __FirstFiscalMonth )" 69 | }, 70 | { 71 | "Name": "__FiscalYearQuarterNumber", 72 | "Expression": "CONVERT ( __FiscalYearNumber * 4 + __FiscalQuarterNumber - 1, INTEGER )" 73 | }, 74 | { 75 | "Name": "__FiscalMonthNumber", 76 | "Expression": "__MonthNumber - __FirstFiscalMonth + 1 + 12 * (__MonthNumber < __FirstFiscalMonth)" 77 | }, 78 | { 79 | "Name": "__FiscalQuarterNumber", 80 | "Expression": "ROUNDUP ( __FiscalMonthNumber / 3, 0 )" 81 | }, 82 | { 83 | "Name": "__HolidayName", 84 | "Expression": "@@GETHOLIDAYNAME( __Date )" 85 | }, 86 | { 87 | "Name": "__IsWorkingDay", 88 | "Expression": "WEEKDAY ( __Date, 1 ) IN __WorkingDays && ISBLANK ( __HolidayName )" 89 | }, 90 | { 91 | "Name": "__LastTransactionDate", 92 | "Expression": "@@GETMAXDATE()" 93 | }, 94 | { 95 | "Name": "__IsStandardLocale", 96 | "Expression": "IF ( FORMAT( DATE( 2000, 1, 1 ), \"oooo\"@@GETISO() ) = \"oooo\", TRUE, FALSE )" 97 | }, 98 | { 99 | "Name": "__DayFormatString", 100 | "Expression": "IF( __IsStandardLocale, \"ddd\", \"aaa\" )" 101 | }, 102 | { 103 | "Name": "__MonthFormatString", 104 | "Expression": "IF( __IsStandardLocale, \"mmm\", \"ooo\" )" 105 | } 106 | ], 107 | "Columns": [ 108 | { 109 | "Name": "Date", 110 | "DataType": "DateTime", 111 | "FormatString": null, 112 | "Step": "__Calendar", 113 | "DataCategory": "PaddedDateTableDates", 114 | "AttributeTypes": [ 115 | "Date", 116 | "FiscalDate" 117 | ] 118 | }, 119 | { 120 | "Name": "Year Month", 121 | "Expression": "FORMAT ( __Date, __MonthFormatString & \" yyyy\"@@GETISO() )", 122 | "DataType": "String", 123 | "SortByColumn": "Year Month Number", 124 | "DataCategory": "Months" 125 | }, 126 | { 127 | "Name": "Year Month Number", 128 | "Expression": "__YearNumber * 12 + __MonthNumber - 1", 129 | "DataType": "Int64", 130 | "IsHidden": true, 131 | "AttributeType": "FiscalMonths", 132 | "DataCategory": "Months" 133 | }, 134 | { 135 | "Name": "Fiscal Month", 136 | "Expression": "FORMAT ( __Date, __MonthFormatString@@GETISO() )", 137 | "DataType": "String", 138 | "SortByColumn": "Fiscal Month Number", 139 | "DataCategory": "MonthOfYear" 140 | }, 141 | { 142 | "Name": "Fiscal Month Number", 143 | "Expression": "__FiscalMonthNumber", 144 | "DataType": "Int64", 145 | "IsHidden": true, 146 | "DataCategory": "MonthOfYear" 147 | }, 148 | { 149 | "Name": "Day of Week Number", 150 | "Expression": "__WeekDayNumber", 151 | "DataType": "Int64", 152 | "IsHidden": true, 153 | "AttributeType": "DayOfWeek", 154 | "DataCategory": "DayOfWeek", 155 | "Annotations": { 156 | "SQLBI_FilterSafe": true 157 | } 158 | }, 159 | { 160 | "Name": "Day of Week", 161 | "Expression": "__WeekDay", 162 | "DataType": "String", 163 | "SortByColumn": "Day of Week Number", 164 | "DataCategory": "DayOfWeek", 165 | "Annotations": { 166 | "SQLBI_FilterSafe": true 167 | } 168 | }, 169 | { 170 | "Name": "Fiscal Year", 171 | "Expression": "FORMAT ( __FiscalYearNumber, \"@_FY_@ 0000\"@@GETISO() )", 172 | "DataType": "String", 173 | "SortByColumn": "Fiscal Year Number", 174 | "DataCategory": "Years" 175 | }, 176 | { 177 | "Name": "Fiscal Year Number", 178 | "Expression": "__FiscalYearNumber", 179 | "DataType": "Int64", 180 | "IsHidden": true, 181 | "AttributeType": "FiscalYears", 182 | "DataCategory": "Years" 183 | }, 184 | { 185 | "Name": "Fiscal Year Quarter", 186 | "Expression": "FORMAT ( __FiscalQuarterNumber, \"@_FQ_@0\"@@GETISO() ) & \"-\" & FORMAT ( __FiscalYearNumber, \"0000\"@@GETISO() )", 187 | "DataType": "String", 188 | "SortByColumn": "Fiscal Year Quarter Number", 189 | "DataCategory": "Quarters" 190 | }, 191 | { 192 | "Name": "Fiscal Year Quarter Number", 193 | "Expression": "__FiscalYearQuarterNumber", 194 | "DataType": "Int64", 195 | "IsHidden": true, 196 | "AttributeType": "FiscalQuarters", 197 | "DataCategory": "Quarters" 198 | }, 199 | { 200 | "Name": "Fiscal Quarter", 201 | "Expression": "FORMAT( __FiscalQuarterNumber, \"@_FQ_@0\"@@GETISO() )", 202 | "DataType": "String", 203 | "DataCategory": "QuarterOfYear" 204 | }, 205 | { 206 | "Name": "IsWorking", 207 | "Expression": "__IsWorkingDay", 208 | "DataType": "Boolean", 209 | "IsHidden": true, 210 | "RequiresHolidays": true, 211 | "Annotations": { 212 | "SQLBI_FilterSafe": true 213 | } 214 | }, 215 | { 216 | "Name": "Working Day Value", 217 | "Expression": "IF ( __IsWorkingDay, 1 )", 218 | "DataType": "Int64", 219 | "IsHidden": true, 220 | "RequiresHolidays": true, 221 | "Annotations": { 222 | "SQLBI_FilterSafe": true 223 | } 224 | }, 225 | { 226 | "Name": "Holiday Name", 227 | "Expression": "__HolidayName", 228 | "DataType": "String", 229 | "RequiresHolidays": true, 230 | "Annotations": { 231 | "SQLBI_FilterSafe": true 232 | } 233 | }, 234 | { 235 | "Name": "DateWithTransactions", 236 | "Expression": "__Date <= __LastTransactionDate", 237 | "DataType": "Boolean", 238 | "IsHidden": true, 239 | "AttributeType": "DateDuration" 240 | } 241 | ], 242 | "Hierarchies": [ 243 | { 244 | "Name": "Fiscal", 245 | "Levels": [ 246 | { 247 | "Name": "Year", 248 | "Column": "Fiscal Year" 249 | }, 250 | { 251 | "Name": "Month", 252 | "Column": "Year Month" 253 | }, 254 | { 255 | "Name": "Date", 256 | "Column": "Date" 257 | } 258 | ] 259 | } 260 | ] 261 | } -------------------------------------------------------------------------------- /src/Dax.Template.TestUI/Templates/DateTemplate-04.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/sql-bi/Bravo/main/src/Assets/ManageDates/Schemas/date-template.schema.json", 3 | "_comment": "Reference Monthly Fiscal Calendar", 4 | "CalendarTypes": [ "Fiscal" ], 5 | "FormatPrefixes": [ "FY", "FQ" ], 6 | "Steps": [ 7 | { 8 | "Name": "__Calendar", 9 | "Expression": "@@GETCALENDAR()" 10 | } 11 | ], 12 | "GlobalVariables": [ 13 | { 14 | "Name": "__FirstFiscalMonth", 15 | "Expression": "3", 16 | "IsConfigurable": true 17 | }, 18 | { 19 | "Name": "__MonthsInYear", 20 | "Expression": "12", 21 | "IsConfigurable": true 22 | }, 23 | { 24 | "Name": "__OffsetFiscalMonthNumber", 25 | "Expression": "__MonthsInYear + 1 - (__MonthsInYear - 12)" 26 | }, 27 | { 28 | "Name": "__OffsetYears", 29 | "Expression": "1", 30 | "IsConfigurable": true, 31 | "Comment": "Increase range of fiscal years before/after boundaries defined" 32 | }, 33 | { 34 | "Name": "__TypeStartFiscalYear", 35 | "Expression": "1", 36 | "IsConfigurable": true, 37 | "MultiLineComment": [ 38 | "Fiscal year as Calendar Year of :", 39 | "0 - First day of fiscal year", 40 | "1 - Last day of fiscal year" 41 | ] 42 | } 43 | ], 44 | "RowVariables": [ 45 | { 46 | "Name": "__Date", 47 | "Expression": "[Date]" 48 | }, 49 | { 50 | "Name": "__YearMonthNumber", 51 | "Expression": "YEAR ( __Date ) * __MonthsInYear + MONTH ( __Date ) - 1" 52 | }, 53 | { 54 | "Name": "__FiscalMonthNumber", 55 | "Expression": "MOD ( __YearMonthNumber + 1 * (__FirstFiscalMonth > 1) * (__MonthsInYear + 1 - __FirstFiscalMonth), __MonthsInYear ) + 1" 56 | }, 57 | { 58 | "Name": "__FiscalYearNumber", 59 | "Expression": "QUOTIENT ( __YearMonthNumber + __TypeStartFiscalYear * (__FirstFiscalMonth > 1) * (__MonthsInYear + 1 - __FirstFiscalMonth), __MonthsInYear )" 60 | }, 61 | { 62 | "Name": "__MonthNumber", 63 | "MultiLineExpression": [ 64 | "IF (", 65 | " __FiscalMonthNumber <= 12 && __FirstFiscalMonth > 1,", 66 | " __FiscalMonthNumber + __FirstFiscalMonth", 67 | " - IF (", 68 | " __FiscalMonthNumber > (__OffsetFiscalMonthNumber - __FirstFiscalMonth),", 69 | " __OffsetFiscalMonthNumber,", 70 | " 1", 71 | " ),", 72 | " __FiscalMonthNumber", 73 | ")" 74 | ] 75 | }, 76 | { 77 | "Name": "__YearNumber", 78 | "Expression": "__FiscalYearNumber - 1 * (__MonthNumber > __FiscalMonthNumber)" 79 | }, 80 | { 81 | "Name": "__YearMonthKey", 82 | "Expression": "__YearNumber * 100 + __MonthNumber" 83 | }, 84 | { 85 | "Name": "__MonthDate", 86 | "Expression": "DATE ( __YearNumber, __MonthNumber, 1 )" 87 | }, 88 | { 89 | "Name": "__FiscalQuarterNumber", 90 | "Expression": "MIN ( ROUNDUP ( __FiscalMonthNumber / 3, 0 ), 4 )" 91 | }, 92 | { 93 | "Name": "__FiscalYearQuarterNumber", 94 | "Expression": "__FiscalYearNumber * 4 + __FiscalQuarterNumber - 1" 95 | }, 96 | { 97 | "Name": "__FiscalMonthInQuarterNumber", 98 | "Expression": "MOD ( __FiscalMonthNumber - 1, 3 ) + 1 + 3 * ( __MonthNumber > 12 )" 99 | }, 100 | { 101 | "Name": "__QuarterNumber", 102 | "Expression": "MIN ( ROUNDUP ( __MonthNumber / 3, 0 ), 4 )" 103 | }, 104 | { 105 | "Name": "__YearQuarterNumber", 106 | "Expression": "__YearNumber * 4 + __QuarterNumber - 1" 107 | }, 108 | { 109 | "Name": "__IsStandardLocale", 110 | "Expression": "IF ( FORMAT( DATE( 2000, 1, 1 ), \"oooo\"@@GETISO() ) = \"oooo\", TRUE, FALSE )" 111 | }, 112 | { 113 | "Name": "__MonthFormatString", 114 | "Expression": "IF( __IsStandardLocale, \"mmm\", \"ooo\" )" 115 | } 116 | ], 117 | "Columns": [ 118 | { 119 | "Name": "Date", 120 | "DataType": "DateTime", 121 | "FormatString": null, 122 | "Step": "__Calendar", 123 | "DataCategory": "PaddedDateTableDates", 124 | "AttributeTypes": [ 125 | "Date", 126 | "FiscalDate" 127 | ] 128 | }, 129 | { 130 | "Name": "Year Month Key", 131 | "Expression": "__YearNumber * 100 + __MonthNumber", 132 | "DataType": "Int64", 133 | "IsHidden": true 134 | }, 135 | { 136 | "Name": "Year Month", 137 | "MultiLineExpression": [ 138 | "IF (", 139 | " __MonthNumber > 12,", 140 | " FORMAT ( __MonthNumber, \"@_M_@00\"@@GETISO() ) & FORMAT ( __YearNumber, \" 0000\"@@GETISO() ),", 141 | " FORMAT ( __MonthDate, __MonthFormatString & \" yyyy\"@@GETISO() )", 142 | ")" 143 | ], 144 | "DataType": "String", 145 | "SortByColumn": "Year Month Number", 146 | "DataCategory": "Months" 147 | }, 148 | { 149 | "Name": "Year Month Number", 150 | "Expression": "__YearMonthNumber", 151 | "DataType": "Int64", 152 | "IsHidden": true, 153 | "DataCategory": "Months", 154 | "AttributeTypes": [ 155 | "Months", 156 | "FiscalMonths" 157 | ] 158 | }, 159 | { 160 | "Name": "Fiscal Month", 161 | "MultiLineExpression": [ 162 | "IF (", 163 | " __MonthNumber > 12,", 164 | " FORMAT ( __MonthNumber, \"@_M_@00\"@@GETISO() ),", 165 | " FORMAT ( __MonthDate, __MonthFormatString@@GETISO() )", 166 | ")" 167 | ], 168 | "DataType": "String", 169 | "SortByColumn": "Fiscal Month Number", 170 | "DataCategory": "MonthOfYear" 171 | }, 172 | { 173 | "Name": "Fiscal Month Number", 174 | "Expression": "__FiscalMonthNumber", 175 | "DataType": "Int64", 176 | "IsHidden": true, 177 | "AttributeType": "FiscalMonthOfYear", 178 | "DataCategory": "MonthOfYear" 179 | }, 180 | { 181 | "Name": "Fiscal Month in Quarter Number", 182 | "Expression": "__FiscalMonthInQuarterNumber", 183 | "DataType": "Int64", 184 | "IsHidden": true, 185 | "AttributeType": "FiscalMonthOfQuarter", 186 | "DataCategory": "MonthOfQuarter" 187 | }, 188 | { 189 | "Name": "Fiscal Quarter", 190 | "Expression": "FORMAT( __FiscalQuarterNumber, \"@_FQ_@0\"@@GETISO() )", 191 | "DataType": "String", 192 | "DataCategory": "QuarterOfYear" 193 | }, 194 | { 195 | "Name": "Fiscal Year Quarter", 196 | "Expression": "FORMAT ( __FiscalQuarterNumber, \"@_FQ_@0\"@@GETISO() ) & \"-\" & FORMAT ( __FiscalYearNumber, \"0000\"@@GETISO() )", 197 | "DataType": "String", 198 | "SortByColumn": "Fiscal Year Quarter Number", 199 | "DataCategory": "Quarters" 200 | }, 201 | { 202 | "Name": "Fiscal Year Quarter Number", 203 | "Expression": "__FiscalYearQuarterNumber", 204 | "DataType": "Int64", 205 | "IsHidden": true, 206 | "AttributeType": "FiscalQuarters", 207 | "DataCategory": "Quarters" 208 | }, 209 | { 210 | "Name": "Fiscal Year", 211 | "Expression": "FORMAT ( __FiscalYearNumber, \"@_FY_@ 0000\"@@GETISO() )", 212 | "DataType": "String", 213 | "SortByColumn": "Fiscal Year Number", 214 | "DataCategory": "Years" 215 | }, 216 | { 217 | "Name": "Fiscal Year Number", 218 | "Expression": "__FiscalYearNumber", 219 | "DataType": "Int64", 220 | "IsHidden": true, 221 | "AttributeType": "FiscalYears", 222 | "DataCategory": "Years" 223 | } 224 | ], 225 | "Hierarchies": [ 226 | { 227 | "Name": "Fiscal Year-Quarter", 228 | "Levels": [ 229 | { 230 | "Name": "Year", 231 | "Column": "Fiscal Year" 232 | }, 233 | { 234 | "Name": "Quarter", 235 | "Column": "Fiscal Year Quarter" 236 | }, 237 | { 238 | "Name": "Month", 239 | "Column": "Year Month" 240 | } 241 | ] 242 | }, 243 | { 244 | "Name": "Fiscal Year-Month", 245 | "Levels": [ 246 | { 247 | "Name": "Year", 248 | "Column": "Fiscal Year" 249 | }, 250 | { 251 | "Name": "Month", 252 | "Column": "Year Month" 253 | } 254 | ] 255 | } 256 | ] 257 | } -------------------------------------------------------------------------------- /src/Dax.Template.Tests/_data/Templates/DateTemplate-01.json: -------------------------------------------------------------------------------- 1 | { 2 | "_comment": "Reference FY prefix format in DAX expression using the syntax @_FY_@", 3 | "CalendarType": "Calendar", 4 | "FormatPrefixes": [ "Q", "FY", "FQ" ], 5 | "Steps": [ 6 | { 7 | "Name": "__Calendar", 8 | "Expression": "@@GETCALENDAR()" 9 | } 10 | ], 11 | "GlobalVariables": [ 12 | { 13 | "Name": "__FirstFiscalMonth", 14 | "Expression": "7", 15 | "IsConfigurable": true 16 | }, 17 | { 18 | "Name": "__FirstDayOfWeek", 19 | "Expression": "0", 20 | "IsConfigurable": true 21 | }, 22 | { 23 | "Name": "__WeekDayCalculationType", 24 | "Expression": "IF ( __FirstDayOfWeek = 0, 7, __FirstDayOfWeek ) + 10" 25 | }, 26 | { 27 | "Name": "__WorkingDays", 28 | "Expression": "@@GETCONFIG( WorkingDays )", 29 | "IsConfigurable": true 30 | } 31 | ], 32 | "RowVariables": [ 33 | { 34 | "Name": "__Date", 35 | "Expression": "[Date]" 36 | }, 37 | { 38 | "Name": "__YearNumber", 39 | "Expression": "YEAR ( __Date )" 40 | }, 41 | { 42 | "Name": "__MonthNumber", 43 | "Expression": "MONTH ( __Date )" 44 | }, 45 | { 46 | "Name": "__QuarterNumber", 47 | "Expression": "QUARTER ( __Date )" 48 | }, 49 | { 50 | "Name": "__MonthNumberQ", 51 | "Expression": "__MonthNumber - 3 * (__QuarterNumber - 1)" 52 | }, 53 | { 54 | "Name": "__YearQuarterNumber", 55 | "Expression": "CONVERT ( __YearNumber * 4 + __QuarterNumber - 1, INTEGER )" 56 | }, 57 | { 58 | "Name": "__WeekDayNumber", 59 | "Expression": "WEEKDAY ( __Date, __WeekDayCalculationType )" 60 | }, 61 | { 62 | "Name": "__WeekDay", 63 | "Expression": "FORMAT ( __Date, \"ddd\"@@GETISO() )" 64 | }, 65 | { 66 | "Name": "__FiscalYearNumber", 67 | "Expression": "__YearNumber + 1 * ( __FirstFiscalMonth > 1 && __MonthNumber >= __FirstFiscalMonth )" 68 | }, 69 | { 70 | "Name": "__FiscalYearQuarterNumber", 71 | "Expression": "CONVERT ( __FiscalYearNumber * 4 + __FiscalQuarterNumber - 1, INTEGER )" 72 | }, 73 | { 74 | "Name": "__FiscalMonthNumber", 75 | "Expression": "__MonthNumber - __FirstFiscalMonth + 1 + 12 * (__MonthNumber < __FirstFiscalMonth)" 76 | }, 77 | { 78 | "Name": "__FiscalQuarterNumber", 79 | "Expression": "ROUNDUP ( __FiscalMonthNumber / 3, 0 )" 80 | }, 81 | { 82 | "Name": "__HolidayName", 83 | "Expression": "@@GETHOLIDAYNAME( __Date )" 84 | }, 85 | { 86 | "Name": "__IsWorkingDay", 87 | "Expression": "WEEKDAY ( __Date, 1 ) IN __WorkingDays && ISBLANK ( __HolidayName )" 88 | }, 89 | { 90 | "Name": "__LastTransactionDate", 91 | "Expression": "@@GETMAXDATE()" 92 | } 93 | ], 94 | "Columns": [ 95 | { 96 | "Name": "Date", 97 | "DataType": "DateTime", 98 | "FormatString": null, 99 | "Step": "__Calendar", 100 | "DataCategory": "PaddedDateTableDates", 101 | "AttributeTypes": [ 102 | "Date", 103 | "FiscalDate" 104 | ] 105 | }, 106 | { 107 | "Name": "Year", 108 | "Expression": "__YearNumber", 109 | "DataType": "Int64", 110 | "DataCategory": "Years" 111 | }, 112 | { 113 | "Name": "Year Quarter Number", 114 | "Expression": "__YearQuarterNumber", 115 | "DataType": "Int64", 116 | "IsHidden": true, 117 | "DataCategory": "Quarters" 118 | }, 119 | { 120 | "Name": "Year Quarter", 121 | "Expression": "FORMAT ( __QuarterNumber, \"@_Q_@0\"@@GETISO() ) & \"-\" & FORMAT ( __YearNumber, \"0000\"@@GETISO() )", 122 | "DataType": "String", 123 | "SortByColumn": "Year Quarter Number", 124 | "DataCategory": "Quarters" 125 | }, 126 | { 127 | "Name": "Quarter", 128 | "Expression": "FORMAT( __QuarterNumber, \"@_Q_@0\"@@GETISO() )", 129 | "DataType": "String", 130 | "DataCategory": "QuarterOfYear" 131 | }, 132 | { 133 | "Name": "Year Month", 134 | "Expression": "FORMAT ( __Date, \"mmm yyyy\"@@GETISO() )", 135 | "DataType": "String", 136 | "SortByColumn": "Year Month Number", 137 | "DataCategory": "Months" 138 | }, 139 | { 140 | "Name": "Year Month Number", 141 | "Expression": "__YearNumber * 12 + __MonthNumber - 1", 142 | "DataType": "Int64", 143 | "IsHidden": true, 144 | "AttributeType": "FiscalMonths", 145 | "DataCategory": "Months" 146 | }, 147 | { 148 | "Name": "Month", 149 | "Expression": "FORMAT ( __Date, \"mmm\"@@GETISO() )", 150 | "DataType": "String", 151 | "SortByColumn": "Month Number", 152 | "DataCategory": "MonthOfYear" 153 | }, 154 | { 155 | "Name": "Month Number", 156 | "Expression": "__MonthNumber", 157 | "DataType": "Int64", 158 | "IsHidden": true, 159 | "DataCategory": "MonthOfYear" 160 | }, 161 | { 162 | "Name": "Day of Week Number", 163 | "Expression": "__WeekDayNumber", 164 | "DataType": "Int64", 165 | "IsHidden": true, 166 | "AttributeType": "DayOfWeek", 167 | "DataCategory": "DayOfWeek", 168 | "Annotations": { 169 | "SQLBI_FilterSafe": true 170 | } 171 | }, 172 | { 173 | "Name": "Day of Week", 174 | "Expression": "__WeekDay", 175 | "DataType": "String", 176 | "SortByColumn": "Day of Week Number", 177 | "DataCategory": "DayOfWeek", 178 | "Annotations": { 179 | "SQLBI_FilterSafe": true 180 | } 181 | }, 182 | { 183 | "Name": "Fiscal Year", 184 | "Expression": "FORMAT ( __FiscalYearNumber, \"@_FY_@ 0000\"@@GETISO() )", 185 | "DataType": "String", 186 | "SortByColumn": "Fiscal Year Number", 187 | "DataCategory": "Years" 188 | }, 189 | { 190 | "Name": "Fiscal Year Number", 191 | "Expression": "__FiscalYearNumber", 192 | "DataType": "Int64", 193 | "IsHidden": true, 194 | "AttributeType": "FiscalYears", 195 | "DataCategory": "Years" 196 | }, 197 | { 198 | "Name": "Fiscal Year Quarter", 199 | "Expression": "FORMAT ( __FiscalQuarterNumber, \"@_FQ_@0\"@@GETISO() ) & \"-\" & FORMAT ( __FiscalYearNumber, \"0000\"@@GETISO() )", 200 | "DataType": "String", 201 | "SortByColumn": "Fiscal Year Quarter Number", 202 | "DataCategory": "Quarters" 203 | }, 204 | { 205 | "Name": "Fiscal Year Quarter Number", 206 | "Expression": "__FiscalYearQuarterNumber", 207 | "DataType": "Int64", 208 | "IsHidden": true, 209 | "AttributeType": "FiscalQuarters", 210 | "DataCategory": "Quarters" 211 | }, 212 | { 213 | "Name": "Fiscal Quarter", 214 | "Expression": "FORMAT( __FiscalQuarterNumber, \"@_FQ_@0\"@@GETISO() )", 215 | "DataType": "String", 216 | "DataCategory": "QuarterOfYear" 217 | }, 218 | { 219 | "Name": "IsWorking", 220 | "Expression": "__IsWorkingDay", 221 | "DataType": "Boolean", 222 | "IsHidden": true, 223 | "RequiresHolidays": true, 224 | "Annotations": { 225 | "SQLBI_FilterSafe": true 226 | } 227 | }, 228 | { 229 | "Name": "Working Day Value", 230 | "Expression": "IF ( __IsWorkingDay, 1 )", 231 | "DataType": "Int64", 232 | "IsHidden": true, 233 | "RequiresHolidays": true, 234 | "Annotations": { 235 | "SQLBI_FilterSafe": true 236 | } 237 | }, 238 | { 239 | "Name": "Holiday Name", 240 | "Expression": "__HolidayName", 241 | "DataType": "String", 242 | "RequiresHolidays": true, 243 | "Annotations": { 244 | "SQLBI_FilterSafe": true 245 | } 246 | }, 247 | { 248 | "Name": "DateWithTransactions", 249 | "Expression": "__Date <= __LastTransactionDate", 250 | "DataType": "Boolean", 251 | "IsHidden": true, 252 | "AttributeType": "DateDuration" 253 | } 254 | ], 255 | "Hierarchies": [ 256 | { 257 | "Name": "Calendar", 258 | "Levels": [ 259 | { 260 | "Name": "Year", 261 | "Column": "Year" 262 | }, 263 | { 264 | "Name": "Month", 265 | "Column": "Year Month" 266 | }, 267 | { 268 | "Name": "Date", 269 | "Column": "Date" 270 | } 271 | ] 272 | }, 273 | { 274 | "Name": "Fiscal", 275 | "Levels": [ 276 | { 277 | "Name": "Year", 278 | "Column": "Fiscal Year" 279 | }, 280 | { 281 | "Name": "Month", 282 | "Column": "Year Month" 283 | }, 284 | { 285 | "Name": "Date", 286 | "Column": "Date" 287 | } 288 | ] 289 | } 290 | ] 291 | } -------------------------------------------------------------------------------- /src/Dax.Template/Tables/Dates/HolidaysDefinitionTable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Microsoft.AnalysisServices.Tabular; 4 | using TabularModel = Microsoft.AnalysisServices.Tabular.Model; 5 | using Dax.Template.Syntax; 6 | using Dax.Template.Constants; 7 | using Column = Dax.Template.Model.Column; 8 | using System.Text.Json.Serialization; 9 | using System.Threading; 10 | 11 | namespace Dax.Template.Tables.Dates 12 | { 13 | public class HolidaysDefinitionTable : CalculatedTableTemplateBase 14 | { 15 | public enum SubstituteEnum { 16 | NoSubstituteHoliday = 0, 17 | SubstituteHolidayWithNextWorkingDay = 1, 18 | /// 19 | /// Use 2 before 1 only, e.g. Christmas = 2, Boxing Day = 1 20 | /// 21 | SubstituteHolidayWithNextNextWorkingDay = 2, 22 | /// 23 | /// If it falls on a Saturday then it is observed on Friday, 24 | /// If it falls on a Sunday then it is observed on Monday 25 | /// 26 | FridayIfSaturdayOrMondayIfSunday = -1 27 | } 28 | public class HolidayLine 29 | { 30 | /// 31 | /// ISO country code (filter holidays based on country) 32 | /// 33 | public string? IsoCountry { get; set; } 34 | /// 35 | /// Number of month - use 99 for relative dates using Easter as a reference 36 | /// 37 | public int MonthNumber { get; set; } = default; 38 | /// 39 | /// Absolute day (ignore WeekDayNumber, otherwise use 0) 40 | /// 41 | public int DayNumber { get; set; } = default; 42 | /// 43 | /// 0 = Sunday, 1 = Monday, ... , 6 = Saturday 44 | /// 45 | public int WeekDayNumber { get; set; } = default; 46 | /// 47 | /// 1 = first, 2 = second, ... -1 = last, -2 = second-last, ... 48 | /// 49 | public int OffsetWeek { get; set; } = default; 50 | /// 51 | /// days to add after offsetWeek and WeekDayNumber have been applied 52 | /// 53 | public int OffsetDays { get; set; } = default; 54 | /// 55 | /// Holiday name 56 | /// 57 | public string? HolidayName { get; set; } 58 | /// 59 | /// Define logic to move an holiday to another day in case 60 | /// the date is already a non-working day (e.g. "in lieu of...") 61 | /// 62 | [JsonConverter(typeof(JsonStringEnumConverter))] 63 | public SubstituteEnum SubstituteHoliday { get; set; } = SubstituteEnum.NoSubstituteHoliday; 64 | /// 65 | /// Priority in case of two or more holidays in the same date - lower number --> higher priority 66 | /// For example: marking Easter relative days with 150 and other holidays with 100 means that other holidays take 67 | /// precedence over Easter-related days; use 50 for Easter related holidays to invert such a priority 68 | /// 69 | public int ConflictPriority { get; set; } = default; 70 | /// 71 | /// First year for the holiday, 0 if it is not defined 72 | /// 73 | public int FirstYear { get; set; } = default; 74 | /// 75 | /// Last year for the holiday, 0 if it is not defined 76 | /// 77 | public int LastYear { get; set; } = default; 78 | 79 | internal string GetTableLine() 80 | { 81 | return $"{{ \"{IsoCountry}\", {MonthNumber}, {DayNumber}, {WeekDayNumber}, {OffsetWeek}, {OffsetDays}, \"{HolidayName}\", {(int)SubstituteHoliday}, {ConflictPriority}, {FirstYear}, {LastYear} }}"; 82 | } 83 | } 84 | public class HolidaysDefinitions 85 | { 86 | public HolidayLine[] Holidays { get; set; } = Array.Empty(); 87 | } 88 | 89 | private readonly DaxStep __HolidaysDefinition; 90 | public HolidaysDefinitionTable(HolidaysDefinitions holidaysDefinitions) 91 | { 92 | string padding = new(' ', 8); 93 | Annotations.Add(Attributes.SQLBI_TEMPLATE_ATTRIBUTE, Attributes.SQLBI_TEMPLATE_HOLIDAYS); 94 | Annotations.Add(Attributes.SQLBI_TEMPLATETABLE_ATTRIBUTE, Attributes.SQLBI_TEMPLATETABLE_HOLIDAYSDEFINITION); 95 | __HolidaysDefinition = new() 96 | { 97 | Name = "__HolidayParameters", 98 | Expression = $@" 99 | DATATABLE ( 100 | ""ISO Country"", STRING, -- ISO country code(to enable filter based on country) 101 | ""MonthNumber"", INTEGER, -- Number of month - use 99,98,97,96 for relative dates using an offset over special references: 102 | -- 99 = Easter (DayNumber 1 = Easter Monday, DayNumber -2 = Easter Friday) 103 | -- 98 = Swedish Midsummer Day 104 | -- 97 = September Equinox 105 | -- 96 = March Equinox 106 | ""DayNumber"", INTEGER, -- Absolute day(ignore WeekDayNumber, otherwise use 0) 107 | ""WeekDayNumber"", INTEGER, -- 0 = Sunday, 1 = Monday, ... , 7 = Saturday 108 | ""OffsetWeek"", INTEGER, -- 1 = first, 2 = second, ... -1 = last, -2 = second - last, ... 109 | ""OffsetDays"", INTEGER, -- days to add after offsetWeek and WeekDayNumber have been applied 110 | ""HolidayName"", STRING, -- Holiday name 111 | ""SubstituteHoliday"", INTEGER, -- 0 = no substituteHoliday, 1 = substitute holiday with next working day, 2 = substitute holiday with next working day 112 | -- (use 2 before 1 only, e.g.Christmas = 2, Boxing Day = 1) 113 | -- -1 = if it falls on a Saturday then it is observed on Friday, if it falls on a Sunday then it is observed on Monday 114 | ""ConflictPriority"", INTEGER, -- Priority in case of two or more holidays in the same date - lower number-- > higher priority 115 | -- For example: marking Easter relative days with 150 and other holidays with 100 means that other holidays take 116 | -- precedence over Easter - related days; use 50 for Easter related holidays to invert such a priority 117 | ""FirstYear"", INTEGER, -- First year for the holiday, 0 if it is not defined 118 | ""LastYear"", INTEGER, -- Last year for the holiday, 0 if it is not defined 119 | {{ 120 | {string.Join($",\r\n{padding}",holidaysDefinitions.Holidays.Select(h => h.GetTableLine()))} 121 | }} 122 | )" 123 | }; 124 | 125 | Column[] columns = { 126 | new Column { 127 | Name = "ISO Country", 128 | DataType = DataType.String 129 | }, 130 | new Column { 131 | Name = "MonthNumber", 132 | DataType = DataType.Int64 133 | }, 134 | new Column { 135 | Name = "DayNumber", 136 | DataType = DataType.Int64 137 | }, 138 | new Column { 139 | Name = "WeekDayNumber", 140 | DataType = DataType.Int64 141 | }, 142 | new Column { 143 | Name = "OffsetWeek", 144 | DataType = DataType.Int64 145 | }, 146 | new Column { 147 | Name = "OffsetDays", 148 | DataType = DataType.Int64 149 | }, 150 | new Column { 151 | Name = "HolidayName", 152 | DataType = DataType.String 153 | }, 154 | new Column { 155 | Name = "SubstituteHoliday", 156 | DataType = DataType.Int64 157 | }, 158 | new Column { 159 | Name = "ConflictPriority", 160 | DataType = DataType.Int64 161 | }, 162 | new Column { 163 | Name = "FirstYear", 164 | Expression = "''[FirstYear]", 165 | DataType = DataType.Int64 166 | }, 167 | new Column { 168 | Name = "LastYear", 169 | DataType = DataType.Int64 170 | } 171 | }; 172 | Columns.AddRange(columns); 173 | } 174 | 175 | public override string? GetDaxTableExpression(TabularModel? model, CancellationToken cancellationToken = default) 176 | { 177 | return __HolidaysDefinition.Expression; 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/Dax.Template.TestUI/Templates/DateTemplate-05.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/sql-bi/Bravo/main/src/Assets/ManageDates/Schemas/date-template.schema.json", 3 | "_comment": "Reference Custom Calendar + Holidays", 4 | "CalendarType": "Calendar", 5 | "FormatPrefixes": [ "Q" ], 6 | "Steps": [ 7 | { 8 | "Name": "__Calendar", 9 | "Expression": "@@GETCALENDAR()" 10 | } 11 | ], 12 | "GlobalVariables": [ 13 | { 14 | "Name": "__FirstDayOfWeek", 15 | "Expression": "0", 16 | "IsConfigurable": true, 17 | "Comment": "0 = Sunday, 1 = Monday, ..." 18 | }, 19 | { 20 | "Name": "__WorkingDays", 21 | "Expression": "@@GETCONFIG( WorkingDays )", 22 | "IsConfigurable": true 23 | }, 24 | { 25 | "Name": "__WorkingDayType", 26 | "Expression": "\"Working day\"", 27 | "IsConfigurable": true 28 | }, 29 | { 30 | "Name": "__NonWorkingDayType", 31 | "Expression": "\"Non-working day\"", 32 | "IsConfigurable": true 33 | }, 34 | { 35 | "Name": "__WeekDayCalculationType", 36 | "Expression": "IF ( __FirstDayOfWeek = 0, 7, __FirstDayOfWeek ) + 10" 37 | }, 38 | { 39 | "Name": "__IsStandardLocale", 40 | "Expression": "IF ( FORMAT( DATE( 2000, 1, 1 ), \"oooo\"@@GETISO() ) = \"oooo\", TRUE, FALSE )" 41 | }, 42 | { 43 | "Name": "__DayFormatString", 44 | "Expression": "IF( __IsStandardLocale, \"ddd\", \"aaa\" )" 45 | }, 46 | { 47 | "Name": "__MonthFormatString", 48 | "Expression": "IF( __IsStandardLocale, \"mmm\", \"ooo\" )" 49 | } 50 | ], 51 | "RowVariables": [ 52 | { 53 | "Name": "__Date", 54 | "Expression": "[Date]" 55 | }, 56 | { 57 | "Name": "__YearNumber", 58 | "Expression": "YEAR ( __Date )" 59 | }, 60 | { 61 | "Name": "__MonthNumber", 62 | "Expression": "MONTH ( __Date )" 63 | }, 64 | { 65 | "Name": "__DayOfMonthNumber", 66 | "Expression": "DAY ( __Date )" 67 | }, 68 | { 69 | "Name": "__DateKey", 70 | "Expression": "__YearNumber * 10000 + __MonthNumber * 100 + __DayOfMonthNumber" 71 | }, 72 | { 73 | "Name": "__QuarterNumber", 74 | "Expression": "ROUNDUP ( __MonthNumber / 3, 0 )" 75 | }, 76 | { 77 | "Name": "__YearQuarterNumber", 78 | "Expression": "CONVERT ( __YearNumber * 4 + __QuarterNumber - 1, INTEGER )" 79 | }, 80 | { 81 | "Name": "__MonthInQuarterNumber", 82 | "Expression": "MOD ( __MonthNumber - 1, 3 ) + 1" 83 | }, 84 | { 85 | "Name": "__FirstDayOfYear", 86 | "Expression": "DATE ( __YearNumber, 1, 1 )" 87 | }, 88 | { 89 | "Name": "__YearDayNumber", 90 | "MultiLineExpression": [ 91 | "SUMX (", 92 | " CALENDAR ( __FirstDayOfYear, __Date ),", 93 | " 1 * ( MONTH ( ''[Date] ) <> 2 || DAY ( ''[Date] ) <> 29 )", 94 | ")" 95 | ] 96 | }, 97 | { 98 | "Name": "__WeekDayNumber", 99 | "Expression": "WEEKDAY ( __Date, __WeekDayCalculationType )" 100 | }, 101 | { 102 | "Name": "__WeekDay", 103 | "Expression": "FORMAT ( __Date, __DayFormatString@@GETISO() )" 104 | }, 105 | { 106 | "Name": "__HolidayName", 107 | "Expression": "@@GETHOLIDAYNAME( __Date )" 108 | }, 109 | { 110 | "Name": "__IsWorkingDay", 111 | "Expression": "WEEKDAY ( __Date, 1 ) IN __WorkingDays && ISBLANK ( __HolidayName )" 112 | }, 113 | { 114 | "Name": "__LastTransactionDate", 115 | "Expression": "@@GETMAXDATE()" 116 | } 117 | ], 118 | "Columns": [ 119 | { 120 | "Name": "Date", 121 | "DataType": "DateTime", 122 | "FormatString": null, 123 | "Step": "__Calendar", 124 | "DataCategory": "PaddedDateTableDates", 125 | "AttributeTypes": [ 126 | "Date" 127 | ] 128 | }, 129 | { 130 | "Name": "DateKey", 131 | "Expression": "__DateKey", 132 | "DataType": "Int64", 133 | "IsHidden": true 134 | }, 135 | { 136 | "Name": "Sequential Day Number", 137 | "Expression": "INT ( __Date )", 138 | "DataType": "Int64", 139 | "IsHidden": true, 140 | "AttributeType": "ManufacturingDate", 141 | "_comment": "We use ManufacturingDate as a special tag to identify an alternate date to avoid removing the filter on all the columns because of mark as date table" 142 | }, 143 | { 144 | "Name": "Year Month", 145 | "Expression": "FORMAT ( __Date, __MonthFormatString & \" yyyy\"@@GETISO() )", 146 | "DataType": "String", 147 | "SortByColumn": "Year Month Number", 148 | "DataCategory": "Months" 149 | }, 150 | { 151 | "Name": "Year Month Number", 152 | "Expression": "__YearNumber * 12 + __MonthNumber - 1", 153 | "DataType": "Int64", 154 | "IsHidden": true, 155 | "AttributeType": "Months", 156 | "DataCategory": "Months" 157 | }, 158 | { 159 | "Name": "Year", 160 | "Expression": "__YearNumber", 161 | "DataType": "Int64", 162 | "AttributeType": "Years", 163 | "DataCategory": "Years" 164 | }, 165 | { 166 | "Name": "Year Quarter", 167 | "Expression": "FORMAT ( __QuarterNumber, \"@_Q_@0\"@@GETISO() ) & \"-\" & FORMAT ( __YearNumber, \"0000\"@@GETISO() )", 168 | "DataType": "String", 169 | "SortByColumn": "Year Quarter Number", 170 | "DataCategory": "Quarters" 171 | }, 172 | { 173 | "Name": "Year Quarter Number", 174 | "Expression": "__YearQuarterNumber", 175 | "DataType": "Int64", 176 | "IsHidden": true, 177 | "AttributeType": "Quarters", 178 | "DataCategory": "Quarters" 179 | }, 180 | { 181 | "Name": "Quarter", 182 | "Expression": "FORMAT( __QuarterNumber, \"@_Q_@0\"@@GETISO() )", 183 | "DataType": "String", 184 | "DataCategory": "QuarterOfYear" 185 | }, 186 | { 187 | "Name": "Month", 188 | "Expression": "FORMAT ( __Date, __MonthFormatString@@GETISO() )", 189 | "DataType": "String", 190 | "SortByColumn": "Month Number", 191 | "DataCategory": "MonthOfYear" 192 | }, 193 | { 194 | "Name": "Month Number", 195 | "Expression": "__MonthNumber", 196 | "DataType": "Int64", 197 | "IsHidden": true, 198 | "AttributeType": "MonthOfYear", 199 | "DataCategory": "MonthOfYear" 200 | }, 201 | { 202 | "Name": "Month in Quarter Number", 203 | "Expression": "__MonthInQuarterNumber", 204 | "DataType": "Int64", 205 | "IsHidden": true, 206 | "AttributeType": "MonthOfQuarter", 207 | "DataCategory": "MonthOfQuarter" 208 | }, 209 | { 210 | "Name": "Day of Week", 211 | "Expression": "__WeekDay", 212 | "DataType": "String", 213 | "SortByColumn": "Day of Week Number", 214 | "AttributeType": "DayOfWeek", 215 | "DataCategory": "DayOfWeek", 216 | "Annotations": { 217 | "SQLBI_FilterSafe": true 218 | } 219 | }, 220 | { 221 | "Name": "Day of Week Number", 222 | "Expression": "__WeekDayNumber", 223 | "DataType": "Int64", 224 | "IsHidden": true, 225 | "AttributeType": "DayOfWeek", 226 | "DataCategory": "DayOfWeek", 227 | "Annotations": { 228 | "SQLBI_FilterSafe": true 229 | } 230 | }, 231 | { 232 | "Name": "Day of Month Number", 233 | "Expression": "__DayOfMonthNumber", 234 | "DataType": "Int64", 235 | "IsHidden": true, 236 | "AttributeType": "DayOfMonth", 237 | "DataCategory": "DayOfMonth" 238 | }, 239 | { 240 | "Name": "Day of Year Number", 241 | "Expression": "__YearDayNumber", 242 | "DataType": "Int64", 243 | "IsHidden": true, 244 | "AttributeType": "DayOfYear", 245 | "DataCategory": "DayOfYear" 246 | }, 247 | { 248 | "Name": "Working Day", 249 | "Expression": "IF ( __IsWorkingDay, __WorkingDayType, __NonWorkingDayType )", 250 | "DataType": "String", 251 | "RequiresHolidays": true, 252 | "Annotations": { 253 | "SQLBI_FilterSafe": true 254 | } 255 | }, 256 | { 257 | "Name": "IsWorking", 258 | "Expression": "__IsWorkingDay", 259 | "DataType": "Boolean", 260 | "RequiresHolidays": true, 261 | "IsHidden": true, 262 | "Annotations": { 263 | "SQLBI_FilterSafe": true 264 | } 265 | }, 266 | { 267 | "Name": "Working Day Value", 268 | "Expression": "IF ( __IsWorkingDay, 1 )", 269 | "DataType": "Int64", 270 | "RequiresHolidays": true, 271 | "IsHidden": true, 272 | "Annotations": { 273 | "SQLBI_FilterSafe": true 274 | } 275 | }, 276 | { 277 | "Name": "Holiday Name", 278 | "Expression": "__HolidayName", 279 | "DataType": "String", 280 | "RequiresHolidays": true, 281 | "Annotations": { 282 | "SQLBI_FilterSafe": true 283 | } 284 | }, 285 | { 286 | "Name": "DateWithTransactions", 287 | "Expression": "__Date <= __LastTransactionDate", 288 | "DataType": "Boolean", 289 | "IsHidden": true, 290 | "AttributeType": "DateDuration" 291 | } 292 | ], 293 | "Hierarchies": [ 294 | { 295 | "Name": "Calendar", 296 | "Levels": [ 297 | { 298 | "Name": "Year", 299 | "Column": "Year" 300 | }, 301 | { 302 | "Name": "Quarter", 303 | "Column": "Year Quarter" 304 | }, 305 | { 306 | "Name": "Month", 307 | "Column": "Year Month" 308 | } 309 | ] 310 | } 311 | ] 312 | } -------------------------------------------------------------------------------- /src/Dax.Template/Tables/Dates/SimpleDateTable.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Collections.Generic; 3 | using Microsoft.AnalysisServices.Tabular; 4 | using Dax.Template.Syntax; 5 | using Column = Dax.Template.Model.Column; 6 | using Hierarchy = Dax.Template.Model.Hierarchy; 7 | using Level = Dax.Template.Model.Level; 8 | using TabularModel = Microsoft.AnalysisServices.Tabular.Model; 9 | using Dax.Template.Extensions; 10 | using Dax.Template.Constants; 11 | 12 | namespace Dax.Template.Tables.Dates 13 | { 14 | public class SimpleDateTemplateConfig : TemplateConfiguration 15 | { 16 | public string QuarterPrefix { get; set; } = @"Q"; 17 | public string FiscalYearPrefix { get; set; } = @"FY"; 18 | public string FiscalQuarterPrefix { get; set; } = @"FQ"; 19 | } 20 | public class SimpleDateTable : BaseDateTemplate 21 | { 22 | readonly DaxStep __RenameCalendar; 23 | 24 | //// TODO: this could be localized (as other column names) 25 | const string DATE_COLUMN_NAME = "Date"; 26 | 27 | public SimpleDateTable(SimpleDateTemplateConfig config, TabularModel? model ) : base( config ) 28 | { 29 | Annotations.Add(Attributes.SQLBI_TEMPLATE_ATTRIBUTE, Attributes.SQLBI_TEMPLATE_DATES); 30 | Annotations.Add(Attributes.SQLBI_TEMPLATETABLE_ATTRIBUTE, Attributes.SQLBI_TEMPLATETABLE_DATE); 31 | 32 | string quarterFormatPrefix = string.Concat(from c in Config.QuarterPrefix select @"\" + c); 33 | string fiscalYearFormatPrefix = string.Concat(from c in Config.FiscalYearPrefix select @"\" + c); 34 | string fiscalQuarterFormatPrefix = string.Concat(from c in Config.FiscalQuarterPrefix select @"\" + c); 35 | 36 | DaxStep __Calendar = new() { 37 | Name = "__Calendar", 38 | Expression = GenerateCalendarExpression(model), 39 | IgnoreAutoDependency = true, 40 | }; 41 | 42 | // TODO: Restore [Date] without empty table identifier 43 | __RenameCalendar = new DaxStep 44 | { 45 | Name = "__RenameCalendar", 46 | Expression = $@" 47 | SELECTCOLUMNS ( 48 | __Calendar, 49 | ""{DATE_COLUMN_NAME}"", ''[Date] 50 | )", 51 | IgnoreAutoDependency = true, 52 | Dependencies = new IDependencies[] { __Calendar } 53 | }; 54 | DaxStep[] steps = new DaxStep[] { __Calendar, __RenameCalendar }; 55 | 56 | Model.DateColumn Date = new() 57 | { 58 | Name = DATE_COLUMN_NAME, 59 | Expression = string.Empty, // Does not generate the column, use only the metadata 60 | DataType = DataType.DateTime, 61 | FormatString = "m/dd/yyyy", 62 | IgnoreAutoDependency = true, 63 | Dependencies = new IDependencies[] { __RenameCalendar } 64 | }; 65 | 66 | // TODO consider possible rename/localization in base table expression 67 | Var __Date = new VarRow { 68 | Name = "__Date", 69 | Expression = $"[{DATE_COLUMN_NAME}]", 70 | IgnoreAutoDependency = true, 71 | Dependencies = new IDependencies[] { Date } 72 | }; 73 | 74 | Var[] variables = { 75 | __Date, 76 | new VarGlobal { Name = "__FirstFiscalMonth", Expression = "7" }, 77 | new VarGlobal { Name = "__FirstDayOfWeek", Expression = "0" }, 78 | new VarRow { Name = "__Yr", Expression = "YEAR ( __Date )" }, 79 | new VarRow { Name = "__Mn", Expression = "MONTH ( __Date )" }, 80 | new VarRow { Name = "__Qr", Expression = "QUARTER ( __Date )" }, 81 | new VarRow { Name = "__MnQ", Expression = "__Mn - 3 * (__Qr - 1)" }, 82 | new VarRow { Name = "__Wd", Expression = "WEEKDAY ( __Date, 1 ) - 1" }, 83 | new VarRow { Name = "__Fyr", Expression = "__Yr + 1 * ( __FirstFiscalMonth > 1 && __Mn >= __FirstFiscalMonth )" }, 84 | new VarRow { Name = "__Fqr", Expression = $"FORMAT ( EOMONTH ( __Date, 1 - __FirstFiscalMonth ), \"{fiscalQuarterFormatPrefix}Q\" )" }, 85 | }; 86 | 87 | Column[] columns = { 88 | Date, 89 | new Column { 90 | Name = "Year", 91 | Expression = "DATE(__Yr, 12, 31)", 92 | DataType = DataType.DateTime, 93 | FormatString = "yyyy", 94 | }, 95 | new Column { 96 | Name = "Year Quarter Date", 97 | Expression = "EOMONTH( __Date, 3 - __MnQ )", 98 | DataType = DataType.DateTime, 99 | FormatString = "m/dd/yyyy", 100 | IsHidden = true, 101 | }, 102 | new Column { 103 | Name = "Year Quarter", 104 | Expression = $"FORMAT( __Date, \"{quarterFormatPrefix}Q-YYYY\")", // TODO Add FORMAT argument for localization 105 | DataType = DataType.String, 106 | }, 107 | new Column { 108 | Name = "Quarter", 109 | Expression = $"FORMAT( __Date, \"{quarterFormatPrefix}Q\" )", 110 | DataType = DataType.String, 111 | }, 112 | new Column { 113 | // Use this version for end-of-month 114 | // Name = "Year Month", Expression = @"EOMONTH( _Date, 0 )", DataType = DataType.DateTime, Dependencies = new IDependencies[] { _Date } }; 115 | // Use this version for beginning-of-month 116 | Name = "Year Month", 117 | Expression = @"EOMONTH( __Date, -1 ) + 1", 118 | DataType = DataType.DateTime, 119 | FormatString = "mmm yyyy", 120 | }, 121 | new Column { 122 | Name = "Month", 123 | Expression = "DATE(1900, MONTH( __Date ), 1 )", 124 | DataType = DataType.DateTime, 125 | FormatString = "mmm", 126 | }, 127 | new Column { 128 | Name = "Day of Week", 129 | Expression = "DATE(1900, 1, 7 + __Wd + (7 * (__Wd < __FirstDayOfWeek)))", 130 | DataType = DataType.DateTime, 131 | FormatString = "ddd", 132 | }, 133 | new Column { 134 | Name = "Fiscal Year", 135 | Expression = "DATE(__Fyr + (__FirstFiscalMonth = 1), __FirstFiscalMonth, 1) - 1", 136 | DataType = DataType.DateTime, 137 | FormatString = $"{fiscalYearFormatPrefix} yyyy", 138 | DisplayFolder = "Fiscal" 139 | }, 140 | new Column { 141 | Name = "Fiscal Year Quarter", 142 | Expression = @"__Fqr & ""-"" & __Fyr", 143 | DataType = DataType.String, 144 | FormatString = $"{fiscalQuarterFormatPrefix} yyyy", 145 | DisplayFolder = "Fiscal" 146 | }, 147 | new Column { 148 | Name = "Fiscal Year Quarter Date", 149 | Expression = "EOMONTH( __Date, 3 - __MnQ )", 150 | DataType = DataType.DateTime, 151 | FormatString = "m/dd/yyyy", 152 | IsHidden = true, 153 | DisplayFolder = "Fiscal" 154 | }, 155 | new Column { 156 | Name = "Fiscal Quarter", 157 | Expression = @"__Fqr", 158 | DataType = DataType.String, 159 | DisplayFolder = "Fiscal" 160 | }, 161 | new Column { 162 | Name = "TestColumnDependency", 163 | Expression = @"[Fiscal Quarter]", 164 | DataType = DataType.String, 165 | DisplayFolder = "Fiscal" 166 | } 167 | }; 168 | columns.First(c => c.Name == "Year Quarter").SortByColumn = columns.First(c => c.Name == "Year Quarter Date"); 169 | columns.First(c => c.Name == "Fiscal Year Quarter").SortByColumn = columns.First(c => c.Name == "Fiscal Year Quarter Date"); 170 | Columns.AddRange(columns); 171 | 172 | Hierarchy calendarHierarchy = new() 173 | { 174 | Name = "Calendar", 175 | Levels = new Level[] { 176 | new Level { Name = "Year", Column = columns.First(c => c.Name == "Year") }, 177 | new Level { Name = "Month", Column = columns.First(c => c.Name == "Year Month") }, 178 | new Level { Name = "Date", Column = Date }, 179 | } 180 | }; 181 | Hierarchy fiscalHierarchy = new() 182 | { 183 | Name = "Fiscal", 184 | Levels = new Level[] { 185 | new Level { Name = "Year", Column = columns.First(c => c.Name == "Fiscal Year") }, 186 | new Level { Name = "Month", Column = columns.First(c => c.Name == "Year Month") }, 187 | new Level { Name = "Date", Column = Date }, 188 | }, 189 | DisplayFolder = "Fiscal" 190 | }; 191 | 192 | Hierarchy[] hierarchies = { calendarHierarchy, fiscalHierarchy }; 193 | Hierarchies.AddRange(hierarchies); 194 | 195 | // Set dependencies for all the items 196 | IEnumerable? stepsVariablesColumns = ((IDaxName[])variables).Union(steps).Union(columns); 197 | stepsVariablesColumns.AddDependenciesFromExpression(); 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/Dax.Template.TestUI/Templates/DateTemplate-06.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/sql-bi/Bravo/main/src/Assets/ManageDates/Schemas/date-template.schema.json", 3 | "_comment": "Reference Custom Fiscal Calendar + Holidays", 4 | "CalendarType": "Fiscal", 5 | "FormatPrefixes": [ "FY", "FQ" ], 6 | "Steps": [ 7 | { 8 | "Name": "__Calendar", 9 | "Expression": "@@GETCALENDAR()" 10 | } 11 | ], 12 | "GlobalVariables": [ 13 | { 14 | "Name": "__FirstFiscalMonth", 15 | "Expression": "3", 16 | "IsConfigurable": true 17 | }, 18 | { 19 | "Name": "__FirstDayOfWeek", 20 | "Expression": "0", 21 | "IsConfigurable": true, 22 | "Comment": "0 = Sunday, 1 = Monday, ..." 23 | }, 24 | { 25 | "Name": "__TypeStartFiscalYear", 26 | "Expression": "1", 27 | "IsConfigurable": true, 28 | "MultiLineComment": [ 29 | "Fiscal year as Calendar Year of :", 30 | "0 - First day of fiscal year", 31 | "1 - Last day of fiscal year" 32 | ] 33 | }, 34 | { 35 | "Name": "__WorkingDays", 36 | "Expression": "@@GETCONFIG( WorkingDays )", 37 | "IsConfigurable": true 38 | }, 39 | { 40 | "Name": "__WorkingDayType", 41 | "Expression": "\"Working day\"", 42 | "IsConfigurable": true 43 | }, 44 | { 45 | "Name": "__NonWorkingDayType", 46 | "Expression": "\"Non-working day\"", 47 | "IsConfigurable": true 48 | }, 49 | { 50 | "Name": "__WeekDayCalculationType", 51 | "Expression": "IF ( __FirstDayOfWeek = 0, 7, __FirstDayOfWeek ) + 10" 52 | }, 53 | { 54 | "Name": "__OffsetFiscalYearBeforeMonth", 55 | "Expression": "IF ( __FirstFiscalMonth > 1, __TypeStartFiscalYear - 1, 0 )" 56 | }, 57 | { 58 | "Name": "__OffsetFiscalYearOnOrAfterMonth", 59 | "Expression": "IF ( __FirstFiscalMonth > 1, __TypeStartFiscalYear, 0 )" 60 | }, 61 | { 62 | "Name": "__IsStandardLocale", 63 | "Expression": "IF ( FORMAT( DATE( 2000, 1, 1 ), \"oooo\"@@GETISO() ) = \"oooo\", TRUE, FALSE )" 64 | }, 65 | { 66 | "Name": "__DayFormatString", 67 | "Expression": "IF( __IsStandardLocale, \"ddd\", \"aaa\" )" 68 | }, 69 | { 70 | "Name": "__MonthFormatString", 71 | "Expression": "IF( __IsStandardLocale, \"mmm\", \"ooo\" )" 72 | } 73 | ], 74 | "RowVariables": [ 75 | { 76 | "Name": "__Date", 77 | "Expression": "[Date]" 78 | }, 79 | { 80 | "Name": "__YearNumber", 81 | "Expression": "YEAR ( __Date )" 82 | }, 83 | { 84 | "Name": "__MonthNumber", 85 | "Expression": "MONTH ( __Date )" 86 | }, 87 | { 88 | "Name": "__DayOfMonthNumber", 89 | "Expression": "DAY ( __Date )" 90 | }, 91 | { 92 | "Name": "__DateKey", 93 | "Expression": "__YearNumber * 10000 + __MonthNumber * 100 + __DayOfMonthNumber" 94 | }, 95 | { 96 | "Name": "__FiscalYearNumber", 97 | "Expression": "__YearNumber + __OffsetFiscalYearOnOrAfterMonth * ( __FirstFiscalMonth > 1 && __MonthNumber >= __FirstFiscalMonth ) + __OffsetFiscalYearBeforeMonth * ( __FirstFiscalMonth > 1 && __MonthNumber < __FirstFiscalMonth )" 98 | }, 99 | { 100 | "Name": "__FiscalMonthNumber", 101 | "Expression": "__MonthNumber - __FirstFiscalMonth + 1 + 12 * (__MonthNumber < __FirstFiscalMonth)" 102 | }, 103 | { 104 | "Name": "__FiscalQuarterNumber", 105 | "Expression": "ROUNDUP ( __FiscalMonthNumber / 3, 0 )" 106 | }, 107 | { 108 | "Name": "__FiscalYearQuarterNumber", 109 | "Expression": "CONVERT ( __FiscalYearNumber * 4 + __FiscalQuarterNumber - 1, INTEGER )" 110 | }, 111 | { 112 | "Name": "__FiscalMonthInQuarterNumber", 113 | "Expression": "MOD ( __FiscalMonthNumber - 1, 3 ) + 1" 114 | }, 115 | { 116 | "Name": "__FirstDayOfYear", 117 | "Expression": "DATE ( __FiscalYearNumber - 1 * (__FirstFiscalMonth > 1), __FirstFiscalMonth, 1 )" 118 | }, 119 | { 120 | "Name": "__FiscalYearDayNumber", 121 | "MultiLineExpression": [ 122 | "SUMX (", 123 | " CALENDAR ( __FirstDayOfYear, __Date ),", 124 | " 1 * ( MONTH ( ''[Date] ) <> 2 || DAY ( ''[Date] ) <> 29 )", 125 | ")" 126 | ] 127 | }, 128 | { 129 | "Name": "__WeekDayNumber", 130 | "Expression": "WEEKDAY ( __Date, __WeekDayCalculationType )" 131 | }, 132 | { 133 | "Name": "__WeekDay", 134 | "Expression": "FORMAT ( __Date, __DayFormatString@@GETISO() )" 135 | }, 136 | { 137 | "Name": "__HolidayName", 138 | "Expression": "@@GETHOLIDAYNAME( __Date )" 139 | }, 140 | { 141 | "Name": "__IsWorkingDay", 142 | "Expression": "WEEKDAY ( __Date, 1 ) IN __WorkingDays && ISBLANK ( __HolidayName )" 143 | }, 144 | { 145 | "Name": "__LastTransactionDate", 146 | "Expression": "@@GETMAXDATE()" 147 | } 148 | ], 149 | "Columns": [ 150 | { 151 | "Name": "Date", 152 | "DataType": "DateTime", 153 | "FormatString": null, 154 | "Step": "__Calendar", 155 | "DataCategory": "PaddedDateTableDates", 156 | "AttributeTypes": [ 157 | "Date", 158 | "FiscalDate" 159 | ] 160 | }, 161 | { 162 | "Name": "DateKey", 163 | "Expression": "__DateKey", 164 | "DataType": "Int64", 165 | "IsHidden": true 166 | }, 167 | { 168 | "Name": "Sequential Day Number", 169 | "Expression": "INT ( __Date )", 170 | "DataType": "Int64", 171 | "IsHidden": true, 172 | "AttributeType": "ManufacturingDate", 173 | "_comment": "We use ManufacturingDate as a special tag to identify an alternate date to avoid removing the filter on all the columns because of mark as date table" 174 | }, 175 | { 176 | "Name": "Year Month", 177 | "Expression": "FORMAT ( __Date, __MonthFormatString & \" yyyy\"@@GETISO() )", 178 | "DataType": "String", 179 | "SortByColumn": "Year Month Number", 180 | "DataCategory": "Months" 181 | }, 182 | { 183 | "Name": "Year Month Number", 184 | "Expression": "__YearNumber * 12 + __MonthNumber - 1", 185 | "DataType": "Int64", 186 | "IsHidden": true, 187 | "AttributeType": "FiscalMonths", 188 | "DataCategory": "Months" 189 | }, 190 | { 191 | "Name": "Fiscal Year", 192 | "Expression": "FORMAT ( __FiscalYearNumber, \"@_FY_@ 0000\"@@GETISO() )", 193 | "DataType": "String", 194 | "SortByColumn": "Fiscal Year Number", 195 | "DataCategory": "Years" 196 | }, 197 | { 198 | "Name": "Fiscal Year Number", 199 | "Expression": "__FiscalYearNumber", 200 | "DataType": "Int64", 201 | "IsHidden": true, 202 | "AttributeType": "FiscalYears", 203 | "DataCategory": "Years" 204 | }, 205 | { 206 | "Name": "Fiscal Year Quarter", 207 | "Expression": "FORMAT ( __FiscalQuarterNumber, \"@_FQ_@0\"@@GETISO() ) & \"-\" & FORMAT ( __FiscalYearNumber, \"0000\"@@GETISO() )", 208 | "DataType": "String", 209 | "SortByColumn": "Fiscal Year Quarter Number", 210 | "DataCategory": "Quarters" 211 | }, 212 | { 213 | "Name": "Fiscal Year Quarter Number", 214 | "Expression": "__FiscalYearQuarterNumber", 215 | "DataType": "Int64", 216 | "IsHidden": true, 217 | "AttributeType": "FiscalQuarters", 218 | "DataCategory": "Quarters" 219 | }, 220 | { 221 | "Name": "Fiscal Quarter", 222 | "Expression": "FORMAT( __FiscalQuarterNumber, \"@_FQ_@0\"@@GETISO() )", 223 | "DataType": "String", 224 | "DataCategory": "QuarterOfYear" 225 | }, 226 | { 227 | "Name": "Month", 228 | "Expression": "FORMAT ( __Date, __MonthFormatString@@GETISO() )", 229 | "DataType": "String", 230 | "SortByColumn": "Fiscal Month Number", 231 | "DataCategory": "MonthOfYear" 232 | }, 233 | { 234 | "Name": "Fiscal Month Number", 235 | "Expression": "__FiscalMonthNumber", 236 | "DataType": "Int64", 237 | "IsHidden": true, 238 | "AttributeType": "FiscalMonthOfYear", 239 | "DataCategory": "MonthOfYear" 240 | }, 241 | { 242 | "Name": "Fiscal Month in Quarter Number", 243 | "Expression": "__FiscalMonthInQuarterNumber", 244 | "DataType": "Int64", 245 | "IsHidden": true, 246 | "AttributeType": "FiscalMonthOfQuarter", 247 | "DataCategory": "MonthOfQuarter" 248 | }, 249 | { 250 | "Name": "Day of Week", 251 | "Expression": "__WeekDay", 252 | "DataType": "String", 253 | "SortByColumn": "Day of Week Number", 254 | "AttributeType": "FiscalDayOfWeek", 255 | "DataCategory": "DayOfWeek", 256 | "Annotations": { 257 | "SQLBI_FilterSafe": true 258 | } 259 | }, 260 | { 261 | "Name": "Day of Week Number", 262 | "Expression": "__WeekDayNumber", 263 | "DataType": "Int64", 264 | "IsHidden": true, 265 | "AttributeType": "FiscalDayOfWeek", 266 | "DataCategory": "DayOfWeek", 267 | "Annotations": { 268 | "SQLBI_FilterSafe": true 269 | } 270 | }, 271 | { 272 | "Name": "Day of Month Number", 273 | "Expression": "__DayOfMonthNumber", 274 | "DataType": "Int64", 275 | "IsHidden": true, 276 | "AttributeType": "FiscalDayOfMonth", 277 | "DataCategory": "DayOfMonth" 278 | }, 279 | { 280 | "Name": "Day of Fiscal Year Number", 281 | "Expression": "__FiscalYearDayNumber", 282 | "DataType": "Int64", 283 | "IsHidden": true, 284 | "AttributeType": "FiscalDayOfYear", 285 | "DataCategory": "DayOfYear" 286 | }, 287 | { 288 | "Name": "Working Day", 289 | "Expression": "IF ( __IsWorkingDay, __WorkingDayType, __NonWorkingDayType )", 290 | "DataType": "String", 291 | "RequiresHolidays": true, 292 | "Annotations": { 293 | "SQLBI_FilterSafe": true 294 | } 295 | }, 296 | { 297 | "Name": "IsWorking", 298 | "Expression": "__IsWorkingDay", 299 | "DataType": "Boolean", 300 | "RequiresHolidays": true, 301 | "IsHidden": true, 302 | "Annotations": { 303 | "SQLBI_FilterSafe": true 304 | } 305 | }, 306 | { 307 | "Name": "Working Day Value", 308 | "Expression": "IF ( __IsWorkingDay, 1 )", 309 | "DataType": "Int64", 310 | "RequiresHolidays": true, 311 | "IsHidden": true, 312 | "Annotations": { 313 | "SQLBI_FilterSafe": true 314 | } 315 | }, 316 | { 317 | "Name": "Holiday Name", 318 | "Expression": "__HolidayName", 319 | "DataType": "String", 320 | "RequiresHolidays": true, 321 | "Annotations": { 322 | "SQLBI_FilterSafe": true 323 | } 324 | }, 325 | { 326 | "Name": "DateWithTransactions", 327 | "Expression": "__Date <= __LastTransactionDate", 328 | "DataType": "Boolean", 329 | "IsHidden": true, 330 | "AttributeType": "DateDuration" 331 | } 332 | ], 333 | "Hierarchies": [ 334 | { 335 | "Name": "Fiscal", 336 | "Levels": [ 337 | { 338 | "Name": "Year", 339 | "Column": "Fiscal Year" 340 | }, 341 | { 342 | "Name": "Quarter", 343 | "Column": "Fiscal Year Quarter" 344 | }, 345 | { 346 | "Name": "Month", 347 | "Column": "Year Month" 348 | } 349 | ] 350 | } 351 | ] 352 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Remove the line below if you want to inherit .editorconfig settings from higher directories 2 | root = true 3 | 4 | # C# files 5 | [*.cs] 6 | 7 | #### Core EditorConfig Options #### 8 | 9 | # Indentation and spacing 10 | indent_size = 4 11 | indent_style = space 12 | tab_width = 4 13 | 14 | # New line preferences 15 | end_of_line = crlf 16 | insert_final_newline = false 17 | 18 | #### .NET Coding Conventions #### 19 | 20 | # Organize usings 21 | dotnet_separate_import_directive_groups = false 22 | dotnet_sort_system_directives_first = false 23 | file_header_template = unset 24 | 25 | # this. and Me. preferences 26 | dotnet_style_qualification_for_event = false:warning 27 | dotnet_style_qualification_for_field = false 28 | dotnet_style_qualification_for_method = false:warning 29 | dotnet_style_qualification_for_property = false:warning 30 | 31 | # Language keywords vs BCL types preferences 32 | dotnet_style_predefined_type_for_locals_parameters_members = true:warning 33 | dotnet_style_predefined_type_for_member_access = true:warning 34 | 35 | # Parentheses preferences 36 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion 37 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion 38 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary 39 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion 40 | 41 | # Modifier preferences 42 | dotnet_style_require_accessibility_modifiers = for_non_interface_members 43 | 44 | # Expression-level preferences 45 | dotnet_style_coalesce_expression = true 46 | dotnet_style_collection_initializer = true 47 | dotnet_style_explicit_tuple_names = true 48 | dotnet_style_namespace_match_folder = true 49 | dotnet_style_null_propagation = true 50 | dotnet_style_object_initializer = true 51 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 52 | dotnet_style_prefer_auto_properties = true 53 | dotnet_style_prefer_compound_assignment = true 54 | dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion 55 | dotnet_style_prefer_conditional_expression_over_return = true:suggestion 56 | dotnet_style_prefer_inferred_anonymous_type_member_names = true 57 | dotnet_style_prefer_inferred_tuple_names = true 58 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true 59 | dotnet_style_prefer_simplified_boolean_expressions = true 60 | dotnet_style_prefer_simplified_interpolation = true 61 | 62 | # Field preferences 63 | dotnet_style_readonly_field = true 64 | 65 | # Parameter preferences 66 | dotnet_code_quality_unused_parameters = all 67 | 68 | # Suppression preferences 69 | dotnet_remove_unnecessary_suppression_exclusions = none 70 | 71 | # New line preferences 72 | dotnet_style_allow_multiple_blank_lines_experimental = false:suggestion 73 | dotnet_style_allow_statement_immediately_after_block_experimental = false 74 | 75 | #### C# Coding Conventions #### 76 | 77 | # var preferences 78 | csharp_style_var_elsewhere = true:silent 79 | csharp_style_var_for_built_in_types = true:silent 80 | csharp_style_var_when_type_is_apparent = true:silent 81 | 82 | # Expression-bodied members 83 | csharp_style_expression_bodied_accessors = true:silent 84 | csharp_style_expression_bodied_constructors = false:silent 85 | csharp_style_expression_bodied_indexers = true:silent 86 | csharp_style_expression_bodied_lambdas = true:silent 87 | csharp_style_expression_bodied_local_functions = false:silent 88 | csharp_style_expression_bodied_methods = false:silent 89 | csharp_style_expression_bodied_operators = false:silent 90 | csharp_style_expression_bodied_properties = true:silent 91 | 92 | # Pattern matching preferences 93 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 94 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 95 | csharp_style_prefer_extended_property_pattern = true:suggestion 96 | csharp_style_prefer_not_pattern = true:suggestion 97 | csharp_style_prefer_pattern_matching = true:silent 98 | csharp_style_prefer_switch_expression = true:suggestion 99 | 100 | # Null-checking preferences 101 | csharp_style_conditional_delegate_call = true:suggestion 102 | 103 | # Modifier preferences 104 | csharp_prefer_static_local_function = true:suggestion 105 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async 106 | 107 | # Code-block preferences 108 | csharp_prefer_braces = true:silent 109 | csharp_prefer_simple_using_statement = true:suggestion 110 | csharp_style_namespace_declarations = block_scoped:silent 111 | 112 | # Expression-level preferences 113 | csharp_prefer_simple_default_expression = true:suggestion 114 | csharp_style_deconstructed_variable_declaration = true:suggestion 115 | csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion 116 | csharp_style_inlined_variable_declaration = true:suggestion 117 | csharp_style_prefer_index_operator = true:suggestion 118 | csharp_style_prefer_local_over_anonymous_function = true:suggestion 119 | csharp_style_prefer_null_check_over_type_check = true:suggestion 120 | csharp_style_prefer_range_operator = false:suggestion 121 | csharp_style_prefer_tuple_swap = true:suggestion 122 | csharp_style_throw_expression = true:suggestion 123 | csharp_style_unused_value_assignment_preference = discard_variable:suggestion 124 | csharp_style_unused_value_expression_statement_preference = discard_variable:silent 125 | 126 | # 'using' directive preferences 127 | csharp_using_directive_placement = inside_namespace:none 128 | 129 | # New line preferences 130 | csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false:suggestion 131 | csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:suggestion 132 | csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent 133 | 134 | #### C# Formatting Rules #### 135 | 136 | # New line preferences 137 | csharp_new_line_before_catch = true 138 | csharp_new_line_before_else = true 139 | csharp_new_line_before_finally = true 140 | csharp_new_line_before_members_in_anonymous_types = true 141 | csharp_new_line_before_members_in_object_initializers = true 142 | csharp_new_line_before_open_brace = all 143 | csharp_new_line_between_query_expression_clauses = true 144 | 145 | # Indentation preferences 146 | csharp_indent_block_contents = true 147 | csharp_indent_braces = false 148 | csharp_indent_case_contents = true 149 | csharp_indent_case_contents_when_block = true 150 | csharp_indent_labels = one_less_than_current 151 | csharp_indent_switch_labels = true 152 | 153 | # Space preferences 154 | csharp_space_after_cast = false 155 | csharp_space_after_colon_in_inheritance_clause = true 156 | csharp_space_after_comma = true 157 | csharp_space_after_dot = false 158 | csharp_space_after_keywords_in_control_flow_statements = true 159 | csharp_space_after_semicolon_in_for_statement = true 160 | csharp_space_around_binary_operators = before_and_after 161 | csharp_space_around_declaration_statements = false 162 | csharp_space_before_colon_in_inheritance_clause = true 163 | csharp_space_before_comma = false 164 | csharp_space_before_dot = false 165 | csharp_space_before_open_square_brackets = false 166 | csharp_space_before_semicolon_in_for_statement = false 167 | csharp_space_between_empty_square_brackets = false 168 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 169 | csharp_space_between_method_call_name_and_opening_parenthesis = false 170 | csharp_space_between_method_call_parameter_list_parentheses = false 171 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 172 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 173 | csharp_space_between_method_declaration_parameter_list_parentheses = false 174 | csharp_space_between_parentheses = false 175 | csharp_space_between_square_brackets = false 176 | 177 | # Wrapping preferences 178 | csharp_preserve_single_line_blocks = true 179 | csharp_preserve_single_line_statements = true 180 | 181 | #### Naming styles #### 182 | 183 | # Naming rules 184 | 185 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion 186 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface 187 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i 188 | 189 | dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion 190 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types 191 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case 192 | 193 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion 194 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members 195 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case 196 | 197 | # Symbol specifications 198 | 199 | dotnet_naming_symbols.interface.applicable_kinds = interface 200 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 201 | dotnet_naming_symbols.interface.required_modifiers = 202 | 203 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum 204 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 205 | dotnet_naming_symbols.types.required_modifiers = 206 | 207 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method 208 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 209 | dotnet_naming_symbols.non_field_members.required_modifiers = 210 | 211 | # Naming styles 212 | 213 | dotnet_naming_style.pascal_case.required_prefix = 214 | dotnet_naming_style.pascal_case.required_suffix = 215 | dotnet_naming_style.pascal_case.word_separator = 216 | dotnet_naming_style.pascal_case.capitalization = pascal_case 217 | 218 | dotnet_naming_style.begins_with_i.required_prefix = I 219 | dotnet_naming_style.begins_with_i.required_suffix = 220 | dotnet_naming_style.begins_with_i.word_separator = 221 | dotnet_naming_style.begins_with_i.capitalization = pascal_case 222 | 223 | # IDE0065: Misplaced using directive 224 | dotnet_diagnostic.IDE0065.severity = none 225 | 226 | [*.{cs,vb}] 227 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 228 | tab_width = 4 229 | indent_size = 4 230 | end_of_line = crlf 231 | dotnet_style_coalesce_expression = true:suggestion 232 | dotnet_style_null_propagation = true:suggestion 233 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion 234 | dotnet_style_prefer_auto_properties = true:silent 235 | dotnet_style_object_initializer = true:suggestion 236 | dotnet_style_collection_initializer = true:suggestion 237 | dotnet_style_prefer_simplified_boolean_expressions = true:suggestion 238 | dotnet_style_prefer_conditional_expression_over_assignment = true:none 239 | dotnet_style_prefer_conditional_expression_over_return = true:none 240 | dotnet_style_explicit_tuple_names = true:suggestion 241 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 242 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 243 | dotnet_style_prefer_compound_assignment = true:suggestion 244 | dotnet_style_prefer_simplified_interpolation = true:suggestion 245 | dotnet_style_namespace_match_folder = true:suggestion 246 | dotnet_style_readonly_field = true:suggestion 247 | dotnet_style_predefined_type_for_locals_parameters_members = true:warning 248 | dotnet_style_predefined_type_for_member_access = true:warning 249 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent 250 | dotnet_style_allow_multiple_blank_lines_experimental = false:suggestion 251 | dotnet_style_allow_statement_immediately_after_block_experimental = false:silent 252 | dotnet_code_quality_unused_parameters = all:suggestion 253 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion 254 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion 255 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion 256 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent 257 | dotnet_style_qualification_for_field = false:silent 258 | dotnet_style_qualification_for_property = false:warning 259 | dotnet_style_qualification_for_method = false:warning 260 | dotnet_style_qualification_for_event = false:warning 261 | 262 | # IDE0046: Convert to conditional expression 263 | dotnet_diagnostic.IDE0046.severity = none 264 | 265 | # IDE0045: Convert to conditional expression 266 | dotnet_diagnostic.IDE0045.severity = none 267 | --------------------------------------------------------------------------------