tl;dr „M“ je jazyk používaný v Power Query pro transformaci, filtrování a kombinování dat. Power Query automaticky generuje kód M na základě tvých akcí v uživatelském rozhraní, ale pro složitější operace jej lze psát i ručně. M funguje převážně voláním předdefinovaných funkcí, které upravují data krok za krokem, což z něj činí intuitivní a snadno naučitelný jazyk.
Upozornění: Tento článek je určen pro začátečníky a nepůjde do hloubky jazyka M. Jeho cílem je seznámit tě s „M“, abys ho mohl začít používat již dnes na základní úrovni.
Co je “M”?
M neznamená „Microsoft,“ jak bys možná očekával. Místo toho označuje „Mash-up,“ což odráží jeho účel – kombinování více datových zdrojů do jednoho dobře strukturovaného datasetu nebo dotazu. Toto je však neoficiální vysvětlení. Microsoft nikdy oficiálně nedefinoval, co přesně „M“ znamená.
Jazyk M se primárně používá v Power Query (Power BI, Excel, Dataflows atd.), kde jeho hlavním cílem je úprava datasetů podle tvých potřeb. To zahrnuje transformaci, filtrování a kombinování různých datových zdrojů do jednoho celku. Pokud jsi už s Power Query pracoval, možná sis všiml kódu M v řádku vzorců (formula bar). Každá akce, kterou v Power Query provedeš, generuje na pozadí kód M – tudíž ses s Mkem už vlastně nepřímo seznámil.
Proč jenom “klikání” nestačí
Klikání - interakce s UI - je primární způsob práce v Power Query a vlastně to i dává smysl. Power Query nabízí spoustu možností pro úpravu dat ve formě tlačítek a většinou to plně postačí. Když ale pracuješ se špatně strukturovanými daty nebo potřebuješ provést složité úpravy – a změna na zdroji není možná – rychle narazíš na limity dostupných tlačítek. Právě tehdy nastává čas začít si „tlačítka“ psát sami.
Jednoduchým příkladem může být odstranění čísel z textu nebo naopak. Představ si, že máš sloupec s textovými hodnotami, ale z nějakého důvodu každý záznam obsahuje i ID s proměnlivou délkou.
Tvým cílem by bylo buď zachovat čísla, nebo text. Zkusme druhou možnost a ponechme pouze text. K tomu bys musel napsat něco takového:
= Table.TransformColumns(source, {{"customer", each Text.Remove(_, {"0".."9"}), type text}})
Vím, že technicky existuje funkce v Rozdělit sloupec (Split Column), která umožňuje rozdělit podle číslic a nečíselných znaků. Nicméně přidává další kroky, protože vytvoří nový sloupec místo úpravy toho stávajícího.
Jak můžeš vidět, kód využívá několik vestavěných funkcí, které přijímají parametry. To je v kostce jazyk M – vytváříš sekvenci kroků pomocí těchto funkcí a postupně upravuješ svůj datový zdroj.
To však byla jen ukázka. Pojďme udělat krok zpět a začít od základů jazyka M.
Koncept M
“M” je case-sensitive jazyk vzorců. To, že je case-sensitive ti může později přinést spoustu trápení, respektive, pokud na to zapomeneš, proto doporučuji si tento fakt zapsat za uši. Jediný účel Mka je transformovat data do krásných a struktorovaných dotazů, které poté můžeš použít pro efektivní modelování, například v Power BI.
Mko víceméně funguje jako sada postupných kroků, které tvoje data postupně upravují a vylepšují. Každý krok navazuje na ten předchozí (obecně), tudíž vytvoříš plynulý proud “vylepšováků”. Abychom tyto kroky definovali, používáme klíčová slova let and in.
let, in (“letin”) a proměnné
let a in jsou párová klíčová slova, což znamená, že ke každému let musí existovat odpovídající in. Můžeš je vnořovat do sebe, jen si dej pozor, abys je správně spároval.
let slouží k definování proměnných, zatímco in určuje výstup – a tím může být proměnná nebo vlastně cokoli jiného.
let
a = 10,
b = 20
in
a+b
Tento jednoduchý kód přidává dvě proměnné a vrací jejich součet pomocí části in.
Uvnitř let-in výrazu můžeš definovat libovolný počet proměnných, přičemž každá z nich je oddělena čárkou. Výsledek můžeš vrátit přímo a nechat ho tak, jak je.
Já ale osobně vždy preferuji vracet proměnnou místo přímého zápisu výsledného výrazu. Proč?
- Jednoduší ladění/debugging – Místo přepisování výsledku na více místech můžeš definovat vše jako proměnnou a snadno je zaměňovat, abys viděl, co se děje v jednotlivých krocích.
- Lepší přehlednost v UI – Pokud vrátíš proměnnou (zejména tu poslední), Power Query vytvoří sekvenci viditelných kroků v okně Query Settings → Applied Steps window, což usnadní sledování a kontrolu transformací.
Změňme předchozí řešení a přidejme třetí proměnnou, která sečte proměnné a a b; pojmenujeme ji třeba c.
Místo zapsání výpočtu do “in” části, uchováme výpočet v proměnné c a poté ji taky vrátíme jako výsledek:
let
a = 10,
b = 20,
c = a + b
in
c
Query Settings teď bude též vypadat lépe, protože nám umožní vizuálně přepínat mezi jednotlivými proměnnými v okně Applied Steps. Díky tomu je mnohem snazší ověřit správnost každého kroku.
Neexistuje žádné striktní pravidlo pro escapování znaků, odsazení nebo mezery ve struktuře let-in. Máš volnost ve formátování, bohudíky/žel.
Já však osobně preferuji udržovat kód přehledný správným odsazením vnořených prvků, což zlepšuje čitelnost a usnadňuje orientaci.
Proměnné mohou mít téměř libovolné názvy – můžeš použít camelCase, snake_case, kebab-case, nebo PascalCase. Dokonce můžeš použít i mezery, ale z praktického hlediska to nedoporučuji.
Každá proměnná, která obsahuje speciální znak (*mezera, @, #, $, %, &, , -, etc.), rezervované klíčové slovo, či začínající číslem požaduje speciální zápis: #“název proměnné”.
Pamatuj, že názvy proměnných musí být jedinečné (alespoň v rámci jednoho let-in), a pokud použiješ stejný název s i bez speciálního zápisu, Power Query je bude brát jako jednu a tu samou proměnnou.
Základní operátoři
M má poměrně jednoduché základní operátory, které jsou snadno pochopitelné. Rozdělím je do několika kategorií:
-
Arithmetic Operators
- + Sčítání
- - Odčítání
- * Násobení
- / Dělení
-
Comparison Operators
- = Rovná se
- <> Nerovná se
- < Menší než
- > Větší než
- <= Menší než nebo se rovná
- >= Větší než nebo se rovná
-
Logical Operators
- and Logické AND
- or Logické OR
- not Logické NOT
-
Text, Record, List
- & Spojení
-
Special Operators
- ? - Pole může být null ( [field]? )
- ?? - coalesce - vrátí první hodnotu, která není null ( null??“abc” )
- .. - interval ( {0..9} ) - vrátí seznam čísel v daném rozsahu 0-9
Základní datové typy
V jazyce M máme několik datových typů s unikátním zápisem. Zde je základní seznam těch, které budeš nejčastěji potřebovat (skutečný zápis je zvýrazněn:
-
Čísla
- number → Desetinné číslo (e.g., 123.45)
- integer → Celé číslo (e.g., 10)
-
Text
- text → Textový řetězec (e.g., “Hello”)
-
Logická
- logical → Logická (true or false)
-
Null
- null → Představuje prázdné/nedefinované hodnoty
-
Datum a čas
- date → #date(2024, 2, 5)
- time → #time(12, 30, 0)
- datetime → #datetime(2024, 2, 5, 12, 30, 0)
- datetimezone → #datetimezone(2024, 2, 5, 12, 30, 0, 1, 0)
- duration → #duration(1, 2, 30, 0)
-
Seznam / List
- list → Seřazená kolekce data (e.g., {1, 2, 3})
-
Záznam / Record
- record → Kolekce key-value párů (e.g., [Name = “John”, Age = 30])
-
Tabulka
- table → Struktorovaná data se sloupci a řádky (e.g., #table({“Column1”, “Column2”}, {{1, “A”}, {2, “B”}}))
Existuje několik dalších datových typů, ale tyto patří mezi nejpoužívanější. V jazyce M můžeš libovolně kombinovat různé datové typy – například můžeš mít seznam, kde každý prvek je záznam, a tento záznam může obsahovat další seznam, a tak dále. Pokud jsi někdy pracoval se zanořeným JSON souborem, v Power Query můžeš vytvořit něco velmi podobného.
Měj ale na paměti, že cílem dotazů načítaných do Power BI modelu by měla být co nejvíce „zploštělá“ tabulka, namísto složitých vnořených struktur.
Mimochodem, práce s JSONem v Power Query je docela “pohodinda”, na pár kliků máš rozevřený celý JSON.
Klíčová “klíčová slova”
V jazyce M existuje mnoho dalších klíčových slov, ale protože jde o průvodce pro začátečníky, zmíním jen ta, která se používají nejčastěji:
-
Podmíněná logika
-
if … then … else → Definuje podmínky pro větvení logiky
- Příklad: if 5 > 3 then “Yes” else “No”
-
-
Funkce a iterace
-
each → Vytváří anonymní funkci pro iteraci
- Příklad: List.Transform({1, 2, 3}, each _ * 2)
-
as → Určuje očekávaný datový typ pro parametr nebo proměnnou
- Příklad: (x as number)=>
-
-
Zpracování chyb
-
try … otherwise → Zachytí chyby a poskytne záložní hodnotu
- Příklad: try 1 / 0 otherwise “chyba”
-
-
Kontrola a konverze typů
-
is → Ověří, zda hodnota patří k určitému datovému typu
- Příklad: 123 is number
-
type → Definuje vlastní datový typ, nastavuje datové typy sloupců nebo zajišťuje konzistenci typů
- Příklad type [Name = text, Age = number]
- Příklad Table.AddColumn(tbl, “column”, each 1, type number)
-
Koncep předpřipravených funkcí
Jak jsem už zmínil, M obsahuje stovky vestavěných funkcí, které můžeš použít k úpravě dat. Aby ti usnadnili práci, jsou přehledně rozdělené do kategorií a jejich názvy obvykle přesně vystihují, co dělají.
Jako bonus ti Power Query při inicializaci proměnné napoví potřebné parametry spolu s krátkým vysvětlením.
Například:
Pokud má funkce volitelné parametry, stačí zadat všechny povinné a přidat čárku – Power Query ti pak odhalí tajné tipy k dalším možnostem. Každá další čárka odkryje další volitelný parametr. Pokud se žádné nápovědy nezobrazí, znamená to, že funkce žádné volitelné parametry nemá.
Technicky vzato můžeš prostě „spamovat“ čárky, dokud se neukážou volitelné parametry. Například takhle: Table.AddColumn(,,,)
Další možností (pokud jsi fanoušek klikání) je použít malé šipky kolem čísla v levém dolním rohu. Ty ti odhalí všechny „tajné“ parametry – i bez zadání všech povinných hodnot.
Jak můžeš vidět, tato funkce (jak napovídá její název) přidává do tabulky nový sloupec na základě zadaných kritérií. Názvy funkcí v jazyce M obvykle začínají typem dat, za kterým následuje tečka a konkrétní operace, kterou funkce provádí.
Tato syntaxe pojmenování je v M většinou konzistentní, což usnadňuje orientaci v dostupných funkcích.
Jak začít psát “Mko”
Teď, když jsme si prošli základy a klíčové koncepty, jak to vlastně všechno začít používat? Znamená učení jazyka M, že musíš přestat klikat na tlačítka? Rozhodně ne. Klikání je stále užitečná a efektivní metoda, která ti může ušetřit spoustu času – i když jsi expert na M.
Pokud se ale přistihneš, že musíš pro jednu jedinou úlohu proklikat spoustu tlačítek, zkus se na chvíli zastavit a zamyslet se: Nešlo by to vyřešit jedním chytrým krokem v M? Často tím nejen zjednodušíš proces, ale také zlepšíš výkon celého dotazu.
Příklad #1
Pravděpodobně #1 go-to operace.
Pojďme se podívat na jednu z nejčastěji používaných funkcí v jazyce M: Table.AddColumn(). Jak už název napovídá, tato funkce přidává do tabulky nový sloupec. Podívejme se blíže na to, jak funguje.
Table.AddColumn(table as table, newColumnName as text, columnGenerator as function, optional columnType as nullable type) as table
Tato funkce přijímá 4 parametry – 3 jsou povinné, 1 je volitelný.
table as table - u každé tabulkové funkce je prvním parametrem obvykle zdrojová tabulka. To je tabulka, do které chceš přidat nový sloupec, a ve většině případů to bude jednoduše tvůj předchozí krok.
newColumnName as text - tento krok je jednoduchý – stačí zadat název nového sloupce jako text, což znamená, že musí být v dvojitých uvozovkách.
columnGenerator as function - tady to může začít skřípat. Ale jako začátečník si hlavně zapamatuj – musíš napsat výraz, který se aplikuje na každý řádek zvlášť. Tento výraz může vracet číslo, text, boolean nebo dokonce celou tabulku.
Můžeš také přistupovat k hodnotám z jiných sloupců, vždy s ohledem na konkrétní řádek.
Celý tento parametr musí být napsán jako funkce (což si brzy vysvětlíme). A právě to je ta záludná část!
optional columnType as nullable type - v této části můžeš předem určit datový typ nového sloupce. Obvykle použiješ něco jako “type text” či “type number”. Definování typu hned na začátku ti ušetří další krok s transformací „Changed Type“ později.
Pojďme společně přidat několik praktických sloupců.
Toto je naše zdrojová tabulka a aktuální aplikované kroky.
Chceme přidat nový sloupec s názvem “totalSales”, který vypočítá součin quantity × unitPrice. Tento postup by ti měl být povědomý – ať už přidáváš nový sloupec v Power Query, používáš vzorce v Excelu, nebo funkci SUMX v DAX.
Nejprve zahájíme vlastní krok. To můžeš udělat kliknutím na ikonu funkce vedle řádku vzorců.
Pokud nevidíš řádek vzorců, přejdi na kartu View a vlevo klikni na Formula Bar, čímž ho aktivuješ.
Pokud se to podaří, v řádku vzorců uvidíš jako hodnotu poslední provedený krok.
Teď zavoláme naši funkci Table.AddColumn().
Tabulka bude odpovídat předchozímu kroku, což je v našem případě “changeTypes”. Nový sloupec pojmenujeme “totalSales”. Výraz vypočítá součin dvou sloupců: [quantity] a [unitPrice]. Volitelný parametr nastavíme na “type number”, aby měl nový sloupec správný datový typ.
= Table.AddColumn(changeTypes, "totalSales", each [quantity]*[unitPrice], type number)
V kódu si můžeš všimnout jednoho extra klíčového slova, které jsem zatím nezmínil –“each”. Toto klíčové slovo je “syntax sugar” nebo zjednodušení zápisu. Each by zasloužil samotný článek, nicméně pro zatím si zapamatuj, že each je zkrácený zápis pro (_)=>, funkce s patametrem ”_”. Tento parametr představuje aktuální řádek jako záznam (record).
()=> je obecný syntax funkce, která příjímá parametry uvnitř.
Pokud bychom vrátili samotný parametr ”_”, pro každý řádek v iteraci bychom dostali něco takového (záznam/record se čtyřmi poli):
V našem případě ale jen bereme dvě pole – quantity a unitPrice – a násobíme je.
Technicky bys to měl zapsat takto: _[quantity] * _[unitPrice], protože jenom ”_” reprezentuje celý řádek, a chceme z něj získat hodnoty quantity a unitPrice.
Nicméně díky each, M umožňuje zápis i bez podtržítka – tímto způsobem se Mko snaží být přístupnější, když začínáš.
Každopádně, to je vše, tvůj první řádek v Mku.
Poslední věc, v Applied Steps, tento proces vytvoří nový krok “Custom1”.
Jestli si máš z tohohle článku odnést jen jednu věc, tak prosím přejmenovávej svoje názvy kroků, aby reprezentovaly to, co vlastně dělají. Takže dvakrát klikni na stávájící název nebo zmáčkni F2 a přiřaď jí příhodné jméno - něco jako “addTotalSales”.
Tento zvyk ulehčí práci nejen tobě, ale i ostatním. Představ si, že za šest měsíců se tvůj report zbortí, a budeš muset znovu zjistit, co každý krok dělá - protože věř mi, budeš si sotva pamatovat, jak se jmenuješ.
Samozřejmě, tvůj report nikdy nespadne! Ale řekněme, že zdědíš report od někoho, kdo tento zvyk neměl… to už je jiný příběh.
Příklad #2
Méně obvyklé pro začátečníky, ale game-changer až trošku poskočíš.
Podívejme se na příklad, který jsem zmínil dříve, odstranění číslic z textu.
Tento scénář jsem vybral, protože ukazuje mnoho drobných technik, které můžeš použít. Ve skutečnosti budou tvoje vlastní případy použití pravděpodobně mnohem jednodušší, takže se nenech odradit, pokud to hned nepochopíš – já bych to také nedokázal!
Nejprve naklikejme, jak bychom to mohli provést. Máme několik variant. Můžeme nahradit všechny číslice (10 kroků, 0-9) prázdnými hodnotami, což je však vysoce neefektivní přístup. Druhá možnost by mohla být rozdělení sloupců. Druhá zní lépe.
Mrkněme na vygenerované Mko.
= Table.SplitColumn(source, "customer", Splitter.SplitTextByCharacterTransition({"0".."9"}, (c) => not List.Contains({"0".."9"}, c)), {"customer.1", "customer.2"})
Toto je docela brutál, i pro zkušené veterány. Nicméně můžeme vidět náznaky technik, které se dají využít později.
Okej, toto bylo klikání, teď se vrátíme o krok zpět a zjistíme, jak to napsat lépe. Hlavní kritéria jsou: minulé řešení vytváří další sloupec (což nechceme), navíc chceme zachovat text pouze v existujícím sloupci. Jaký je tedy plán?
Nejprve zahájíme vlastní krok. Můžeš to udělat kliknutím na ikonu funkce vedle řádku vzorců. Pokud nevidíš řádek vzorců, přejdi na pás karet Zobrazení a povol jej kliknutím na Řádek vzorců vlevo.
Víme, že chceme upravit tabulku. Začněme psát “Table” a podívejme se, jaké máme možnosti:
Existuje mnoho funkcí a tvoje první hledání může chvíli trvat – což je v pořádku.
Jedna silná a všestranná funkce, kterou stojí za to se naučit, je Table.TransformColumns(). Tato funkce ti umožňuje definovat seznam sloupců a specifikovat pro ně transformace. Je to skvělá funkce, když chceš upravovat existující sloupce a zároveň nevytvářet sloupce nové.
Máme dva povinné parametry. První je tabulka, což by mělo být jednoduché - obvykle to bývá váš předchozí krok, a v našem případě je to krok nazvaný “source”.
Druhý parametr je trochu speciální a vyžaduje specifickou syntaxi. Pokud si nejsi jistý nějakou funkcí, Microsoft má oficiální dokumentaci k dispozici, takže se na ni kdykoli můžeš podívat. Pro tuto konkrétní funkci je dokumentace k dispozici zde.
Syntaxe pro druhý parametr je seznam transformací sloupců, kde každá položka je dalším seznamem obsahujícím 2 až 3 parametry. Vypadá to nějak takto:
Table.TransformColumns(source,
{
{ column name, transformation, new column type }
}
)
Prvním parametrem je název existujícího sloupce – v našem případě “customer”. Druhým parametrem je transformační funkce, která definuje operaci aplikovanou na každý řádek v tomto sloupci.
V našem případě chceme odstranit číslice z textu. Najděme tedy funkci, která by nám pomohla. Napíšeme “Text” a jde se hledat:
Opět máme spoustu možností, funkce jako Text.Remove, Text.Replace a další. Mnohé z nich nám mohou pomoci dosáhnout požadovaného výsledku, ale některé budou účinnější než jiné.
Vybereme třeba Text.Remove.
Text.Remove potřebuje dva parametry. Prvním je text, který chceme upravit, a druhým jsou znaky, které chceme odstranit.
Text, který upravujeme, je reprezentován podtržítkem (_), které drží hodnotu aktuálního řádku při použití each. Tento koncept by mohl být samostatným tématem, ale prozatím si zapamatuj: kdykoliv použiješ each pro definování funkce, aktuální položka v rozsahu je přístupná přes _.
V našem případě bude Table.TransformColumns procházet všechny řádky ve sloupci “customer” a _ bude představovat hodnotu každého jednotlivého řádku, když je zpracováván.
Druhý parametr funkce Text.Remove vyžaduje, abychom uvedli jednotlivé znaky, s kterými se už nekamarádíme - chceme se jich zbavit. V tomto případě chceme odstranit jakýkoliv výskyt konkrétní číslice, tedy 0-9. Abychom zajistili jednotlivý výmaz, musíme definovat seznam obsahující všech deset číslic.
Buď ho můžeme napsat manuálně:
{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"}
Nebo si ulehčit život a napsat interval:
{"0".."9"}
Operátor .. představuje rozsah a Power Query automaticky vyplní chybějící hodnoty mezi “0” a “9”. I když jsou tyto hodnoty napsány jako text, Power Query vyhledá jejich Unicode hodnoty a vyplní sekvenci na základě číselné reprezentace počátečního a koncového znaku.
V tomto případě dostaneme stejný výsledek jako ručně vytvořený seznam. Podobně by fungoval stejný přístup i pro anglickou abecedu, například:
{"A".."Z"}
Toto by vygenerovalo {“A”, “B”, “C”, …, “Z”}, protože velká anglická písmena jsou uložena sekvenčně v Unicode.
Když teď dáme všechny kroky dohromady, dostaneme:
Nakonec můžeme zmínit volitelný parametr “new column type”, který nám umožňuje přímo nastavit datový typ sloupce.
Ve výchozím nastavení Table.TransformColumns odstraní jakýkoli dříve přiřazený datový typ pro upravovaný sloupec. Abychom se vyhnuli nutnosti jej znovu přiřazovat v samostatném kroku, můžeme použít tento volitelný parametr a rovnou jej nastavit na typ text.
Pro tip: Pokud tvoje změny převádějí text na číslo a nastavíš volitelný parametr na typ čísla, Power Query může stále vnímat jako text - to chceš (joke). Abys tomu zabránil, jednoduše zabal výsledný výraz do Number.From().
A máme hotovo! Váš první řádek v Mku. 🎉
A teď si představ,všechny ty možnosti, jak si ulehčit spoustu kliků. Pamatuj si, že vždy existuje způsob, jak něco udělat lépe, a M ti to umožní.
Klíčové funkce na zapamatování
Jako bonus ti ulehčím život a vypíšu pár klíčových funkcí, které při každodenním psaní Mka potkáš nejčastěji. Pořadí nic neznamená.
Funkce tabulky
- Table.TransformColumns(table, transformations, optional columnType) – Upraví hodnoty v existujících sloupcích.
- Table.ColumnNames(table) – Dodá seznam názvu sloupců.
- Table.Column(table, columnName) – Získá obsah sloupce jako seznam/list.
- Table.Buffer(table) – Uloží aktuální stav tabulky do paměti.
- Table.SelectRows(table, condition as function) – Vyfiltruj tabulku podle podmínky.
Funkce seznamu/listu
- List.Distinct(list) – Vyhodí duplicity ze seznamu.
- List.Transform(list, transformation as function) – Pro každý řádek vyhodnotí výraz.
- List.Select(list, condition as function) – Vyfiltruje seznam podle podmínky.
- List.Contains(list, value) – Zkontroluje, jestli hodnota existuje v seznamu.
- List.Count(list) – Sečte počet položek v seznamu.
- List.Dates(start, count, step) – Vygeneruje seznam dat (datumů).
Funkce záznamu/recordu
- Record.FieldNames(record) – Vrátí seznam polí v záznamu.
- Record.SelectFields(record, fieldNames as list) – Ponechá konkrétní pole ze záznamu.
- Record.TransformFields(record, transformations as list) – Transformuje vybrané pole.
- Record.Field(record, fieldName as text) – Vrátí hodnotu konkrétního pole.
Summary
M je výkonný, ale intuitivní jazyk používaný v Power Query pro efektivní transformaci, filtrování a kombinaci dat. I když často stačí klikání přes uživatelské rozhraní, znalost M ti umožní optimalizovat dotazy, snížit počet zbytečných kroků a zvládat složité transformace. Pochopením klíčových konceptů, jako jsou let…in, vestavěné funkce a datové typy, můžeš plně ovládnout svá data. Začni s malými kroky, experimentuj a postupně zapoj M do svého pracovního postupu; ušetří ti to čas a úsilí z dlouhodobého hlediska! 🚀
