
You're building your twentieth Power Query solution this year. The project involves transforming sales data, and once again, you find yourself writing the same date parsing logic you wrote last month. The same currency conversion function from three months ago. The same complex text cleaning routine that took you hours to perfect six months back. Sound familiar?
This repetitive coding isn't just inefficient—it's a maintenance nightmare. When business requirements change (and they always do), you're hunting through dozens of queries to update the same logic. When a bug surfaces, you're fixing it in multiple places. When a new team member joins, they're reinventing solutions you've already perfected.
Building a reusable function library in Power Query transforms how you work. Instead of copy-pasting code fragments, you create a centralized repository of battle-tested functions that can be called across any query in your organization. Think of it as your personal toolkit of M language solutions, ready to be deployed wherever needed.
What you'll learn:
This lesson assumes you're comfortable writing M language code, understand Power Query's evaluation model, and have experience creating custom functions. You should be familiar with M's type system, record and list manipulation, and basic error handling patterns.
Before diving into library construction, we need to understand how Power Query's M language handles functions at an architectural level. Unlike traditional programming languages where functions exist as standalone entities, M treats functions as first-class values that can be stored, passed around, and invoked just like numbers or text.
This fundamental design enables powerful patterns but also creates unique challenges for library construction. Let's explore the mechanics through a concrete example.
Consider this simple function that standardizes phone numbers:
let
StandardizePhone = (phoneText as text) as text =>
let
// Remove all non-numeric characters
cleanedPhone = Text.Select(phoneText, {"0".."9"}),
// Format based on length
formattedPhone =
if Text.Length(cleanedPhone) = 10
then "(" & Text.Range(cleanedPhone, 0, 3) & ") " &
Text.Range(cleanedPhone, 3, 3) & "-" &
Text.Range(cleanedPhone, 6, 4)
else if Text.Length(cleanedPhone) = 11 and Text.Start(cleanedPhone, 1) = "1"
then "(" & Text.Range(cleanedPhone, 1, 3) & ") " &
Text.Range(cleanedPhone, 4, 3) & "-" &
Text.Range(cleanedPhone, 7, 4)
else phoneText // Return original if can't parse
in
formattedPhone
in
StandardizePhone
This function works perfectly in isolation, but what happens when you need it across multiple queries? The naive approach is copying and pasting, but this creates several problems:
The solution lies in understanding M's module system and how to properly architect shared functions.
Power Query provides a clean way to create shared functions through what I call the "library pattern." This involves creating dedicated queries that contain multiple related functions, organized as records.
Let's build a text processing library that demonstrates this pattern:
let
// Text Processing Library v1.0
// Contains functions for common text manipulation tasks
StandardizePhone = (phoneText as text) as text =>
let
cleanedPhone = Text.Select(phoneText, {"0".."9"}),
formattedPhone =
if Text.Length(cleanedPhone) = 10
then "(" & Text.Range(cleanedPhone, 0, 3) & ") " &
Text.Range(cleanedPhone, 3, 3) & "-" &
Text.Range(cleanedPhone, 6, 4)
else if Text.Length(cleanedPhone) = 11 and Text.Start(cleanedPhone, 1) = "1"
then "(" & Text.Range(cleanedPhone, 1, 3) & ") " &
Text.Range(cleanedPhone, 4, 3) & "-" &
Text.Range(cleanedPhone, 7, 4)
else phoneText
in
formattedPhone,
CleanCompanyName = (companyText as text) as text =>
let
// Remove common suffixes and standardize
suffixesToRemove = {"Inc.", "LLC", "Corp.", "Corporation", "Limited", "Ltd."},
trimmedText = Text.Trim(companyText),
// Remove suffixes
cleanedText = List.Accumulate(
suffixesToRemove,
trimmedText,
(state, suffix) =>
if Text.EndsWith(state, suffix, Comparer.OrdinalIgnoreCase)
then Text.Trim(Text.RemoveRange(state, Text.Length(state) - Text.Length(suffix)))
else state
),
// Proper case conversion
properCaseText = Text.Proper(cleanedText)
in
properCaseText,
ExtractDomain = (emailText as text) as text =>
let
atPosition = Text.PositionOf(emailText, "@"),
domain =
if atPosition >= 0
then Text.Range(emailText, atPosition + 1)
else null
in
domain,
// Metadata about this library
LibraryInfo = [
Name = "TextProcessing",
Version = "1.0",
Description = "Common text manipulation functions",
Functions = {"StandardizePhone", "CleanCompanyName", "ExtractDomain"}
]
in
[
StandardizePhone = StandardizePhone,
CleanCompanyName = CleanCompanyName,
ExtractDomain = ExtractDomain,
Info = LibraryInfo
]
Save this as a query named "TextProcessingLib." Now you can reference these functions from any other query in your workbook:
let
Source = Excel.CurrentWorkbook(){[Name="CustomerData"]}[Content],
// Reference the library
TextLib = TextProcessingLib,
// Apply library functions
ProcessedData = Table.AddColumn(
Table.AddColumn(
Source,
"StandardizedPhone",
each TextLib[StandardizePhone]([Phone])
),
"CleanCompany",
each TextLib[CleanCompanyName]([Company])
)
in
ProcessedData
This pattern provides several advantages:
As your library grows, you'll encounter scenarios that require more sophisticated function design. Let's explore several advanced patterns that make functions more flexible and maintainable.
Real-world data comes in many forms. A robust function should handle multiple input types gracefully. Consider this date parsing function:
let
SmartDateParser = (dateValue as any) as nullable date =>
let
// Handle null values
result = if dateValue = null then null
else
let
valueType = Value.Type(dateValue)
in
// Already a date
if Type.Is(valueType, type date) then dateValue
// Text that might be a date
else if Type.Is(valueType, type text) then
let
trimmedText = Text.Trim(dateValue),
// Try standard parsing first
standardParse = try Date.From(trimmedText) otherwise null,
// If that fails, try common formats
customParse = if standardParse = null then
let
// Try MM/DD/YYYY format
mmddyyyy = try Date.FromText(trimmedText, "en-US") otherwise null,
// Try DD/MM/YYYY format
ddmmyyyy = if mmddyyyy = null then
try
let
parts = Text.Split(trimmedText, "/"),
day = Number.From(parts{0}),
month = Number.From(parts{1}),
year = Number.From(parts{2})
in
#date(year, month, day)
otherwise null
else mmddyyyy,
// Try Excel serial dates
serialDate = if ddmmyyyy = null then
try
let
numericValue = Number.From(trimmedText),
// Excel epoch is 1900-01-01, but has a leap year bug
baseDate = #date(1900, 1, 1),
daysToAdd = Duration.Days(Duration.From(numericValue - 2))
in
Date.AddDays(baseDate, daysToAdd)
otherwise null
else ddmmyyyy
in
serialDate
else standardParse
in
customParse
// Numeric values (Excel serial dates)
else if Type.Is(valueType, type number) then
try
let
baseDate = #date(1900, 1, 1),
daysToAdd = Duration.Days(Duration.From(dateValue - 2))
in
Date.AddDays(baseDate, daysToAdd)
otherwise null
else null
in
result
in
SmartDateParser
This function demonstrates several important patterns:
Value.Type() and Type.Is() to determine input typetry...otherwise to handle parsing failuresSome functions benefit from configuration parameters that control their behavior. This pattern is particularly useful for functions that need to adapt to different business rules:
let
ConfigurableValidator = (value as any, config as record) as record =>
let
// Default configuration
defaultConfig = [
AllowNull = false,
MinLength = 0,
MaxLength = 999999,
RequiredPattern = null,
ForbiddenValues = {},
CaseSensitive = false
],
// Merge user config with defaults
finalConfig = Record.Combine({defaultConfig, config}),
// Initialize result
validationResult = [IsValid = true, Errors = {}],
// Null check
nullCheck = if not finalConfig[AllowNull] and value = null then
[IsValid = false, Errors = {"Value cannot be null"}]
else validationResult,
// Length checks (for text values)
lengthCheck = if nullCheck[IsValid] and value <> null then
if Value.Is(value, type text) then
let
textLength = Text.Length(value),
tooShort = textLength < finalConfig[MinLength],
tooLong = textLength > finalConfig[MaxLength],
errors =
(if tooShort then {"Value too short (min: " & Number.ToText(finalConfig[MinLength]) & ")"} else {}) &
(if tooLong then {"Value too long (max: " & Number.ToText(finalConfig[MaxLength]) & ")"} else {})
in
[
IsValid = not (tooShort or tooLong),
Errors = errors
]
else nullCheck
else nullCheck,
// Pattern check
patternCheck = if lengthCheck[IsValid] and finalConfig[RequiredPattern] <> null then
if Value.Is(value, type text) then
let
patternMatch = try Text.Contains(value, finalConfig[RequiredPattern]) otherwise false,
result = if patternMatch then lengthCheck
else [IsValid = false, Errors = lengthCheck[Errors] & {"Value doesn't match required pattern"}]
in
result
else lengthCheck
else lengthCheck,
// Forbidden values check
forbiddenCheck = if patternCheck[IsValid] and List.Count(finalConfig[ForbiddenValues]) > 0 then
let
compareValue = if finalConfig[CaseSensitive] then value else Text.Upper(value),
forbiddenList = if finalConfig[CaseSensitive] then finalConfig[ForbiddenValues]
else List.Transform(finalConfig[ForbiddenValues], Text.Upper),
isForbidden = List.Contains(forbiddenList, compareValue),
result = if isForbidden then
[IsValid = false, Errors = patternCheck[Errors] & {"Value is not allowed"}]
else patternCheck
in
result
else patternCheck
in
forbiddenCheck
in
ConfigurableValidator
You can use this function with different configurations:
// Strict email validation
EmailValidator = (email) => ConfigurableValidator(email, [
AllowNull = false,
MinLength = 5,
MaxLength = 254,
RequiredPattern = "@",
ForbiddenValues = {"test@test.com", "admin@admin.com"}
])
// Flexible name validation
NameValidator = (name) => ConfigurableValidator(name, [
AllowNull = true,
MinLength = 1,
MaxLength = 100,
CaseSensitive = false
])
M language supports functional programming patterns, including functions that accept other functions as parameters. This enables powerful abstraction:
let
TableProcessor = (sourceTable as table, transformFunctions as list) as table =>
let
// Apply each transformation function in sequence
result = List.Accumulate(
transformFunctions,
sourceTable,
(currentTable, transformFunc) => transformFunc(currentTable)
)
in
result,
// Example transformation functions
AddRowNumbers = (tbl as table) as table =>
Table.AddIndexColumn(tbl, "RowNumber", 1),
RemoveEmptyRows = (tbl as table) as table =>
Table.SelectRows(tbl, each not List.IsEmpty(List.RemoveNulls(Record.FieldValues(_)))),
StandardizeHeaders = (tbl as table) as table =>
let
currentColumns = Table.ColumnNames(tbl),
newColumns = List.Transform(currentColumns, each Text.Proper(Text.Replace(_, " ", "_"))),
renamedTable = Table.RenameColumns(tbl, List.Zip({currentColumns, newColumns}))
in
renamedTable
in
[
TableProcessor = TableProcessor,
AddRowNumbers = AddRowNumbers,
RemoveEmptyRows = RemoveEmptyRows,
StandardizeHeaders = StandardizeHeaders
]
Usage example:
let
Source = Excel.CurrentWorkbook(){[Name="RawData"]}[Content],
ProcessorLib = TableProcessorLib,
// Define transformation pipeline
transformations = {
ProcessorLib[RemoveEmptyRows],
ProcessorLib[StandardizeHeaders],
ProcessorLib[AddRowNumbers]
},
// Apply all transformations
ProcessedTable = ProcessorLib[TableProcessor](Source, transformations)
in
ProcessedTable
When building function libraries, performance becomes critical—especially when functions are applied to large datasets. Understanding how Power Query's evaluation engine works is essential for creating efficient libraries.
Query folding is Power Query's ability to push operations back to the data source (like SQL Server). Functions that break query folding can dramatically impact performance:
let
// This function preserves query folding for supported sources
FoldableDateRange = (startDate as date, endDate as date) as function =>
(dateColumn as any) as any =>
dateColumn >= startDate and dateColumn <= endDate,
// This function breaks query folding due to M-specific logic
NonFoldableDateRange = (startDate as date, endDate as date) as function =>
(dateColumn as any) as any =>
let
// Any custom M logic here breaks folding
adjustedStart = Date.AddDays(startDate, 0),
adjustedEnd = Date.AddDays(endDate, 0)
in
dateColumn >= adjustedStart and dateColumn <= adjustedEnd,
// Smart function that attempts to preserve folding
SmartDateFilter = (tbl as table, dateColumnName as text, startDate as date, endDate as date) as table =>
let
// Check if we can use simple comparison (foldable)
simpleFilter = try
Table.SelectRows(tbl, each Record.Field(_, dateColumnName) >= startDate and
Record.Field(_, dateColumnName) <= endDate)
otherwise null,
// Fallback to row-by-row processing if needed
complexFilter = if simpleFilter = null then
Table.SelectRows(tbl,
each
let
dateValue = Record.Field(_, dateColumnName)
in
dateValue <> null and dateValue >= startDate and dateValue <= endDate
)
else simpleFilter
in
complexFilter
in
[
FoldableDateRange = FoldableDateRange,
NonFoldableDateRange = NonFoldableDateRange,
SmartDateFilter = SmartDateFilter
]
M language uses lazy evaluation—expressions are only computed when their results are needed. You can leverage this for performance:
let
ExpensiveCalculationCache = () as function =>
let
// This creates a memoization cache
cache = {},
memoizedFunction = (input as text) as any =>
let
// Check if result is already cached
cachedResult = try Record.Field(cache, input) otherwise null,
result = if cachedResult <> null then cachedResult
else
let
// Expensive operation here
calculated = Text.Length(input) * 1000 + Text.ToNumber(Text.Start(input, 1)),
// Store in cache for next time
updatedCache = Record.AddField(cache, input, calculated)
in
calculated
in
result
in
memoizedFunction,
// Efficient bulk processing function
BulkTextProcessor = (textList as list, processingFunction as function) as list =>
let
// Process in batches to manage memory
batchSize = 1000,
batches = List.Generate(
() => 0,
each _ < List.Count(textList),
each _ + batchSize,
each List.Range(textList, _, Number.Min(batchSize, List.Count(textList) - _))
),
// Process each batch
processedBatches = List.Transform(batches,
each List.Transform(_, processingFunction)
),
// Flatten results
result = List.Combine(processedBatches)
in
result
in
[
ExpensiveCalculationCache = ExpensiveCalculationCache,
BulkTextProcessor = BulkTextProcessor
]
Large datasets require careful memory management in your functions:
let
StreamingTableProcessor = (sourceTable as table, rowProcessor as function, batchSize as number) as table =>
let
totalRows = Table.RowCount(sourceTable),
// Process table in chunks to manage memory
processedChunks = List.Generate(
() => [StartIndex = 0, ProcessedRows = {}],
each [StartIndex] < totalRows,
each
let
currentStart = [StartIndex],
currentBatch = Table.Range(sourceTable, currentStart, batchSize),
processedBatch = Table.TransformRows(currentBatch, rowProcessor)
in
[
StartIndex = currentStart + batchSize,
ProcessedRows = processedBatch
],
each [ProcessedRows]
),
// Combine all processed rows
allProcessedRows = List.Combine(processedChunks),
// Convert back to table
result = if List.Count(allProcessedRows) > 0 then
Table.FromRecords(allProcessedRows)
else
Table.FromRecords({})
in
result
in
StreamingTableProcessor
Production function libraries must handle errors gracefully. M language provides several mechanisms for robust error handling:
let
RobustDataConverter = (value as any, targetType as text, config as nullable record) as record =>
let
// Default configuration
defaultConfig = [
AllowNull = true,
DefaultValue = null,
StrictMode = false,
LogErrors = true
],
finalConfig = if config = null then defaultConfig else Record.Combine({defaultConfig, config}),
// Main conversion logic with error boundaries
conversionResult = try
let
result = if value = null then
if finalConfig[AllowNull] then
[Success = true, Value = null, Error = null]
else
[Success = false, Value = finalConfig[DefaultValue], Error = "Null values not allowed"]
else
// Type-specific conversion logic
let
convertedValue = if targetType = "number" then
if Value.Is(value, type number) then value
else if Value.Is(value, type text) then
let
cleanText = Text.Trim(Text.Replace(Text.Replace(value, ",", ""), "$", "")),
numericValue = try Number.From(cleanText) otherwise null
in
if numericValue = null then
error "Cannot convert '" & value & "' to number"
else numericValue
else error "Unsupported conversion from " & Text.From(Value.Type(value)) & " to number"
else if targetType = "text" then
Text.From(value)
else if targetType = "date" then
if Value.Is(value, type date) then value
else if Value.Is(value, type text) then
try Date.FromText(value) otherwise
try Date.From(value) otherwise
error "Cannot parse '" & value & "' as date"
else if Value.Is(value, type number) then
try Date.From(value) otherwise
error "Cannot convert numeric value " & Number.ToText(value) & " to date"
else error "Unsupported conversion to date"
else error "Unsupported target type: " & targetType
in
[Success = true, Value = convertedValue, Error = null]
in
result
otherwise
let
errorMessage = if finalConfig[StrictMode] then
"Conversion failed: " & (try Text.From(value) otherwise "Unknown value") & " to " & targetType
else
"Using default value due to conversion error",
fallbackValue = if finalConfig[StrictMode] then
error errorMessage
else
finalConfig[DefaultValue]
in
[Success = false, Value = fallbackValue, Error = errorMessage],
// Add logging if requested
finalResult = if finalConfig[LogErrors] and conversionResult[Success] = false then
let
logEntry = [
Timestamp = DateTime.LocalNow(),
InputValue = value,
TargetType = targetType,
Error = conversionResult[Error]
]
// In a real implementation, you might write this to a log table
in
conversionResult
else conversionResult
in
finalResult,
// Validation function with detailed error reporting
ValidateRecord = (record as record, schema as record) as record =>
let
// Schema format: [FieldName = [Type = "text", Required = true, Validator = someFunction]]
fieldNames = Record.FieldNames(schema),
validationResults = List.Transform(fieldNames,
(fieldName) =>
let
fieldSchema = Record.Field(schema, fieldName),
fieldValue = try Record.Field(record, fieldName) otherwise null,
// Required field check
requiredCheck = if fieldSchema[Required] = true and fieldValue = null then
[Field = fieldName, IsValid = false, Error = "Required field is missing"]
else
[Field = fieldName, IsValid = true, Error = null],
// Type check
typeCheck = if requiredCheck[IsValid] and fieldValue <> null then
let
conversionResult = RobustDataConverter(fieldValue, fieldSchema[Type], null)
in
if conversionResult[Success] then requiredCheck
else [Field = fieldName, IsValid = false, Error = conversionResult[Error]]
else requiredCheck,
// Custom validation
customCheck = if typeCheck[IsValid] and fieldSchema[Validator]? <> null then
try
let
validatorResult = fieldSchema[Validator](fieldValue)
in
if validatorResult then typeCheck
else [Field = fieldName, IsValid = false, Error = "Failed custom validation"]
otherwise
[Field = fieldName, IsValid = false, Error = "Custom validator threw an error"]
else typeCheck
in
customCheck
),
overallResult = [
IsValid = List.AllTrue(List.Transform(validationResults, each [IsValid])),
Errors = List.Select(validationResults, each [IsValid] = false),
Details = validationResults
]
in
overallResult
in
[
RobustDataConverter = RobustDataConverter,
ValidateRecord = ValidateRecord
]
A robust function library needs comprehensive testing. Here's a simple testing framework built in M:
let
// Simple testing framework for M functions
TestRunner = [
// Create a test case
CreateTest = (name as text, testFunction as function, expectedResult as any) as record =>
[
Name = name,
TestFunction = testFunction,
Expected = expectedResult,
Passed = null,
Actual = null,
Error = null
],
// Run a single test
RunTest = (test as record) as record =>
let
result = try
let
actual = test[TestFunction](),
passed = actual = test[Expected]
in
[
Name = test[Name],
TestFunction = test[TestFunction],
Expected = test[Expected],
Passed = passed,
Actual = actual,
Error = null
]
otherwise
[
Name = test[Name],
TestFunction = test[TestFunction],
Expected = test[Expected],
Passed = false,
Actual = null,
Error = "Exception: " & Text.From([Message])
]
in
result,
// Run a suite of tests
RunTestSuite = (tests as list) as record =>
let
results = List.Transform(tests, TestRunner[RunTest]),
passed = List.Count(List.Select(results, each [Passed] = true)),
total = List.Count(results),
summary = [
TotalTests = total,
PassedTests = passed,
FailedTests = total - passed,
SuccessRate = if total = 0 then 0 else passed / total,
Results = results,
FailedTests = List.Select(results, each [Passed] <> true)
]
in
summary
]
in
TestRunner
Usage example:
let
// Import your function library
TextLib = TextProcessingLib,
TestFramework = TestRunner,
// Define test cases
phoneTests = {
TestFramework[CreateTest](
"Standard 10-digit phone",
() => TextLib[StandardizePhone]("1234567890"),
"(123) 456-7890"
),
TestFramework[CreateTest](
"Phone with formatting",
() => TextLib[StandardizePhone]("(123) 456-7890"),
"(123) 456-7890"
),
TestFramework[CreateTest](
"11-digit phone with country code",
() => TextLib[StandardizePhone]("11234567890"),
"(123) 456-7890"
)
},
// Run tests
testResults = TestFramework[RunTestSuite](phoneTests)
in
testResults
A function library is only as good as its documentation and discoverability. Here are patterns for creating self-documenting, team-friendly libraries:
let
// Advanced function with embedded documentation
CreateDocumentedFunction = (functionName as text, functionLogic as function, documentation as record) as record =>
let
documentedFunction = Record.Combine({
[Function = functionLogic],
[Documentation = documentation]
})
in
Record.AddField([], functionName, documentedFunction),
// Comprehensive business logic library with full documentation
BusinessRulesLibrary =
let
// Customer classification function
ClassifyCustomerDoc = [
Name = "ClassifyCustomer",
Description = "Classifies customers into segments based on purchase history and demographics",
Parameters = [
totalPurchases = [Type = "number", Description = "Total lifetime purchases", Required = true],
daysSinceLastPurchase = [Type = "number", Description = "Days since last purchase", Required = true],
preferredCustomer = [Type = "logical", Description = "Whether customer is in preferred program", Required = false, Default = false]
],
ReturnType = "text",
ReturnDescription = "Customer segment: Premium, Standard, AtRisk, or Inactive",
Examples = {
[Input = [totalPurchases = 5000, daysSinceLastPurchase = 30, preferredCustomer = true], Output = "Premium"],
[Input = [totalPurchases = 1000, daysSinceLastPurchase = 90, preferredCustomer = false], Output = "Standard"],
[Input = [totalPurchases = 100, daysSinceLastPurchase = 365, preferredCustomer = false], Output = "Inactive"]
},
Version = "2.1",
LastModified = #date(2024, 1, 15),
Author = "Data Team"
],
ClassifyCustomerFunction = (totalPurchases as number, daysSinceLastPurchase as number, optional preferredCustomer as nullable logical) as text =>
let
isPreferred = if preferredCustomer = null then false else preferredCustomer,
classification =
if totalPurchases >= 2000 and daysSinceLastPurchase <= 60 then "Premium"
else if totalPurchases >= 500 and daysSinceLastPurchase <= 180 then "Standard"
else if daysSinceLastPurchase <= 365 then "AtRisk"
else "Inactive",
// Preferred customers get upgraded classification
finalClassification = if isPreferred and classification = "Standard" then "Premium"
else if isPreferred and classification = "AtRisk" then "Standard"
else classification
in
finalClassification,
// Price calculation with business rules
CalculatePriceDoc = [
Name = "CalculatePrice",
Description = "Calculates final price with discounts, taxes, and business rules",
Parameters = [
basePrice = [Type = "number", Description = "Base price before adjustments", Required = true],
customerSegment = [Type = "text", Description = "Customer segment from ClassifyCustomer", Required = true],
region = [Type = "text", Description = "Customer's region code", Required = true],
quantity = [Type = "number", Description = "Quantity purchased", Required = false, Default = 1]
],
BusinessRules = {
"Premium customers get 15% discount",
"Standard customers get 5% discount on quantities > 10",
"Regional tax rates: US=8.5%, CA=12%, EU=20%",
"Volume discounts: 50+ items = additional 10%, 100+ items = additional 15%"
},
Version = "1.3",
LastModified = #date(2024, 1, 10)
],
CalculatePriceFunction = (basePrice as number, customerSegment as text, region as text, optional quantity as nullable number) as number =>
let
qty = if quantity = null then 1 else quantity,
// Customer segment discounts
segmentDiscount = if customerSegment = "Premium" then 0.15
else if customerSegment = "Standard" and qty > 10 then 0.05
else 0,
// Volume discounts
volumeDiscount = if qty >= 100 then 0.15
else if qty >= 50 then 0.10
else 0,
// Apply discounts
discountedPrice = basePrice * (1 - segmentDiscount - volumeDiscount) * qty,
// Regional tax rates
taxRate = if region = "US" then 0.085
else if region = "CA" then 0.12
else if region = "EU" then 0.20
else 0.08, // default rate
finalPrice = discountedPrice * (1 + taxRate)
in
finalPrice,
// Library metadata
LibraryMetadata = [
Name = "BusinessRulesLibrary",
Version = "2.1",
Description = "Core business logic functions for customer management and pricing",
LastUpdated = #date(2024, 1, 15),
Maintainer = "Data Engineering Team",
Functions = {
[Name = "ClassifyCustomer", Documentation = ClassifyCustomerDoc],
[Name = "CalculatePrice", Documentation = CalculatePriceDoc]
}
]
in
[
ClassifyCustomer = [Function = ClassifyCustomerFunction, Documentation = ClassifyCustomerDoc],
CalculatePrice = [Function = CalculatePriceFunction, Documentation = CalculatePriceDoc],
GetDocumentation = (functionName as text) as record =>
if functionName = "ClassifyCustomer" then ClassifyCustomerDoc
else if functionName = "CalculatePrice" then CalculatePriceDoc
else [Error = "Function not found"],
LibraryInfo = LibraryMetadata
]
in
BusinessRulesLibrary
Managing library versions across multiple workbooks requires systematic approaches:
let
// Version management utilities
VersionManager = [
// Create version info record
CreateVersion = (major as number, minor as number, patch as number, label as nullable text) as record =>
[
Major = major,
Minor = minor,
Patch = patch,
Label = label,
VersionString = Number.ToText(major) & "." & Number.ToText(minor) & "." & Number.ToText(patch) &
(if label <> null then "-" & label else ""),
ReleaseDate = Date.From(DateTime.LocalNow())
],
// Compare two versions
CompareVersions = (version1 as record, version2 as record) as text =>
let
comparison = if version1[Major] > version2[Major] then "newer"
else if version1[Major] < version2[Major] then "older"
else if version1[Minor] > version2[Minor] then "newer"
else if version1[Minor] < version2[Minor] then "older"
else if version1[Patch] > version2[Patch] then "newer"
else if version1[Patch] < version2[Patch] then "older"
else "same"
in
comparison,
// Check compatibility between versions
IsCompatible = (libraryVersion as record, requiredVersion as record) as logical =>
let
// Major version must match, minor version must be >= required
compatible = libraryVersion[Major] = requiredVersion[Major] and
(libraryVersion[Minor] > requiredVersion[Minor] or
(libraryVersion[Minor] = requiredVersion[Minor] and
libraryVersion[Patch] >= requiredVersion[Patch]))
in
compatible
],
// Enhanced library with version checking
VersionedLibrary = [
Version = VersionManager[CreateVersion](2, 1, 0, "stable"),
RequiredPowerBIVersion = "2.0",
// Function that checks compatibility on first use
EnsureCompatibility = (requiredVersion as record) as logical =>
let
isCompatible = VersionManager[IsCompatible](
VersionedLibrary[Version],
requiredVersion
),
result = if not isCompatible then
error "Library version " & VersionedLibrary[Version][VersionString] &
" is not compatible with required version " & requiredVersion[VersionString]
else true
in
result,
// Wrapper that adds version checking to functions
VersionCheckedFunction = (functionLogic as function, requiredVersion as record) as function =>
() =>
let
versionCheck = VersionedLibrary[EnsureCompatibility](requiredVersion),
result = if versionCheck then functionLogic() else error "Version check failed"
in
result
]
in
[
VersionManager = VersionManager,
VersionedLibrary = VersionedLibrary
]
Getting your function libraries into the hands of users requires thoughtful distribution strategies. Here are several approaches:
For Excel-based libraries, create a template workbook with your functions:
Users can then reference your library by connecting to the shared workbook:
let
// Reference external library workbook
LibraryWorkbook = Excel.Workbook(File.Contents("\\shared\path\FunctionLibrary_v1.xlsx"), null, true),
TextProcessingLib = LibraryWorkbook{[Item="TextProcessingLib",Kind="Table"]}[Data]
in
TextProcessingLib
For Power BI environments, create template files (.pbit) that include your libraries:
Power BI Premium and Power Platform dataflows provide excellent library hosting:
let
// Reference function from a dataflow
FunctionDataflow = PowerPlatform.Dataflows(null){[workspaceId="your-workspace-id"]}
{[dataflowId="your-dataflow-id"]}
{[entityId="TextProcessingLib"]}[Data]
in
FunctionDataflow
For advanced teams, consider version-controlled distribution:
Let's build a comprehensive financial analysis library that demonstrates all the concepts we've covered:
Scenario: You're building a library for financial data processing that needs to handle currency conversions, calculate various financial metrics, and validate financial data across multiple business units.
Create a new query called "FinancialAnalysisLib":
let
// Version and metadata
LibraryVersion = [
Major = 1,
Minor = 0,
Patch = 0,
ReleaseDate = #date(2024, 1, 15),
Author = "Financial Systems Team"
],
// Currency conversion with caching
CurrencyConverter = () =>
let
// In real implementation, this would call an external API
exchangeRates = [
USD_EUR = 0.85,
USD_GBP = 0.73,
USD_CAD = 1.25,
USD_JPY = 110.0,
EUR_GBP = 0.86,
EUR_CAD = 1.47
],
converter = (amount as number, fromCurrency as text, toCurrency as text) as number =>
let
result = if fromCurrency = toCurrency then amount
else
let
rateKey = fromCurrency & "_" & toCurrency,
reverseKey = toCurrency & "_" & fromCurrency,
rate = try Record.Field(exchangeRates, rateKey)
otherwise try 1 / Record.Field(exchangeRates, reverseKey)
otherwise error "Exchange rate not available for " & fromCurrency & " to " & toCurrency,
convertedAmount = amount * rate
in
Number.Round(convertedAmount, 2)
in
result
in
converter,
// Financial ratio calculations
CalculateROI = (initialInvestment as number, currentValue as number) as number =>
let
roi = if initialInvestment = 0 then null
else Number.Round((currentValue - initialInvestment) / initialInvestment * 100, 2)
in
roi,
CalculateCAGR = (beginningValue as number, endingValue as number, years as number) as number =>
let
cagr = if beginningValue <= 0 or endingValue <= 0 or years <= 0 then null
else Number.Round((Number.Power(endingValue / beginningValue, 1 / years) - 1) * 100, 2)
in
cagr,
// Financial data validator
ValidateFinancialData = (record as record) as record =>
let
schema = [
Amount = [Type = "number", Required = true, Validator = (x) => x <> 0],
Currency = [Type = "text", Required = true, Validator = (x) => List.Contains({"USD", "EUR", "GBP", "CAD", "JPY"}, x)],
Date = [Type = "date", Required = true, Validator = (x) => x <= Date.From(DateTime.LocalNow())],
Category = [Type = "text", Required = false]
],
// Reuse our validation framework from earlier
validationResult = ValidateRecord(record, schema)
in
validationResult,
// Bulk financial processing
ProcessFinancialTable = (sourceTable as table, baseCurrency as text) as table =>
let
converter = CurrencyConverter(),
processedTable = Table.AddColumn(
Table.AddColumn(
sourceTable,
"AmountInBaseCurrency",
each try converter([Amount], [Currency], baseCurrency) otherwise null
),
"ValidationResult",
each ValidateFinancialData(_)
)
in
processedTable,
// Library interface
LibraryInterface = [
// Functions
CurrencyConverter = CurrencyConverter(),
CalculateROI = CalculateROI,
CalculateCAGR = CalculateCAGR,
ValidateFinancialData = ValidateFinancialData,
ProcessFinancialTable = ProcessFinancialTable,
// Metadata
Version = LibraryVersion,
// Documentation
GetDocumentation = (functionName as text) as record =>
if functionName = "CurrencyConverter" then [
Description = "Converts amounts between currencies using current exchange rates",
Parameters = ["amount: number", "fromCurrency: text", "toCurrency: text"],
Returns = "Converted amount as number"
]
else if functionName = "CalculateROI" then [
Description = "Calculates Return on Investment as a percentage",
Parameters = ["initialInvestment: number", "currentValue: number"],
Returns = "ROI percentage as number"
]
else [Error = "Function documentation not found"]
]
in
LibraryInterface
Create a query called "FinancialLibraryTests":
let
FinLib = FinancialAnalysisLib,
TestFramework = TestRunner,
// Currency conversion tests
currencyTests = {
TestFramework[CreateTest](
"Same currency conversion",
() => FinLib[CurrencyConverter](100, "USD", "USD"),
100
),
TestFramework[CreateTest](
"USD to EUR conversion",
() => FinLib[CurrencyConverter](100, "USD", "EUR"),
85 // Based on our mock rate
)
},
// ROI calculation tests
roiTests = {
TestFramework[CreateTest](
"Positive ROI calculation",
() => FinLib[CalculateROI](1000, 1200),
20 // 20% ROI
),
TestFramework[CreateTest](
"Negative ROI calculation",
() => FinLib[CalculateROI](1000, 800),
-20 // -20% ROI
)
},
// Combine all tests
allTests = currencyTests & roiTests,
// Run test suite
testResults = TestFramework[RunTestSuite](allTests)
in
testResults
Create a query called "LibraryDocumentation":
let
FinLib = FinancialAnalysisLib,
// Generate comprehensive documentation
documentation = [
LibraryName = "Financial Analysis Library",
Version = FinLib[Version],
// Function catalog
Functions = {
[
Name = "CurrencyConverter",
Description = "Converts monetary amounts between different currencies",
Syntax = "CurrencyConverter(amount, fromCurrency, toCurrency)",
Example = "CurrencyConverter(1000, \"USD\", \"EUR\")",
Documentation = FinLib[GetDocumentation]("CurrencyConverter")
],
[
Name = "CalculateROI",
Description = "Calculates return on investment percentage",
Syntax = "CalculateROI(initialInvestment, currentValue)",
Example = "CalculateROI(10000, 12000)",
Documentation = FinLib[GetDocumentation]("CalculateROI")
],
[
Name = "ProcessFinancialTable",
Description = "Processes entire tables of financial data with validation",
Syntax = "ProcessFinancialTable(sourceTable, baseCurrency)",
Example = "ProcessFinancialTable(MyData, \"USD\")"
]
},
// Usage examples
UsageExamples = {
[
Scenario = "Convert quarterly sales to USD",
Code = "
let
Source = Excel.CurrentWorkbook(){[Name=\"QuarterlySales\"]}[Content],
FinLib = FinancialAnalysisLib,
ConvertedData = Table.AddColumn(
Source,
\"Sales_USD\",
each FinLib[CurrencyConverter]([Sales_Amount], [Currency], \"USD\")
)
in
ConvertedData"
]
}
],
// Convert to table for easy viewing
documentationTable = Table.FromRecords(documentation[Functions])
in
documentationTable
Create a sample data table and test your library:
let
Source = Excel.CurrentWorkbook(){[Name="SampleFinancialData"]}[Content],
FinLib = FinancialAnalysisLib,
// Process the financial data
ProcessedData = FinLib[ProcessFinancialTable](Source, "USD"),
// Add ROI calculation for items with previous values (mock example)
WithROI = Table.AddColumn(
ProcessedData,
"ROI_Sample",
each if [Category] = "Investment" then
FinLib[CalculateROI]([Amount], [AmountInBaseCurrency] * 1.1) // Mock current value
else null
)
in
WithROI
This exercise demonstrates building a production-ready function library with proper testing, documentation, and real-world usage patterns.
One of the most common mistakes is misunderstanding how variable scope works in M functions:
// WRONG: Variable captured incorrectly
let
multiplier = 10,
CreateMultiplier = (factor) =>
() => factor * multiplier // This captures multiplier from outer scope
in
CreateMultiplier
// RIGHT: Explicit parameter passing
let
CreateMultiplier = (factor, multiplier) =>
() => factor * multiplier
in
CreateMultiplier
Problem: Functions that inadvertently break query folding:
// WRONG: Breaks query folding
FilterByComplexLogic = (tbl as table) =>
Table.SelectRows(tbl, each
let
processedValue = Text.Upper([Name]) // Any M-specific processing breaks folding
in
Text.StartsWith(processedValue, "A")
)
// BETTER: Preserve folding where possible
FilterByComplexLogic = (tbl as table) =>
let
// First apply foldable filter
preFiltered = Table.SelectRows(tbl, each Text.StartsWith([Name], "A") or Text.StartsWith([Name], "a")),
// Then apply complex logic to reduced dataset
finalFiltered = Table.SelectRows(preFiltered, each Text.StartsWith(Text.Upper([Name]), "A"))
in
finalFiltered
Problem: Swallowing errors without proper logging:
// WRONG: Silent failure
SafeFunction = (input) =>
try SomeComplexOperation(input) otherwise null
// BETTER: Structured error handling
SafeFunction = (input) =>
let
result = try SomeComplexOperation(input) otherwise [
Success = false,
Value = null,
Error = [Message],
Input = input,
Timestamp = DateTime.LocalNow()
]
in
result
Problem: Processing large datasets without chunking:
// WRONG: Processes entire dataset in memory
ProcessLargeTable = (tbl) =>
Table.TransformRows(tbl, each SomeExpensiveOperation(_))
// BETTER: Batch processing
ProcessLargeTable = (tbl) =>
let
batchSize = 1000,
// Implementation from earlier examples
result = StreamingTableProcessor(tbl, SomeExpensiveOperation, batchSize)
in
result
Common Issues:
Solutions:
You've now built a comprehensive understanding of creating reusable function libraries in Power Query. The key principles we've covered—modular design, robust error handling, performance optimization, and comprehensive documentation—will serve you well as you develop your own function repositories.
Your function library should now include:
The journey doesn't end here. As you build your function library, you'll discover patterns specific to your domain and organization. Consider these next steps:
Immediate Actions:
Advanced Exploration:
Community Engagement:
Building reusable function libraries transforms you from someone who writes queries to someone who builds data infrastructure. Your future self—and your colleagues—will thank you for the investment in creating maintainable, reliable, and well-documented solutions.
Learning Path: Advanced M Language