Creating Custom Functions
View SourceLearn how to implement custom functions for your specific use cases.
Overview
In Mau, functions and filters are the same thing - they use identical implementation patterns. This guide covers creating functions that perform specialized operations in templates.
Function Basics
Custom functions follow the same structure as filters:
defmodule MyApp.CustomFunctions do
def spec do
%{
category: :custom,
description: "Custom functions for MyApp",
filters: %{
"my_function" => %{
description: "What this function does",
function: {__MODULE__, :my_function}
}
}
}
end
def my_function(value, args) do
{:ok, result}
end
endMath Functions
Create domain-specific math operations:
defmodule MyApp.MathFunctions do
def spec do
%{
category: :math,
description: "Advanced math functions",
filters: %{
"percentage_of" => %{
description: "Calculate percentage of a value",
function: {__MODULE__, :percentage_of}
},
"tax" => %{
description: "Calculate tax on amount",
function: {__MODULE__, :tax}
},
"discount" => %{
description: "Calculate discounted price",
function: {__MODULE__, :discount}
},
"compound_interest" => %{
description: "Calculate compound interest",
function: {__MODULE__, :compound_interest}
}
}
}
end
@doc """
Calculate percentage of a value.
Usage: {{ amount | percentage_of(20) }}
100 -> 20 (20% of 100)
"""
def percentage_of(amount, [percentage]) when is_number(amount) and is_number(percentage) do
result = amount * percentage / 100
{:ok, Float.round(result, 2)}
end
def percentage_of(_value, _args) do
{:error, "percentage_of requires a number and percentage"}
end
@doc """
Calculate tax on amount.
Usage: {{ amount | tax(8.5) }}
Default tax rate: 10%
"""
def tax(amount, args) when is_number(amount) do
tax_rate = case args do
[rate] when is_number(rate) -> rate
_ -> 10 # Default to 10%
end
result = amount * tax_rate / 100
{:ok, Float.round(result, 2)}
end
def tax(_value, _args) do
{:error, "tax requires a number"}
end
@doc """
Calculate discounted price.
Usage: {{ original_price | discount(25) }}
Subtracts discount percentage from original price
"""
def discount(original_price, [discount_percent])
when is_number(original_price) and is_number(discount_percent) do
discount_amount = original_price * discount_percent / 100
result = original_price - discount_amount
{:ok, Float.round(result, 2)}
end
def discount(_value, _args) do
{:error, "discount requires a price and discount percentage"}
end
@doc """
Calculate compound interest.
Usage: {{ principal | compound_interest(rate, years, compounds) }}
compound_interest(1000, [5, 10, 12]) -> amount after 10 years at 5% compounded monthly
"""
def compound_interest(principal, [rate, years, compounds])
when is_number(principal) and is_number(rate) and is_number(years) and is_number(compounds) do
rate_decimal = rate / 100
exponent = compounds * years
base = 1 + rate_decimal / compounds
result = principal * :math.pow(base, exponent)
{:ok, Float.round(result, 2)}
end
def compound_interest(_value, _args) do
{:error, "compound_interest requires principal, rate, years, and compounds"}
end
endUsage:
context = %{
"amount" => 100,
"price" => 99.99,
"principal" => 1000
}
Mau.render("{{ amount | percentage_of(20) }}", context)
# Output: "20"
Mau.render("{{ price | tax(8.5) }}", context)
# Output: "8.5"
Mau.render("{{ price | discount(25) }}", context)
# Output: "74.99"Date/Time Functions
Create date and time utilities:
defmodule MyApp.DateFunctions do
def spec do
%{
category: :datetime,
description: "Date and time functions",
filters: %{
"days_since" => %{
description: "Calculate days since a date",
function: {__MODULE__, :days_since}
},
"format_date" => %{
description: "Format date string",
function: {__MODULE__, :format_date}
},
"is_past" => %{
description: "Check if date is in the past",
function: {__MODULE__, :is_past}
}
}
}
end
@doc """
Calculate days since a given date.
Usage: {{ date | days_since }}
Requires ISO8601 formatted date string
"""
def days_since(date_string, _args) when is_binary(date_string) do
try do
{:ok, date, _} = DateTime.from_iso8601(date_string)
today = DateTime.utc_now()
days = DateTime.diff(today, date, :day)
{:ok, days}
rescue
_ -> {:error, "Invalid date format. Use ISO8601."}
end
end
def days_since(_value, _args) do
{:error, "days_since requires an ISO8601 date string"}
end
@doc """
Format date for display.
Usage: {{ date | format_date("%B %d, %Y") }}
"""
def format_date(date_string, [format]) when is_binary(date_string) and is_binary(format) do
try do
{:ok, date, _} = DateTime.from_iso8601(date_string)
formatted = Calendar.strftime(date, format)
{:ok, formatted}
rescue
_ -> {:error, "Could not format date"}
end
end
def format_date(_value, _args) do
{:error, "format_date requires a date string and format"}
end
@doc """
Check if date is in the past.
"""
def is_past(date_string, _args) when is_binary(date_string) do
try do
{:ok, date, _} = DateTime.from_iso8601(date_string)
is_past_date = DateTime.compare(date, DateTime.utc_now()) == :lt
{:ok, is_past_date}
rescue
_ -> {:error, "Invalid date format"}
end
end
def is_past(_value, _args) do
{:error, "is_past requires an ISO8601 date string"}
end
endValidation Functions
Create functions that validate data:
defmodule MyApp.ValidationFunctions do
def spec do
%{
category: :validation,
description: "Data validation functions",
filters: %{
"is_email" => %{
description: "Validate email format",
function: {__MODULE__, :is_email}
},
"is_phone" => %{
description: "Validate phone number",
function: {__MODULE__, :is_phone}
},
"is_url" => %{
description: "Validate URL format",
function: {__MODULE__, :is_url}
}
}
}
end
@doc """
Validate email format.
"""
def is_email(email, _args) when is_binary(email) do
# Simple email validation regex
regex = ~r/^[^\s@]+@[^\s@]+\.[^\s@]+$/
{:ok, Regex.match?(regex, email)}
end
def is_email(_value, _args) do
{:error, "is_email requires a string"}
end
@doc """
Validate phone number (basic check).
"""
def is_phone(phone, _args) when is_binary(phone) do
# Remove common separators and check for digits
cleaned = String.replace(phone, ~r/[\s\-\(\)\+]/, "")
is_valid = String.length(cleaned) >= 10 and String.match?(cleaned, ~r/^\d+$/)
{:ok, is_valid}
end
def is_phone(_value, _args) do
{:error, "is_phone requires a string"}
end
@doc """
Validate URL format.
"""
def is_url(url, _args) when is_binary(url) do
# Simple URL validation
regex = ~r/^https?:\/\/.+/
{:ok, Regex.match?(regex, url)}
end
def is_url(_value, _args) do
{:error, "is_url requires a string"}
end
endUsage in templates:
{% if email | is_email %}
Email is valid
{% else %}
Please enter a valid email
{% endif %}Encoding/Decoding Functions
Create encoding and decoding utilities:
defmodule MyApp.EncodingFunctions do
def spec do
%{
category: :encoding,
description: "Encoding and decoding functions",
filters: %{
"base64_encode" => %{
description: "Encode to base64",
function: {__MODULE__, :base64_encode}
},
"base64_decode" => %{
description: "Decode from base64",
function: {__MODULE__, :base64_decode}
},
"url_encode" => %{
description: "URL encode a string",
function: {__MODULE__, :url_encode}
},
"html_escape" => %{
description: "Escape HTML special characters",
function: {__MODULE__, :html_escape}
}
}
}
end
def base64_encode(value, _args) when is_binary(value) do
encoded = Base.encode64(value)
{:ok, encoded}
end
def base64_encode(_value, _args) do
{:error, "base64_encode requires a string"}
end
def base64_decode(value, _args) when is_binary(value) do
case Base.decode64(value) do
{:ok, decoded} -> {:ok, decoded}
:error -> {:error, "Invalid base64 string"}
end
end
def base64_decode(_value, _args) do
{:error, "base64_decode requires a string"}
end
def url_encode(value, _args) when is_binary(value) do
encoded = URI.encode(value)
{:ok, encoded}
end
def url_encode(_value, _args) do
{:error, "url_encode requires a string"}
end
def html_escape(value, _args) when is_binary(value) do
escaped =
value
|> String.replace("&", "&")
|> String.replace("<", "<")
|> String.replace(">", ">")
|> String.replace("\"", """)
|> String.replace("'", "'")
{:ok, escaped}
end
def html_escape(_value, _args) do
{:error, "html_escape requires a string"}
end
endStatistical Functions
Create functions for data analysis:
defmodule MyApp.StatisticsFunctions do
def spec do
%{
category: :statistics,
description: "Statistical functions",
filters: %{
"mean" => %{
description: "Calculate mean of numbers",
function: {__MODULE__, :mean}
},
"median" => %{
description: "Calculate median of numbers",
function: {__MODULE__, :median}
},
"std_dev" => %{
description: "Calculate standard deviation",
function: {__MODULE__, :std_dev}
}
}
}
end
@doc """
Calculate mean (average) of a list of numbers.
"""
def mean(values, _args) when is_list(values) do
numbers = Enum.filter(values, &is_number/1)
if Enum.empty?(numbers) do
{:error, "No numeric values in list"}
else
average = Enum.sum(numbers) / length(numbers)
{:ok, Float.round(average, 2)}
end
end
def mean(_value, _args) do
{:error, "mean requires a list of numbers"}
end
@doc """
Calculate median of a list of numbers.
"""
def median(values, _args) when is_list(values) do
numbers =
values
|> Enum.filter(&is_number/1)
|> Enum.sort()
case length(numbers) do
0 ->
{:error, "No numeric values in list"}
count ->
mid = div(count, 2)
result = if rem(count, 2) == 0 do
(Enum.at(numbers, mid - 1) + Enum.at(numbers, mid)) / 2
else
Enum.at(numbers, mid)
end
{:ok, Float.round(result, 2)}
end
end
def median(_value, _args) do
{:error, "median requires a list of numbers"}
end
@doc """
Calculate standard deviation.
"""
def std_dev(values, _args) when is_list(values) do
numbers = Enum.filter(values, &is_number/1)
if length(numbers) < 2 do
{:error, "std_dev requires at least 2 numeric values"}
else
mean_val = Enum.sum(numbers) / length(numbers)
variance =
numbers
|> Enum.map(&:math.pow(&1 - mean_val, 2))
|> Enum.sum()
|> Kernel./(length(numbers))
std = :math.sqrt(variance)
{:ok, Float.round(std, 2)}
end
end
def std_dev(_value, _args) do
{:error, "std_dev requires a list of numbers"}
end
endBest Practices
1. Clear Documentation
@doc """
Description of what the function does.
Parameters:
- input (type): What it is
- arg1 (type): What it's for
Returns:
- Success: {:ok, result}
- Error: {:error, "error message"}
Usage examples:
{{ value | my_func }}
{{ value | my_func: arg1, arg2 }}
"""2. Consistent Error Messages
def my_func(value, args) do
case validate(value, args) do
:ok -> process(value, args)
:error -> {:error, "my_func: description of what went wrong"}
end
end3. Handle Edge Cases
def divide(numerator, [denominator]) when is_number(numerator) and is_number(denominator) do
if denominator == 0 do
{:error, "Cannot divide by zero"}
else
{:ok, numerator / denominator}
end
end4. Use Guards
# Good: Early filtering of invalid inputs
def my_func(value, _args) when is_binary(value) do
{:ok, process(value)}
end
def my_func(_value, _args) do
{:error, "my_func requires a string"}
end5. Test Thoroughly
defmodule MyApp.CustomFunctionsTest do
use ExUnit.Case
test "mean calculates average correctly" do
assert {:ok, 3.0} = MyApp.StatisticsFunctions.mean([1, 2, 3, 4, 5], [])
end
test "mean handles empty list" do
assert {:error, _} = MyApp.StatisticsFunctions.mean([], [])
end
test "mean handles non-numeric values" do
assert {:error, _} = MyApp.StatisticsFunctions.mean(["a", "b", "c"], [])
end
endSee Also
- Custom Filters - Creating custom filters
- Performance Tuning - Optimization techniques
- API Reference - Mau API documentation