Code Library

Linguagem

M

Itens reutilizáveis de M organizados por tipo de objeto e categoria/biblioteca. Extensão canônica .pq.

Itens
21
Categorias
7
Exibindo
21

Data e hora

7

fnFeriados

Tabela com os feriados nacionais e os pontos facultativos federais do Brasil no ano informado.

UDFv2.0
let
    fnFeriados = (y as number) as table =>
        let
            // Domingo de Páscoa (gregoriano)
            a = Number.Mod(y, 19),
            b = Number.IntegerDivide(y, 100),
            c = Number.Mod(y, 100),
            d = Number.IntegerDivide(b, 4),
            e = Number.Mod(b, 4),
            f = Number.IntegerDivide(b + 8, 25),
            g = Number.IntegerDivide(b - f + 1, 3),
            h = Number.Mod(19 * a + b - d - g + 15, 30),
            i = Number.IntegerDivide(c, 4),
            k = Number.Mod(c, 4),
            l = Number.Mod(32 + 2 * e + 2 * i - h - k, 7),
            m = Number.IntegerDivide(a + 11 * h + 22 * l, 451),
            vPascoa = #date(y, Number.IntegerDivide(h + l - 7 * m + 114, 31), Number.Mod(h + l - 7 * m + 114, 31) + 1),

            vFixos = {
                {#date(y, 1, 1), "Confraternização Universal", "Feriado nacional"},
                {#date(y, 4, 21), "Tiradentes", "Feriado nacional"},
                {#date(y, 5, 1), "Dia do Trabalho", "Feriado nacional"},
                {#date(y, 9, 7), "Independência do Brasil", "Feriado nacional"},
                {#date(y, 10, 12), "Nossa Senhora Aparecida", "Feriado nacional"},
                {#date(y, 11, 2), "Finados", "Feriado nacional"},
                {#date(y, 11, 15), "Proclamação da República", "Feriado nacional"},
                {#date(y, 12, 25), "Natal", "Feriado nacional"}
            }
            & (if y >= 2024 then {{#date(y, 11, 20), "Dia Nacional de Zumbi e da Consciência Negra", "Feriado nacional"}} else {}),

            vMoveis = {
                {Date.AddDays(vPascoa, -48), "Carnaval (segunda-feira)", "Ponto facultativo"},
                {Date.AddDays(vPascoa, -47), "Carnaval (terça-feira)", "Ponto facultativo"},
                {Date.AddDays(vPascoa, -2), "Paixão de Cristo", "Feriado nacional"},
                {vPascoa, "Páscoa", "Data comemorativa"},
                {Date.AddDays(vPascoa, 60), "Corpus Christi", "Ponto facultativo"}
            },

            vTabela = #table(type table [Data = date, Feriado = text, Tipo = text], vFixos & vMoveis)
        in
            Table.Sort(vTabela, {{"Data", Order.Ascending}}),

    Documentacao = [
        Documentation.Name = "fnFeriados",
        Documentation.Description = "Feriados nacionais e pontos facultativos federais do ano informado.",
        Documentation.LongDescription = "Colunas Data, Feriado e Tipo (Feriado nacional, Ponto facultativo ou Data comemorativa). Móveis calculados a partir da Páscoa. Consciência Negra incluída a partir de 2024.",
        Documentation.Category = "Data e hora",
        Documentation.Author = "Laennder Alves, adaptado por Paulo André Jesus",
        Documentation.Version = "2.0",
        Documentation.Examples = {
            [Description = "Feriados de 2026", Code = "fnFeriados(2026)", Result = "Tabela com 14 linhas; Paixão de Cristo em 03/04/2026 e Corpus Christi em 04/06/2026"]
        }
    ]
in
    Value.ReplaceType(fnFeriados, Value.ReplaceMetadata(Value.Type(fnFeriados), Documentacao))

fnHolidaysInPeriodEN

Feriados federais dos EUA entre duas datas, com a data de folga observada.

UDFv2.0
let
    fnHolidaysInPeriodEN = (DataInicial as date, DataFinal as date) as table =>
        let
            // Enésima ocorrência de um dia da semana no mês (0 = segunda ... 6 = domingo)
            fnEnesimo = (ano as number, mes as number, dia as number, n as number) as date =>
                let
                    vPrimeiro = #date(ano, mes, 1),
                    vDesloc = Number.Mod(dia - Date.DayOfWeek(vPrimeiro, Day.Monday) + 7, 7)
                in
                    Date.AddDays(vPrimeiro, vDesloc + 7 * (n - 1)),
            // Última ocorrência de um dia da semana no mês
            fnUltimo = (ano as number, mes as number, dia as number) as date =>
                let
                    vUltimo = Date.EndOfMonth(#date(ano, mes, 1)),
                    vDesloc = Number.Mod(Date.DayOfWeek(vUltimo, Day.Monday) - dia + 7, 7)
                in
                    Date.AddDays(vUltimo, -vDesloc),
            // Data fixa no sábado → folga na sexta; no domingo → folga na segunda
            fnObservado = (d as date) as date =>
                let
                    vDia = Date.DayOfWeek(d, Day.Monday)
                in
                    if vDia = 5 then Date.AddDays(d, -1) else if vDia = 6 then Date.AddDays(d, 1) else d,
            // Linhas {DateReference, HolidayEN, ObservedDate} de um ano
            fnAno = (y as number) as list =>
                let
                    vFixos =
                        {{#date(y, 1, 1), "New Year's Day"}}
                        & (if y >= 2021 then {{#date(y, 6, 19), "Juneteenth National Independence Day"}} else {})
                        & {
                            {#date(y, 7, 4), "Independence Day"},
                            {#date(y, 11, 11), "Veterans Day"},
                            {#date(y, 12, 25), "Christmas Day"}
                        },
                    vMoveis = {
                        {fnEnesimo(y, 1, 0, 3), "Martin Luther King Jr. Day"},
                        {fnEnesimo(y, 2, 0, 3), "Presidents' Day"},
                        {fnUltimo(y, 5, 0), "Memorial Day"},
                        {fnEnesimo(y, 9, 0, 1), "Labor Day"},
                        {fnEnesimo(y, 10, 0, 2), "Columbus Day"},
                        {fnEnesimo(y, 11, 3, 4), "Thanksgiving Day"}
                    }
                in
                    List.Transform(vFixos, each {_{0}, _{1}, fnObservado(_{0})})
                    & List.Transform(vMoveis, each {_{0}, _{1}, _{0}}),
            vAnos = if DataFinal < DataInicial then {} else {Date.Year(DataInicial) .. Date.Year(DataFinal)},
            vTabela = #table(
                type table [DateReference = date, HolidayEN = text, ObservedDate = date],
                List.Combine(List.Transform(vAnos, fnAno))
            ),
            vPeriodo = Table.SelectRows(vTabela, each [DateReference] >= DataInicial and [DateReference] <= DataFinal)
        in
            Table.Sort(vPeriodo, {{"DateReference", Order.Ascending}}),

    Documentacao = [
        Documentation.Name = "fnHolidaysInPeriodEN",
        Documentation.Description = "Feriados federais dos EUA entre duas datas, com a data de folga observada.",
        Documentation.LongDescription = "Colunas DateReference, HolidayEN e ObservedDate. Feriado de data fixa no sábado é observado na sexta; no domingo, na segunda. Juneteenth a partir de 2021. Período sem feriado retorna tabela vazia com as colunas.",
        Documentation.Category = "Data e hora",
        Documentation.Author = "Paulo André Jesus",
        Documentation.Version = "2.0",
        Documentation.Examples = {
            [Description = "2026", Code = "fnHolidaysInPeriodEN(#date(2026, 1, 1), #date(2026, 12, 31))",
                Result = "11 linhas; Independence Day em 04/07/2026 (sábado) com ObservedDate 03/07/2026; Thanksgiving em 26/11/2026"]
        }
    ]
in
    Value.ReplaceType(fnHolidaysInPeriodEN, Value.ReplaceMetadata(Value.Type(fnHolidaysInPeriodEN), Documentacao))

fnHolidaysInPeriodFR

Feriados da França entre duas datas.

UDFv2.0
let
    fnHolidaysInPeriodFR = (StartDate as date, EndDate as date, optional AlsaceMoselle as nullable logical) as table =>
        let
            fnAno = (y as number) as list =>
                let
                    // Domingo de Páscoa: algoritmo gregoriano anônimo (Meeus/Jones/Butcher)
                    a = Number.Mod(y, 19),
                    b = Number.IntegerDivide(y, 100),
                    c = Number.Mod(y, 100),
                    d = Number.IntegerDivide(b, 4),
                    e = Number.Mod(b, 4),
                    f = Number.IntegerDivide(b + 8, 25),
                    g = Number.IntegerDivide(b - f + 1, 3),
                    h = Number.Mod(19 * a + b - d - g + 15, 30),
                    i = Number.IntegerDivide(c, 4),
                    k = Number.Mod(c, 4),
                    l = Number.Mod(32 + 2 * e + 2 * i - h - k, 7),
                    m = Number.IntegerDivide(a + 11 * h + 22 * l, 451),
                    vPaques = #date(y, Number.IntegerDivide(h + l - 7 * m + 114, 31), Number.Mod(h + l - 7 * m + 114, 31) + 1)
                in
                    {
                        {#date(y, 1, 1), "Jour de l'An"},
                        {Date.AddDays(vPaques, 1), "Lundi de Pâques"},
                        {#date(y, 5, 1), "Fête du Travail"},
                        {#date(y, 5, 8), "Victoire 1945"},
                        {Date.AddDays(vPaques, 39), "Ascension"},
                        {Date.AddDays(vPaques, 50), "Lundi de Pentecôte"},
                        {#date(y, 7, 14), "Fête Nationale"},
                        {#date(y, 8, 15), "Assomption"},
                        {#date(y, 11, 1), "Toussaint"},
                        {#date(y, 11, 11), "Armistice 1918"},
                        {#date(y, 12, 25), "Noël"}
                    }
                    & (if AlsaceMoselle = true
                        then {{Date.AddDays(vPaques, -2), "Vendredi saint"}, {#date(y, 12, 26), "Saint-Étienne"}}
                        else {}),
            vAnos = if EndDate < StartDate then {} else {Date.Year(StartDate) .. Date.Year(EndDate)},
            vTabela = #table(type table [DateReference = date, HolidayFR = text], List.Combine(List.Transform(vAnos, fnAno))),
            vPeriodo = Table.SelectRows(vTabela, each [DateReference] >= StartDate and [DateReference] <= EndDate)
        in
            Table.Sort(vPeriodo, {{"DateReference", Order.Ascending}}),

    Documentacao = [
        Documentation.Name = "fnHolidaysInPeriodFR",
        Documentation.Description = "Feriados da França entre duas datas.",
        Documentation.LongDescription = "Os 11 feriados do Code du travail (art. L3133-1); móveis calculados a partir da Páscoa. Com AlsaceMoselle = true, inclui Vendredi saint e Saint-Étienne. Colunas DateReference e HolidayFR.",
        Documentation.Category = "Data e hora",
        Documentation.Author = "Paulo André Jesus",
        Documentation.Version = "2.0",
        Documentation.Examples = {
            [Description = "2026", Code = "fnHolidaysInPeriodFR(#date(2026, 1, 1), #date(2026, 12, 31))",
                Result = "11 linhas; Lundi de Pâques 06/04, Ascension 14/05, Lundi de Pentecôte 25/05"],
            [Description = "Alsácia-Mosela", Code = "fnHolidaysInPeriodFR(#date(2026, 1, 1), #date(2026, 12, 31), true)", Result = "13 linhas"]
        }
    ]
in
    Value.ReplaceType(fnHolidaysInPeriodFR, Value.ReplaceMetadata(Value.Type(fnHolidaysInPeriodFR), Documentacao))

fnHolidaysInPeriodPT

Feriados nacionais e pontos facultativos do Brasil entre duas datas.

UDFv2.0
let
    fnHolidaysInPeriodPT = (StartDate as date, EndDate as date) as table =>
        let
            vAnos = if EndDate < StartDate then {} else {Date.Year(StartDate) .. Date.Year(EndDate)},
            // Tabela vazia tipada garante as colunas mesmo sem nenhum ano
            vTodos = Table.Combine(
                {#table(type table [Data = date, Feriado = text, Tipo = text], {})}
                & List.Transform(vAnos, fnFeriados)
            ),
            vPeriodo = Table.SelectRows(vTodos, each [Data] >= StartDate and [Data] <= EndDate),
            vRenomeado = Table.RenameColumns(vPeriodo, {{"Data", "DateReference"}, {"Feriado", "HolidayPT"}, {"Tipo", "HolidayType"}})
        in
            Table.Sort(vRenomeado, {{"DateReference", Order.Ascending}}),

    Documentacao = [
        Documentation.Name = "fnHolidaysInPeriodPT",
        Documentation.Description = "Feriados nacionais e pontos facultativos do Brasil entre duas datas.",
        Documentation.LongDescription = "Chama fnFeriados para cada ano do período e filtra as datas. Colunas DateReference, HolidayPT e HolidayType (Feriado nacional, Ponto facultativo ou Data comemorativa).",
        Documentation.Category = "Data e hora",
        Documentation.Author = "Laennder Alves, adaptado por Paulo André Jesus",
        Documentation.Version = "2.0",
        Documentation.Examples = {
            [Description = "2024 a 2026", Code = "fnHolidaysInPeriodPT(#date(2024, 1, 1), #date(2026, 12, 31))", Result = "42 linhas (14 por ano)"],
            [Description = "Só feriados nacionais", Code = "Table.SelectRows(fnHolidaysInPeriodPT(#date(2026, 1, 1), #date(2026, 12, 31)), each [HolidayType] = ""Feriado nacional"")", Result = "10 linhas"]
        }
    ]
in
    Value.ReplaceType(fnHolidaysInPeriodPT, Value.ReplaceMetadata(Value.Type(fnHolidaysInPeriodPT), Documentacao))

fnHorasEntreDatas

Lista de datas/horas de hora em hora, de Start até End.

UDFv1.2
let
    fnHorasEntreDatas = (Start as datetime, End as datetime) as list =>
        if End < Start then {}
        else List.Transform(
            {0 .. Number.RoundDown(Duration.TotalHours(End - Start))},
            each Start + #duration(0, _, 0, 0)
        ),

    Documentacao = [
        Documentation.Name = "fnHorasEntreDatas",
        Documentation.Description = "Lista de datas/horas de hora em hora entre Start e End.",
        Documentation.LongDescription = "Começa em Start e soma uma hora por item enquanto não passar de End (inclusive). End anterior a Start retorna lista vazia.",
        Documentation.Category = "Data e hora",
        Documentation.Source = "https://github.com/ImkeF/M",
        Documentation.Author = "Imke Feldmann, adaptado por Paulo André Jesus",
        Documentation.Version = "1.2",
        Documentation.Examples = {
            [Description = "Das 1:00 às 4:29", Code = "fnHorasEntreDatas(#datetime(2018, 7, 8, 1, 0, 0), #datetime(2018, 7, 8, 4, 29, 0))",
                Result = "{#datetime(2018, 7, 8, 1, 0, 0), #datetime(2018, 7, 8, 2, 0, 0), #datetime(2018, 7, 8, 3, 0, 0), #datetime(2018, 7, 8, 4, 0, 0)}"],
            [Description = "24 horas exatas: 25 itens, inclui o fim", Code = "fnHorasEntreDatas(#datetime(2025, 2, 1, 0, 0, 0), #datetime(2025, 2, 2, 0, 0, 0))",
                Result = "{#datetime(2025, 2, 1, 0, 0, 0), ..., #datetime(2025, 2, 2, 0, 0, 0)}"]
        }
    ]
in
    Value.ReplaceType(fnHorasEntreDatas, Value.ReplaceMetadata(Value.Type(fnHorasEntreDatas), Documentacao))

fnLastWeekday

Última ocorrência de um dia da semana num mês.

UDFv1.1
let
    fnLastWeekday = (Ano as number, Mes as number, DiaDaSemana as number) as nullable date =>
        let
            vUltimoDia = Date.EndOfMonth(#date(Ano, Mes, 1)),
            // Dias a voltar a partir do último dia do mês (0 a 6); 0 = segunda ... 6 = domingo
            vDesloc = Number.Mod(Date.DayOfWeek(vUltimoDia, Day.Monday) - DiaDaSemana + 7, 7)
        in
            // Dia fora de 0-6 → null
            if not List.Contains({0 .. 6}, DiaDaSemana) then null
            else Date.AddDays(vUltimoDia, -vDesloc),

    Documentacao = [
        Documentation.Name = "fnLastWeekday",
        Documentation.Description = "Última ocorrência de um dia da semana num mês.",
        Documentation.LongDescription = "DiaDaSemana vai de 0 (segunda) a 6 (domingo), a mesma numeração de Date.DayOfWeek(data, Day.Monday). Retorna null quando DiaDaSemana está fora de 0-6.",
        Documentation.Category = "Data e hora",
        Documentation.Author = "Paulo André Jesus",
        Documentation.Version = "1.1",
        Documentation.Examples = {
            [Description = "Última sexta-feira de março de 2025", Code = "fnLastWeekday(2025, 3, 4)", Result = "#date(2025, 3, 28)"],
            [Description = "Última segunda-feira de novembro de 2024", Code = "fnLastWeekday(2024, 11, 0)", Result = "#date(2024, 11, 25)"],
            [Description = "Último domingo de fevereiro de 2026", Code = "fnLastWeekday(2026, 2, 6)", Result = "#date(2026, 2, 22)"]
        }
    ]
in
    Value.ReplaceType(fnLastWeekday, Value.ReplaceMetadata(Value.Type(fnLastWeekday), Documentacao))

fnNthWeekday

Enésima ocorrência de um dia da semana num mês.

UDFv1.1
let
    fnNthWeekday = (Ano as number, Mes as number, DiaDaSemana as number, N as number) as nullable date =>
        let
            vPrimeiroDia = #date(Ano, Mes, 1),
            // Dias até o primeiro DiaDaSemana do mês (0 a 6); 0 = segunda ... 6 = domingo
            vDesloc = Number.Mod(DiaDaSemana - Date.DayOfWeek(vPrimeiroDia, Day.Monday) + 7, 7),
            vData = Date.AddDays(vPrimeiroDia, vDesloc + 7 * (N - 1))
        in
            // N inválido, dia fora de 0-6 ou ocorrência que cai no mês seguinte → null
            if N < 1 or not List.Contains({0 .. 6}, DiaDaSemana) or Date.Month(vData) <> Mes then null
            else vData,

    Documentacao = [
        Documentation.Name = "fnNthWeekday",
        Documentation.Description = "Enésima ocorrência de um dia da semana num mês.",
        Documentation.LongDescription = "DiaDaSemana vai de 0 (segunda) a 6 (domingo), a mesma numeração de Date.DayOfWeek(data, Day.Monday). Retorna null quando a ocorrência não existe no mês, quando N < 1 ou quando DiaDaSemana está fora de 0-6.",
        Documentation.Category = "Data e hora",
        Documentation.Author = "Paulo André Jesus",
        Documentation.Version = "1.1",
        Documentation.Examples = {
            [Description = "3ª segunda-feira de janeiro de 2025", Code = "fnNthWeekday(2025, 1, 0, 3)", Result = "#date(2025, 1, 20)"],
            [Description = "2ª sexta-feira de julho de 2024", Code = "fnNthWeekday(2024, 7, 4, 2)", Result = "#date(2024, 7, 12)"],
            [Description = "5ª segunda-feira de fevereiro de 2025 (não existe)", Code = "fnNthWeekday(2025, 2, 0, 5)", Result = "null"]
        }
    ]
in
    Value.ReplaceType(fnNthWeekday, Value.ReplaceMetadata(Value.Type(fnNthWeekday), Documentacao))

Formatação

1

fnFormatWithLeadingZeros

Completa um número ou texto com zeros à esquerda até o tamanho informado.

UDFv1.4
let
    fnFormatWithLeadingZeros = (pNumero as any, pNumeroDeCaracteres as nullable number) as nullable text =>
        let
            // Tamanho mínimo 1, sempre inteiro
            vTamanho = Int64.From(Number.RoundDown(List.Max({1, Number.Abs(pNumeroDeCaracteres ?? 1)}))),
            // Número vira texto sem sinal (cultura fixa: 1.5 não vira "1,5"); o resto vira texto sem espaços
            vTexto =
                if pNumero = null then null
                else if pNumero is number then Text.From(Number.Abs(pNumero), "en-US")
                else if pNumero is text then Text.Trim(pNumero)
                else Text.Trim(Text.From(pNumero)),
            // Texto vazio ou "#" vira null; "-" inicial é removido
            vLimpo =
                if vTexto = null or vTexto = "" or vTexto = "#" then null
                else if Text.StartsWith(vTexto, "-") then Text.End(vTexto, Text.Length(vTexto) - 1)
                else vTexto
        in
            if vLimpo = null then null else Text.PadStart(vLimpo, vTamanho, "0"),

    Documentacao = [
        Documentation.Name = "fnFormatWithLeadingZeros",
        Documentation.Description = "Completa um número ou texto com zeros à esquerda.",
        Documentation.LongDescription = "Recebe um número ou texto e o tamanho desejado. Descarta o sinal negativo. Tamanho mínimo 1. Valor nulo, vazio ou ""#"" retorna null.",
        Documentation.Category = "Formatação",
        Documentation.Author = "Paulo André Jesus",
        Documentation.Version = "1.4",
        Documentation.Examples = {
            [Description = "Número", Code = "fnFormatWithLeadingZeros(25, 5)", Result = """00025"""],
            [Description = "Número negativo", Code = "fnFormatWithLeadingZeros(-7, 3)", Result = """007"""],
            [Description = "Texto já com zeros", Code = "fnFormatWithLeadingZeros(""0012"", 6)", Result = """000012"""],
            [Description = "Maior que o tamanho", Code = "fnFormatWithLeadingZeros(12345, 3)", Result = """12345"""]
        }
    ]
in
    Value.ReplaceType(fnFormatWithLeadingZeros, Value.ReplaceMetadata(Value.Type(fnFormatWithLeadingZeros), Documentacao))

Geografia

1

fnGrausParaDecimais

Converte coordenada em graus, minutos e segundos para grau decimal.

UDFv2.0
let
    fnGrausParaDecimais = (graus as nullable text) as nullable number =>
        let
            // Padroniza símbolos: ′ ’ → '   ″ ” '' → "   vírgula → ponto
            vTexto = Text.Upper(Text.Trim(List.Accumulate(
                {{"′", "'"}, {"’", "'"}, {"″", """"}, {"”", """"}, {"''", """"}, {",", "."}},
                graus ?? "",
                (t, par) => Text.Replace(t, par{0}, par{1})
            ))),
            fnNumero = (t as text) as number => Number.FromText(Text.Trim(t), "en-US"),
            vGraus = Number.Abs(fnNumero(Text.BeforeDelimiter(vTexto, "°"))),
            vMinutos = fnNumero(Text.BetweenDelimiters(vTexto, "°", "'")),
            vSegundos = fnNumero(Text.BetweenDelimiters(vTexto, "'", """")),
            vNegativo = List.Contains({"S", "W", "O"}, Text.End(vTexto, 1)) or Text.StartsWith(vTexto, "-")
        in
            if graus = null then null
            else (vGraus + vMinutos / 60 + vSegundos / 3600) * (if vNegativo then -1 else 1),

    Documentacao = [
        Documentation.Name = "fnGrausParaDecimais",
        Documentation.Description = "Converte coordenada em graus, minutos e segundos para grau decimal.",
        Documentation.LongDescription = "Formato esperado: G°M'S""D, com D = N, S, E, L, W ou O. S, W, O ou sinal negativo geram valor negativo. Lê números com ponto ou vírgula decimal, independente da cultura do arquivo.",
        Documentation.Category = "Geografia",
        Documentation.Author = "Paulo André Jesus",
        Documentation.Version = "2.0",
        Documentation.Examples = {
            [Description = "Latitude sul", Code = "fnGrausParaDecimais(""23°34'45""""S"")", Result = "-23.5791666666667"],
            [Description = "Longitude oeste", Code = "fnGrausParaDecimais(""46°37'14""""W"")", Result = "-46.6205555555556"],
            [Description = "Latitude norte", Code = "fnGrausParaDecimais(""12°15'30""""N"")", Result = "12.2583333333333"]
        }
    ]
in
    Value.ReplaceType(fnGrausParaDecimais, Value.ReplaceMetadata(Value.Type(fnGrausParaDecimais), Documentacao))

Máscara de dados

3

fnDocumentoAnonimo

Mascara CPF e CNPJ, mantendo o início e os dígitos verificadores.

UDFv2.0
let
    fnDocumentoAnonimo = (documento as nullable text) as nullable text =>
        let
            vLimpo = Text.Select(Text.Upper(documento ?? ""), {"0".."9", "A".."Z"})
        in
            if Text.Length(vLimpo) = 11 then Text.Start(vLimpo, 3) & ".***.***-" & Text.End(vLimpo, 2)
            else if Text.Length(vLimpo) = 14 then Text.Start(vLimpo, 2) & ".***.***/****-" & Text.End(vLimpo, 2)
            else null,

    Documentacao = [
        Documentation.Name = "fnDocumentoAnonimo",
        Documentation.Description = "Mascara CPF e CNPJ, mantendo o início e os dígitos verificadores.",
        Documentation.LongDescription = "11 caracteres = CPF; 14 = CNPJ (numérico ou alfanumérico). Aceita entrada formatada ou não. Outros tamanhos retornam null.",
        Documentation.Category = "Máscara de dados",
        Documentation.Author = "Paulo André Jesus",
        Documentation.Version = "2.0",
        Documentation.Examples = {
            [Description = "CPF", Code = "fnDocumentoAnonimo(""123.456.789-00"")", Result = """123.***.***-00"""],
            [Description = "CNPJ alfanumérico", Code = "fnDocumentoAnonimo(""12.ABC.345/01DE-35"")", Result = """12.***.***/****-35"""]
        }
    ]
in
    Value.ReplaceType(fnDocumentoAnonimo, Value.ReplaceMetadata(Value.Type(fnDocumentoAnonimo), Documentacao))

fnEmailAnonimo

Mascara um e-mail trocando por * uma parte dos caracteres da parte local e do nome do domínio, em posições pseudoaleatórias e estáveis entre refreshes.

UDFv1.2
let
    fnEmailAnonimo = (email as nullable text, optional proporcao as nullable number) as nullable text =>
        let
            vEmail = Text.Lower(Text.Trim(email ?? "")),
            vPartes = Text.Split(vEmail, "@"),
            // Exatamente um "@", parte local preenchida e domínio com ponto (não no início)
            // "and" para no primeiro false: vPartes{1} só é lido quando há 2 partes
            vValido = List.Count(vPartes) = 2 and vPartes{0} <> "" and Text.PositionOf(vPartes{1}, ".") > 0,
            // Proporção por parte: padrão 50%, limitada entre 10% e 100%
            vProporcao = List.Max({0.1, List.Min({1, proporcao ?? 0.5})}),

            // Mascara um trecho mantendo o 1º caractere e os separadores (. - _ +).
            // A semente vem do PRÓPRIO trecho: o mesmo domínio recebe sempre a mesma máscara,
            // e comparar linhas não revela as letras escondidas.
            fnMascarar = (trecho as text) as text =>
                let
                    vChars = List.Buffer(Text.ToList(trecho)),
                    vElegiveis = List.Select(
                        List.Numbers(1, List.Count(vChars) - 1),
                        each not List.Contains({".", "-", "_", "+"}, vChars{_})
                    ),
                    vTotal = List.Count(vElegiveis),
                    vQtd = List.Min({vTotal, List.Max({1, Number.RoundUp(vTotal * vProporcao)})}),
                    vSemente = Number.Mod(
                        List.Sum(List.Transform(List.Positions(vChars), each Character.ToNumber(vChars{_}) * (_ + 1))),
                        2147483647
                    ),
                    // Embaralha as posições elegíveis e pega as vQtd primeiras
                    vSorteados = List.Buffer(
                        List.Transform(
                            List.FirstN(List.Sort(List.Zip({vElegiveis, List.Random(vTotal, vSemente)}), each _{1}), vQtd),
                            each _{0}
                        )
                    )
                in
                    if vTotal = 0 then trecho   // nada a mascarar (ex.: "a")
                    else Text.Combine(
                        List.Transform(List.Positions(vChars), each if List.Contains(vSorteados, _) then "*" else vChars{_})
                    ),

            vLocal = vPartes{0},
            vDominio = vPartes{1},
            vPosPonto = Text.PositionOf(vDominio, "."),
            vNomeDominio = Text.Start(vDominio, vPosPonto),   // "empresa" em "empresa.com.br"
            vSufixo = Text.Middle(vDominio, vPosPonto)         // ".com.br" fica visível
        in
            if not vValido then null
            else fnMascarar(vLocal) & "@" & fnMascarar(vNomeDominio) & vSufixo,

    fnEmailAnonimoTipo = type function (
        email as (type nullable text meta [
            Documentation.FieldCaption = "E-mail",
            Documentation.FieldDescription = "Endereço a mascarar. Espaços nas pontas são removidos; e-mail inválido retorna null."
        ]),
        optional proporcao as (type nullable number meta [
            Documentation.FieldCaption = "Proporção",
            Documentation.FieldDescription = "Fração dos caracteres elegíveis de cada parte que vira * (0.1 a 1; padrão 0.5)."
        ])
    ) as nullable text meta [
        Documentation.Name = "fnEmailAnonimo",
        Documentation.Description = "Mascara um e-mail substituindo caracteres por * em posições pseudoaleatórias.",
        Documentation.LongDescription = "Mascara separadamente a parte local e o nome do domínio, cada um com semente derivada do próprio trecho: o mesmo domínio recebe sempre a mesma máscara, e o resultado é estável entre refreshes. Mantém o 1º caractere de cada parte, o @, os separadores (. - _ +) e o sufixo após o primeiro ponto do domínio. A saída é em minúsculas. E-mail inválido retorna null.",
        Documentation.Category = "Máscara de dados",
        Documentation.Author = "Paulo André Jesus",
        Documentation.Version = "1.2",
        Documentation.Examples = {
            [Description = "Padrão (50%)", Code = "fnEmailAnonimo(""paulo.jesus@officetuning.com.br"")", Result = "Metade dos caracteres elegíveis de cada parte vira *; o domínio sai igual em todas as linhas"],
            [Description = "Máscara mais forte", Code = "fnEmailAnonimo(""paulo.jesus@officetuning.com.br"", 0.8)", Result = "80% dos caracteres elegíveis de cada parte viram *"],
            [Description = "Nada elegível", Code = "fnEmailAnonimo(""a@b.com"")", Result = """a@b.com"""],
            [Description = "Inválido", Code = "fnEmailAnonimo(""paulo.officetuning.com"")", Result = "null"]
        }
    ]
in
    Value.ReplaceType(fnEmailAnonimo, fnEmailAnonimoTipo)

fnTelefoneAnonimo

Mascara telefone fixo ou celular brasileiro, mantendo o DDD, os dois primeiros e os dois últimos dígitos. Aceita código do país (55), prefixo de discagem (0) e código de operadora.

UDFv2.1
let
    fnTelefoneAnonimo = (telefone as nullable text) as nullable text =>
        let
            vDigitos = Text.Select(telefone ?? "", {"0".."9"}),
            vTam = Text.Length(vDigitos),
            // Código do país: 55 + 10 dígitos (fixo) ou 55 + 11 (celular)
            vSemPais =
                if List.Contains({12, 13}, vTam) and Text.StartsWith(vDigitos, "55")
                then Text.End(vDigitos, vTam - 2)
                else vDigitos,
            vTamSemPais = Text.Length(vSemPais),
            // DDD nunca começa com 0: um 0 inicial é prefixo de discagem.
            // 0 + DDD + número = 11 ou 12 dígitos; 0 + operadora (2) + DDD + número = 13 ou 14
            vNacional =
                if Text.StartsWith(vSemPais, "0") and List.Contains({11, 12}, vTamSemPais)
                then Text.End(vSemPais, vTamSemPais - 1)
                else if Text.StartsWith(vSemPais, "0") and List.Contains({13, 14}, vTamSemPais)
                then Text.End(vSemPais, vTamSemPais - 3)
                else vSemPais,
            vDDD = Text.Start(vNacional, 2),
            vInicio = Text.Middle(vNacional, 2, 2),
            vFim = Text.End(vNacional, 2)
        in
            if Text.StartsWith(vNacional, "0") then null   // DDD inválido
            else if Text.Length(vNacional) = 10 then "(" & vDDD & ") " & vInicio & "**-**" & vFim
            else if Text.Length(vNacional) = 11 then "(" & vDDD & ") " & vInicio & "***-**" & vFim
            else null,

    fnTelefoneAnonimoTipo = type function (
        telefone as (type nullable text meta [
            Documentation.FieldCaption = "Telefone",
            Documentation.FieldDescription = "Telefone em qualquer formatação. Aceita +55, prefixo 0 e código de operadora; outros tamanhos retornam null."
        ])
    ) as nullable text meta [
        Documentation.Name = "fnTelefoneAnonimo",
        Documentation.Description = "Mascara telefone fixo ou celular, mantendo DDD, dois dígitos iniciais e dois finais.",
        Documentation.LongDescription = "Considera só os dígitos da entrada. Descarta o código do país 55, o prefixo de discagem 0 e o código de operadora (0 + 2 dígitos). Depois disso, 10 dígitos = fixo e 11 = celular; outros tamanhos, ou DDD começando com 0, retornam null.",
        Documentation.Category = "Máscara de dados",
        Documentation.Author = "Paulo André Jesus",
        Documentation.Version = "2.1",
        Documentation.Examples = {
            [Description = "Fixo", Code = "fnTelefoneAnonimo(""(11) 3221-1234"")", Result = """(11) 32**-**34"""],
            [Description = "Celular com país", Code = "fnTelefoneAnonimo(""+55 21 98765-4321"")", Result = """(21) 98***-**21"""],
            [Description = "Prefixo 0", Code = "fnTelefoneAnonimo(""(021) 3221-1234"")", Result = """(21) 32**-**34"""],
            [Description = "Código de operadora", Code = "fnTelefoneAnonimo(""0 15 11 98765-4321"")", Result = """(11) 98***-**21"""],
            [Description = "Inválido", Code = "fnTelefoneAnonimo(""123456"")", Result = "null"]
        }
    ]
in
    Value.ReplaceType(fnTelefoneAnonimo, fnTelefoneAnonimoTipo)

Saúde

1

fnClassificacaoIMC

Classifica o IMC pelas faixas da OMS, em código ordenável ou pelo nome da faixa.

UDFv2.0
let
    fnClassificacaoIMC = (IMC as nullable number, optional Rotulo as nullable logical) as nullable text =>
        let
            // {limite superior (exclusivo), código, rótulo}
            vFaixas = {
                {17, "01-7", "Magreza moderada a grave"},
                {18.5, "02-6", "Magreza leve"},
                {25, "03-5", "Peso normal"},
                {30, "04-4", "Pré-obesidade"},
                {35, "05-3", "Obesidade grau I"},
                {40, "06-2", "Obesidade grau II"},
                {Number.PositiveInfinity, "07-1", "Obesidade grau III"}
            },
            vFaixa = List.First(List.Select(vFaixas, each IMC < _{0}), null)
        in
            if IMC = null or vFaixa = null then null
            else if Rotulo = true then vFaixa{2}
            else vFaixa{1},

    Documentacao = [
        Documentation.Name = "fnClassificacaoIMC",
        Documentation.Description = "Classifica o IMC pelas faixas da OMS.",
        Documentation.LongDescription = "Retorna o código XX-Y (XX sobe com o IMC; Y é a ordem invertida) ou, com Rotulo = true, o nome da faixa. IMC null retorna null.",
        Documentation.Category = "Saúde",
        Documentation.Author = "Paulo André Jesus",
        Documentation.Version = "2.0",
        Documentation.Examples = {
            [Description = "Código", Code = "fnClassificacaoIMC(22)", Result = """03-5"""],
            [Description = "Rótulo", Code = "fnClassificacaoIMC(37, true)", Result = """Obesidade grau II"""],
            [Description = "Nulo", Code = "fnClassificacaoIMC(null)", Result = "null"]
        }
    ]
in
    Value.ReplaceType(fnClassificacaoIMC, Value.ReplaceMetadata(Value.Type(fnClassificacaoIMC), Documentacao))

Texto

6

fnAddOrdinalSuffixEN

Adiciona o sufixo ordinal em inglês: 1st, 2nd, 3rd, 4th...

UDFv1.1
let
    fnAddOrdinalSuffixEN = (SingleNumber as nullable number) as nullable text =>
        let
            // O sinal não muda o sufixo: -1 → "-1st"
            vAbs = Number.Abs(SingleNumber),
            vDoisUltimos = Number.Mod(vAbs, 100),
            vUltimo = Number.Mod(vAbs, 10),
            // 11, 12 e 13 são exceção: sempre "th"
            vSufixo =
                if List.Contains({11, 12, 13}, vDoisUltimos) then "th"
                else if vUltimo = 1 then "st"
                else if vUltimo = 2 then "nd"
                else if vUltimo = 3 then "rd"
                else "th"
        in
            if SingleNumber = null then null else Text.From(SingleNumber, "en-US") & vSufixo,

    Documentacao = [
        Documentation.Name = "fnAddOrdinalSuffixEN",
        Documentation.Description = "Adiciona o sufixo ordinal em inglês (1st, 2nd, 3rd, 4th...).",
        Documentation.LongDescription = "Recebe um número inteiro e devolve o número com o sufixo ordinal do inglês. Números terminados em 11, 12 e 13 levam sempre 'th'. Entrada null retorna null.",
        Documentation.Category = "Texto",
        Documentation.Author = "Paulo André Jesus",
        Documentation.Version = "1.1",
        Documentation.Examples = {
            [Description = "Regra geral", Code = "fnAddOrdinalSuffixEN(22)", Result = """22nd"""],
            [Description = "Exceção", Code = "fnAddOrdinalSuffixEN(112)", Result = """112th"""],
            [Description = "Centena", Code = "fnAddOrdinalSuffixEN(101)", Result = """101st"""]
        }
    ]
in
    Value.ReplaceType(fnAddOrdinalSuffixEN, Value.ReplaceMetadata(Value.Type(fnAddOrdinalSuffixEN), Documentacao))

fnAddOrdinalSuffixFR

Adiciona o sufixo ordinal em francês: 1er/1re, 2e, 3e...

UDFv1.1
let
    fnAddOrdinalSuffixFR = (Numero as nullable number, optional Genero as nullable text) as nullable text =>
        let
            // "F", "f" ou "Féminin" → feminino; omitido ou qualquer outro valor → masculino
            vFeminino = Text.Upper(Text.Start(Genero ?? "M", 1)) = "F",
            vSufixo = if Numero = 1 then (if vFeminino then "re" else "er") else "e"
        in
            if Numero = null then null else Text.From(Numero, "fr-FR") & vSufixo,

    Documentacao = [
        Documentation.Name = "fnAddOrdinalSuffixFR",
        Documentation.Description = "Adiciona o sufixo ordinal em francês: 1er/1re, 2e, 3e...",
        Documentation.LongDescription = "Recebe um número e, opcionalmente, o gênero. O número 1 recebe er (masculino) ou re (feminino, Genero começando com F); os demais recebem e. Numero null retorna null.",
        Documentation.Category = "Texto",
        Documentation.Author = "Paulo André Jesus",
        Documentation.Version = "1.1",
        Documentation.Examples = {
            [Description = "1 masculino", Code = "fnAddOrdinalSuffixFR(1)", Result = """1er"""],
            [Description = "1 feminino", Code = "fnAddOrdinalSuffixFR(1, ""F"")", Result = """1re"""],
            [Description = "Semana do ano", Code = "fnAddOrdinalSuffixFR(38, ""F"") & "" semaine""", Result = """38e semaine"""]
        }
    ]
in
    Value.ReplaceType(fnAddOrdinalSuffixFR, Value.ReplaceMetadata(Value.Type(fnAddOrdinalSuffixFR), Documentacao))

fnAddOrdinalSuffixPT

Adiciona o indicador ordinal em português: º (masculino) ou ª (feminino).

UDFv1.1
let
    fnAddOrdinalSuffixPT = (Numero as nullable number, optional Genero as nullable text) as nullable text =>
        let
            // "F", "f" ou "Feminino" → ª; omitido ou qualquer outro valor → º
            vSufixo = if Text.Upper(Text.Start(Genero ?? "M", 1)) = "F" then "ª" else "º"
        in
            if Numero = null then null else Text.From(Numero, "pt-BR") & vSufixo,

    Documentacao = [
        Documentation.Name = "fnAddOrdinalSuffixPT",
        Documentation.Description = "Adiciona o indicador ordinal em português: º (masculino) ou ª (feminino).",
        Documentation.LongDescription = "Recebe um número e, opcionalmente, o gênero. Genero começando com F (F, f, Feminino) gera ª; omitido ou outro valor gera º. Numero null retorna null.",
        Documentation.Category = "Texto",
        Documentation.Author = "Paulo André Jesus",
        Documentation.Version = "1.1",
        Documentation.Examples = {
            [Description = "Masculino (padrão)", Code = "fnAddOrdinalSuffixPT(2)", Result = """2º"""],
            [Description = "Feminino", Code = "fnAddOrdinalSuffixPT(15, ""F"")", Result = """15ª"""],
            [Description = "Semana do ano", Code = "fnAddOrdinalSuffixPT(38, ""F"") & "" semana""", Result = """38ª semana"""]
        }
    ]
in
    Value.ReplaceType(fnAddOrdinalSuffixPT, Value.ReplaceMetadata(Value.Type(fnAddOrdinalSuffixPT), Documentacao))

fnFormataNome

Capitaliza nomes próprios respeitando partículas, hífens, apóstrofos e os prefixos Mc/Mac.

UDFv3.0
let
    fnFormataNome = (NomeCompleto as nullable text) as nullable text =>
        let
            // Partículas minúsculas quando não abrem o nome
            vParticulas = {
                "da", "das", "de", "do", "dos", "e",            // português
                "del", "la", "las", "lo", "los", "y",           // espanhol
                "di", "du",                                     // italiano, francês
                "von", "und", "zu", "der", "den",               // alemão
                "van", "ter", "ten",                            // neerlandês
                "bin", "bint", "ibn", "al", "ben", "bat"        // árabe, hebraico
            },
            // Sobrenomes com "Mac" + maiúscula (lista fechada)
            vMac = {"macarthur", "macdonald", "macgregor", "maclean", "macleod", "macmillan", "macneil", "macpherson"},
            fnCapitaliza = (p as text) as text =>
                if p = "" then "" else Text.Upper(Text.Start(p, 1)) & Text.End(p, Text.Length(p) - 1),
            // Formata um pedaço de palavra (entre hífens)
            fnParte = (p as text, primeira as logical) as text =>
                let
                    vApostrofo = if Text.Contains(p, "’") then "’" else if Text.Contains(p, "'") then "'" else null,
                    vPedacos = if vApostrofo = null then {p} else Text.Split(p, vApostrofo)
                in
                    if vApostrofo <> null and List.Count(vPedacos) = 2 then
                        fnCapitaliza(vPedacos{0}) & vApostrofo & fnCapitaliza(vPedacos{1})
                    else if not primeira and List.Contains(vParticulas, p) then p
                    else if Text.StartsWith(p, "mc") and Text.Length(p) > 2 then "Mc" & fnCapitaliza(Text.End(p, Text.Length(p) - 2))
                    else if List.Contains(vMac, p) then "Mac" & fnCapitaliza(Text.End(p, Text.Length(p) - 3))
                    else fnCapitaliza(p),
            // Espaços repetidos não geram palavra vazia
            vPalavras = List.Select(Text.SplitAny(Text.Lower(Text.Trim(NomeCompleto ?? "")), " #(tab)"), each _ <> ""),
            vFormatadas = List.Transform(
                List.Positions(vPalavras),
                (i) => Text.Combine(List.Transform(Text.Split(vPalavras{i}, "-"), (parte) => fnParte(parte, i = 0)), "-")
            )
        in
            if NomeCompleto = null then null else Text.Combine(vFormatadas, " "),

    Documentacao = [
        Documentation.Name = "fnFormataNome",
        Documentation.Description = "Capitaliza nomes próprios respeitando partículas, hífens, apóstrofos e Mc/Mac.",
        Documentation.LongDescription = "Partículas (de, da, del, van, von, bin...) ficam minúsculas, exceto na primeira palavra. Preserva o apóstrofo reto ou tipográfico. Mac só é tratado para os sobrenomes da lista vMac.",
        Documentation.Category = "Texto",
        Documentation.Author = "Paulo André Jesus",
        Documentation.Version = "3.0",
        Documentation.Examples = {
            [Description = "Mc e Mac", Code = "fnFormataNome(""ANGUS MCGREGOR MACDONALD"")", Result = """Angus McGregor MacDonald"""],
            [Description = "Sobrenome que começa com mac", Code = "fnFormataNome(""JOÃO MACHADO DE MACEDO"")", Result = """João Machado de Macedo"""],
            [Description = "Partículas compostas", Code = "fnFormataNome(""MARÍA DE LAS MERCEDES"")", Result = """María de las Mercedes"""],
            [Description = "Hífen e apóstrofo", Code = "fnFormataNome(""BRIDGET O’REILLY DA SILVA-PEREIRA"")", Result = """Bridget O’Reilly da Silva-Pereira"""]
        }
    ]
in
    Value.ReplaceType(fnFormataNome, Value.ReplaceMetadata(Value.Type(fnFormataNome), Documentacao))

fnNuvemDePalavras

Limpa um texto em português para nuvem de palavras: remove pontuação e palavras sem valor analítico (stopwords) e preserva as negações.

UDFv3.0
let
    fnNuvemDePalavras = (texto as nullable text) as nullable text =>
        let
            // Pontuação e espaços viram delimitadores num único Text.SplitAny; o hífen fica dentro da palavra
            vDelimitadores =
                "!.:;,?@#$%^&*()[]{}\/_+=<>|""“”…'´`~–—"
                & " #(tab)#(cr)#(lf)#(00A0)",
            // Negações NÃO são stopwords: elas se juntam à palavra seguinte
            vNegacoes = {"não", "nao", "nunca", "nem", "jamais"},
            // Amplie a lista conforme o assunto analisado
            vStopWords = {
                "a", "à", "às", "ao", "aos", "as", "o", "os", "um", "uma", "uns", "umas",
                "de", "da", "das", "do", "dos", "em", "na", "nas", "no", "nos", "num", "numa",
                "para", "pra", "pro", "pelo", "pela", "pelos", "pelas", "por", "com", "sem", "sobre", "entre",
                "e", "ou", "mas", "porém", "porque", "pq", "que", "q", "se", "como", "também", "tb", "tbm", "então", "sim",
                "é", "foi", "fui", "ser", "sido", "será", "são", "era", "eram", "está", "estão", "estou", "estava", "estavam",
                "ter", "tem", "têm", "tenho", "tive", "teve", "tinha", "há", "havia",
                "eu", "me", "mim", "você", "vc", "ele", "ela", "eles", "elas", "nós", "lhe", "lhes",
                "seu", "sua", "seus", "suas", "meu", "minha", "meus", "minhas", "teu", "tua", "teus", "tuas",
                "esse", "essa", "esses", "essas", "este", "esta", "estes", "estas",
                "aquele", "aquela", "aqueles", "aquelas", "isto", "isso", "aquilo",
                "muito", "muita", "muitos", "muitas", "mais", "bem", "já", "só", "ainda", "tudo", "todo", "toda"
            },
            vTokens = List.Select(
                Text.SplitAny(Text.Lower(texto), vDelimitadores),
                each _ <> "" and _ <> "-" and not List.Contains(vStopWords, _)
            ),
            // "não recomendo" → "não-recomendo"; negação no fim do texto fica sozinha
            vEstado = List.Accumulate(
                vTokens,
                [Lista = {}, Pendente = null],
                (estado, palavra) =>
                    if List.Contains(vNegacoes, palavra) then
                        [Lista = estado[Lista],
                         Pendente = if estado[Pendente] = null then palavra else estado[Pendente] & "-" & palavra]
                    else
                        [Lista = estado[Lista] & {if estado[Pendente] = null then palavra else estado[Pendente] & "-" & palavra},
                         Pendente = null]
            ),
            vPalavras = if vEstado[Pendente] = null then vEstado[Lista] else vEstado[Lista] & {vEstado[Pendente]}
        in
            if texto = null then null else Text.Combine(vPalavras, " "),

    fnNuvemDePalavrasTipo = type function (
        texto as (type nullable text meta [
            Documentation.FieldCaption = "Texto",
            Documentation.FieldDescription = "Comentário livre (ex.: feedback de pesquisa). Null retorna null."
        ])
    ) as nullable text meta [
        Documentation.Name = "fnNuvemDePalavras",
        Documentation.Description = "Limpa um texto para nuvem de palavras, preservando negações.",
        Documentation.LongDescription = "Converte para minúsculas, quebra por pontuação e espaços, remove artigos, preposições, pronomes, verbos auxiliares e intensificadores do português e junta negações (não, nunca, nem, jamais) à palavra seguinte com hífen, para que 'não recomendo' não vire 'recomendo'. A lista vStopWords pode ser ampliada.",
        Documentation.Category = "Texto",
        Documentation.Author = "Paulo André Jesus",
        Documentation.Version = "3.0",
        Documentation.Examples = {
            [Description = "Negação preservada", Code = "fnNuvemDePalavras(""Não recomendo, tive uma experiência negativa."")", Result = """não-recomendo experiência negativa"""],
            [Description = "Negação após stopword", Code = "fnNuvemDePalavras(""Não estou satisfeito com o suporte."")", Result = """não-satisfeito suporte"""],
            [Description = "Só stopwords", Code = "fnNuvemDePalavras(""O que é isso?"")", Result = """"""]
        }
    ]
in
    Value.ReplaceType(fnNuvemDePalavras, fnNuvemDePalavrasTipo)

fnRemoveAcentos

Remove acentos de letras latinas por uma tabela fixa de substituição.

UDFv2.0
let
    fnRemoveAcentos = (texto as nullable text) as nullable text =>
        let
            vDe   = "ÀÁÂÃÄÅÇÈÉÊËÌÍÎÏÑÒÓÔÕÖØÙÚÛÜÝàáâãäåçèéêëìíîïñòóôõöøùúûüýÿŠšŽžŸ",
            vPara = "AAAAAACEEEEIIIINOOOOOOUUUUYaaaaaaceeeeiiiinoooooouuuuyySsZzY",
            vPares = List.Zip({Text.ToList(vDe), Text.ToList(vPara)})
        in
            if texto = null then null
            else Text.Combine(List.ReplaceMatchingItems(Text.ToList(texto), vPares)),

    Documentacao = [
        Documentation.Name = "fnRemoveAcentos",
        Documentation.Description = "Remove acentos de letras latinas por uma tabela fixa de substituição.",
        Documentation.LongDescription = "Cada caractere de vDe é trocado pelo caractere na mesma posição de vPara. Caracteres fora da tabela não mudam. O resultado não depende da página de código do computador.",
        Documentation.Category = "Texto",
        Documentation.Author = "Paulo André Jesus",
        Documentation.Version = "2.0",
        Documentation.Examples = {
            [Description = "Texto em português", Code = "fnRemoveAcentos(""São João d’Ávila"")", Result = """Sao Joao d’Avila"""],
            [Description = "Caractere fora da tabela", Code = "fnRemoveAcentos(""Straße"")", Result = """Straße"""]
        }
    ]
in
    Value.ReplaceType(fnRemoveAcentos, Value.ReplaceMetadata(Value.Type(fnRemoveAcentos), Documentacao))

Validação

2

fnValidaCNPJ

Valida um CNPJ numérico ou alfanumérico pelos dois dígitos verificadores.

UDFv2.0
let
    fnValidaCNPJ = (CNPJ as any) as nullable logical =>
        let
            vTexto =
                if CNPJ = null then null
                else if CNPJ is number then Text.PadStart(Text.From(Number.Abs(CNPJ), "en-US"), 14, "0")
                else if CNPJ is text then Text.Upper(CNPJ)
                else Text.Upper(Text.From(CNPJ)),
            vLimpo = Text.Select(vTexto ?? "", {"0".."9", "A".."Z"}),
            // Valor de cada caractere: código ASCII - 48
            vValores = List.Transform(Text.ToList(vLimpo), each Character.ToNumber(_) - 48),
            fnDigito = (base as list) as number =>
                let
                    // 12 posições: 5,4,3,2,9..2 | 13 posições: 6,5,4,3,2,9..2
                    vPesos = List.LastN({6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2}, List.Count(base)),
                    vResto = Number.Mod(List.Sum(List.Transform(List.Positions(base), (i) => base{i} * vPesos{i})), 11)
                in
                    if vResto < 2 then 0 else 11 - vResto,
            vBase = List.FirstN(vValores, 12),
            vDV1 = fnDigito(vBase),
            vDV2 = fnDigito(vBase & {vDV1})
        in
            if vTexto = null then null
            else Text.Length(vLimpo) = 14
                and Text.Select(Text.End(vLimpo, 2), {"0".."9"}) = Text.End(vLimpo, 2)
                and List.Count(List.Distinct(vValores)) > 1
                and vDV1 = vValores{12}
                and vDV2 = vValores{13},

    Documentacao = [
        Documentation.Name = "fnValidaCNPJ",
        Documentation.Description = "Valida um CNPJ numérico ou alfanumérico pelos dígitos verificadores.",
        Documentation.LongDescription = "Aceita CNPJ formatado, sem formatação ou numérico. Letras minúsculas são convertidas para maiúsculas. Rejeita sequências repetidas. Entrada null retorna null.",
        Documentation.Category = "Validação",
        Documentation.Author = "Paulo André Jesus",
        Documentation.Version = "2.0",
        Documentation.Examples = {
            [Description = "CNPJ numérico", Code = "fnValidaCNPJ(""12.345.678/0001-95"")", Result = "true"],
            [Description = "CNPJ alfanumérico (exemplo da Receita Federal)", Code = "fnValidaCNPJ(""12.ABC.345/01DE-35"")", Result = "true"],
            [Description = "Dígito errado", Code = "fnValidaCNPJ(""12.345.678/0001-00"")", Result = "false"]
        }
    ]
in
    Value.ReplaceType(fnValidaCNPJ, Value.ReplaceMetadata(Value.Type(fnValidaCNPJ), Documentacao))

fnValidaCPF

Valida um CPF pelos dois dígitos verificadores.

UDFv2.0
let
    fnValidaCPF = (CPF as any) as nullable logical =>
        let
            vTexto =
                if CPF = null then null
                else if CPF is number then Text.PadStart(Text.From(Number.Abs(CPF), "en-US"), 11, "0")
                else if CPF is text then CPF
                else Text.From(CPF),
            vDigitos = Text.Select(vTexto ?? "", {"0".."9"}),
            vNumeros = List.Transform(Text.ToList(vDigitos), Number.FromText),
            // Pesos decrescentes a partir de (tamanho + 1); resto 10 vira 0
            fnDigito = (base as list) as number =>
                let
                    vSoma = List.Sum(List.Transform(List.Positions(base), (i) => base{i} * (List.Count(base) + 1 - i))),
                    vResto = Number.Mod(vSoma * 10, 11)
                in
                    if vResto = 10 then 0 else vResto,
            vDV1 = fnDigito(List.FirstN(vNumeros, 9)),
            vDV2 = fnDigito(List.FirstN(vNumeros, 9) & {vDV1})
        in
            // "and" para no primeiro false: vNumeros{9} só é lido com 11 dígitos
            if vTexto = null then null
            else Text.Length(vDigitos) = 11
                and List.Count(List.Distinct(vNumeros)) > 1
                and vDV1 = vNumeros{9}
                and vDV2 = vNumeros{10},

    Documentacao = [
        Documentation.Name = "fnValidaCPF",
        Documentation.Description = "Valida um CPF pelos dígitos verificadores.",
        Documentation.LongDescription = "Aceita CPF formatado (123.456.789-09), sem formatação ou numérico. Rejeita sequências repetidas (111.111.111-11). Entrada null retorna null.",
        Documentation.Category = "Validação",
        Documentation.Author = "Paulo André Jesus",
        Documentation.Version = "2.0",
        Documentation.Examples = {
            [Description = "CPF válido", Code = "fnValidaCPF(""123.456.789-09"")", Result = "true"],
            [Description = "Dígito errado", Code = "fnValidaCPF(""123.456.789-00"")", Result = "false"],
            [Description = "Sequência repetida", Code = "fnValidaCPF(""111.111.111-11"")", Result = "false"]
        }
    ]
in
    Value.ReplaceType(fnValidaCPF, Value.ReplaceMetadata(Value.Type(fnValidaCPF), Documentacao))

Convenção de nomes — obrigatória

Tabela / Coluna

Cns + PascalCase

CnsClienteTratado

UDF

fn + PascalCase

fnFiscalYear
Documentação embutida: UDF documentada via Value.ReplaceType com Documentation.Name, Description, LongDescription, Category, Author, Version e Examples.