mirror of
https://github.com/forcedotcom/afv-library.git
synced 2026-08-09 17:22:00 +08:00
Adds a skill for authoring Tableau Next semantic models (SDMs) on Data 360: build from scratch, add data objects, define joins, enrich with calculated fields and metrics, and make models AI-ready. Smoke-tested against a live Data 360 org: SDM discovery, AI-readiness flip, dimension creation, metric creation, and description backfill all exercised end-to-end.
194 lines
5.7 KiB
Python
194 lines
5.7 KiB
Python
"""Tableau formula function reference and validation.
|
|
|
|
Based on Tableau Prep function reference, adapted for Tableau Next calculated fields.
|
|
Provides function lists, validation, and helper utilities for expression building.
|
|
"""
|
|
|
|
from typing import Dict, List, Set, Tuple, Optional
|
|
import re
|
|
|
|
# Tableau formula functions (from Tableau Prep reference, adapted for Tableau Next)
|
|
# Note: Some Prep-specific functions may not be available in Tableau Next
|
|
|
|
# Aggregation functions (must be used in LOD expressions or UserAgg calculated fields)
|
|
AGGREGATION_FUNCTIONS = {
|
|
"SUM", "AVG", "COUNT", "COUNTD", "MIN", "MAX", "MEDIAN",
|
|
"STDEV", "STDEVP", "VAR", "VARP", "PERCENTILE"
|
|
}
|
|
|
|
# Level of Detail (LOD) functions
|
|
LOD_FUNCTIONS = {
|
|
"FIXED", "INCLUDE", "EXCLUDE"
|
|
}
|
|
|
|
# Date functions
|
|
DATE_FUNCTIONS = {
|
|
"DATE", "DATETIME", "DATEADD", "DATEDIFF", "DATENAME", "DATEPARSE",
|
|
"DATEPART", "DATETRUNC", "DAY", "MONTH", "YEAR", "TODAY", "NOW",
|
|
"MAKEDATE", "MAKEDATETIME", "MAKETIME"
|
|
}
|
|
|
|
# String functions
|
|
STRING_FUNCTIONS = {
|
|
"ASCII", "CHAR", "CONTAINS", "ENDSWITH", "FIND", "FINDNTH",
|
|
"LEFT", "LEN", "LOWER", "LTRIM", "MID", "PROPER", "REGEXP_EXTRACT",
|
|
"REGEXP_EXTRACT_NTH", "REGEXP_MATCH", "REGEXP_REPLACE", "REPLACE",
|
|
"RIGHT", "RTRIM", "SPACE", "SPLIT", "STARTSWITH", "STR", "TRIM", "UPPER"
|
|
}
|
|
|
|
# Logical functions
|
|
LOGICAL_FUNCTIONS = {
|
|
"IF", "ELSEIF", "ELSE", "END", "AND", "OR", "NOT", "IFNULL", "IIF",
|
|
"ISNULL", "ISDATE", "CASE", "WHEN", "THEN"
|
|
}
|
|
|
|
# Mathematical functions
|
|
MATH_FUNCTIONS = {
|
|
"ABS", "ACOS", "ASIN", "ATAN", "ATAN2", "CEILING", "COS", "COT",
|
|
"DEGREES", "DIV", "EXP", "FLOOR", "FLOAT", "INT", "LN", "LOG",
|
|
"PI", "POWER", "RADIANS", "ROUND", "SIGN", "SIN", "SQRT", "SQUARE",
|
|
"TAN", "ZN"
|
|
}
|
|
|
|
# Window/Analytic functions
|
|
WINDOW_FUNCTIONS = {
|
|
"LOOKUP", "LAST_VALUE", "RANK", "RANK_DENSE", "RANK_MODIFIED",
|
|
"RANK_PERCENTILE", "ROW_NUMBER", "RUNNING_AVG", "RUNNING_SUM", "NTILE"
|
|
}
|
|
|
|
# LOD/Analytic keywords
|
|
LOD_KEYWORDS = {
|
|
"PARTITION", "ORDERBY", "ASC", "DESC"
|
|
}
|
|
|
|
# Type conversion functions
|
|
TYPE_FUNCTIONS = {
|
|
"FLOAT", "INT", "STR", "DATE", "DATETIME"
|
|
}
|
|
|
|
# All valid functions
|
|
ALL_FUNCTIONS = (
|
|
AGGREGATION_FUNCTIONS | LOD_FUNCTIONS | DATE_FUNCTIONS |
|
|
STRING_FUNCTIONS | LOGICAL_FUNCTIONS | MATH_FUNCTIONS |
|
|
WINDOW_FUNCTIONS | TYPE_FUNCTIONS
|
|
)
|
|
|
|
# Function categories for documentation
|
|
FUNCTION_CATEGORIES = {
|
|
"Aggregation": AGGREGATION_FUNCTIONS,
|
|
"LOD": LOD_FUNCTIONS,
|
|
"Date": DATE_FUNCTIONS,
|
|
"String": STRING_FUNCTIONS,
|
|
"Logical": LOGICAL_FUNCTIONS,
|
|
"Math": MATH_FUNCTIONS,
|
|
"Window": WINDOW_FUNCTIONS,
|
|
"Type": TYPE_FUNCTIONS,
|
|
}
|
|
|
|
|
|
def extract_functions(expression: str) -> Set[str]:
|
|
"""Extract function names from a Tableau expression.
|
|
|
|
Args:
|
|
expression: Tableau formula expression
|
|
|
|
Returns:
|
|
Set of function names found in the expression
|
|
"""
|
|
# Pattern to match function calls: FUNCTION_NAME(
|
|
pattern = r'\b([A-Z][A-Z0-9_]*)\s*\('
|
|
matches = re.findall(pattern, expression.upper())
|
|
return set(matches)
|
|
|
|
|
|
def validate_functions(expression: str) -> Tuple[bool, List[str], List[str]]:
|
|
"""Validate that all functions in expression are valid Tableau functions.
|
|
|
|
Args:
|
|
expression: Tableau formula expression
|
|
|
|
Returns:
|
|
(is_valid, invalid_functions, suggestions)
|
|
"""
|
|
found_functions = extract_functions(expression)
|
|
invalid = [f for f in found_functions if f not in ALL_FUNCTIONS]
|
|
|
|
suggestions = []
|
|
for invalid_func in invalid:
|
|
# Find similar function names
|
|
similar = [
|
|
func for func in ALL_FUNCTIONS
|
|
if invalid_func in func or func.startswith(invalid_func[:3])
|
|
]
|
|
if similar:
|
|
suggestions.append(f"{invalid_func} -> {', '.join(similar[:3])}")
|
|
|
|
return len(invalid) == 0, invalid, suggestions
|
|
|
|
|
|
def get_function_category(function: str) -> Optional[str]:
|
|
"""Get the category of a function.
|
|
|
|
Args:
|
|
function: Function name (case-insensitive)
|
|
|
|
Returns:
|
|
Category name or None if not found
|
|
"""
|
|
func_upper = function.upper()
|
|
for category, functions in FUNCTION_CATEGORIES.items():
|
|
if func_upper in functions:
|
|
return category
|
|
return None
|
|
|
|
|
|
def has_aggregation_function(expression: str) -> bool:
|
|
"""Check if expression contains aggregation functions.
|
|
|
|
Args:
|
|
expression: Tableau formula expression
|
|
|
|
Returns:
|
|
True if expression contains aggregation functions
|
|
"""
|
|
found = extract_functions(expression)
|
|
return bool(found & AGGREGATION_FUNCTIONS)
|
|
|
|
|
|
def suggest_functions(partial: str, limit: int = 5) -> List[str]:
|
|
"""Suggest function names matching a partial string.
|
|
|
|
Args:
|
|
partial: Partial function name
|
|
limit: Maximum number of suggestions
|
|
|
|
Returns:
|
|
List of matching function names
|
|
"""
|
|
partial_upper = partial.upper()
|
|
matches = [
|
|
func for func in ALL_FUNCTIONS
|
|
if func.startswith(partial_upper) or partial_upper in func
|
|
]
|
|
return sorted(matches)[:limit]
|
|
|
|
|
|
def get_function_examples() -> Dict[str, str]:
|
|
"""Get example usage for common functions.
|
|
|
|
Returns:
|
|
Dictionary mapping function names to example expressions
|
|
"""
|
|
return {
|
|
"SUM": "SUM([Amount])",
|
|
"AVG": "AVG([Price])",
|
|
"COUNTD": "COUNTD([Customer_ID])",
|
|
"IF": "IF [Profit] > 0 THEN 'Profitable' ELSE 'Loss' END",
|
|
"CASE": "CASE [Stage] WHEN 'Won' THEN 1 WHEN 'Lost' THEN 0 ELSE NULL END",
|
|
"DATEDIFF": "DATEDIFF('day', [Start_Date], [End_Date])",
|
|
"CONTAINS": "CONTAINS([Name], 'Tech')",
|
|
"LEFT": "LEFT([Name], 4)",
|
|
"UPPER": "UPPER([Name])",
|
|
"FIXED": "{FIXED [Region]: SUM([Sales])}",
|
|
}
|