├── .gitattributes ├── .gitignore ├── PowerQueryExtensions.sln ├── README.md ├── build ├── Date.mez ├── List.mez ├── Number.mez ├── Splitter.mez ├── Table.mez ├── Text.mez └── Value.mez ├── media └── Modules.png └── src ├── Date ├── Date.mproj ├── Date.pq └── README.md ├── List ├── List.mproj ├── List.pq └── README.md ├── Number ├── Number.mproj ├── Number.pq └── README.md ├── Splitter ├── README.md ├── Splitter.mproj └── Splitter.pq ├── Table ├── README.md ├── Table.mproj └── Table.pq ├── Text ├── README.md ├── Text.mproj └── Text.pq └── Value ├── README.md ├── Value.mproj └── Value.pq /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc -------------------------------------------------------------------------------- /PowerQueryExtensions.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26507.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{4DF76451-A46A-4C0B-BE03-459FAAFA07E6}") = "Table", "src\Table\Table.mproj", "{BC5E50DE-5AE8-481C-B6BE-CC500490770C}" 7 | EndProject 8 | Project("{4DF76451-A46A-4C0B-BE03-459FAAFA07E6}") = "List", "src\List\List.mproj", "{BF55FED0-B51D-420A-84A0-F63F677CCA2C}" 9 | EndProject 10 | Project("{4DF76451-A46A-4C0B-BE03-459FAAFA07E6}") = "Date", "src\Date\Date.mproj", "{5579955A-C61D-4FBD-80DB-95C20E2AE00D}" 11 | EndProject 12 | Project("{4DF76451-A46A-4C0B-BE03-459FAAFA07E6}") = "Text", "src\Text\Text.mproj", "{CDA74E9B-E9EE-493A-BD2E-C93AA250EF3B}" 13 | EndProject 14 | Project("{4DF76451-A46A-4C0B-BE03-459FAAFA07E6}") = "Number", "src\Number\Number.mproj", "{1916BB62-D744-4EB8-86B2-33D0B9315A15}" 15 | EndProject 16 | Project("{4DF76451-A46A-4C0B-BE03-459FAAFA07E6}") = "Splitter", "src\Splitter\Splitter.mproj", "{846B0CC1-2F97-4E7D-BEEB-3E9869AC76A0}" 17 | EndProject 18 | Project("{4DF76451-A46A-4C0B-BE03-459FAAFA07E6}") = "Value", "src\Value\Value.mproj", "{412F037C-A922-4EB2-BB7B-5F1D509FC400}" 19 | EndProject 20 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FA2BABA9-6435-428E-9848-C1C7920A0266}" 21 | ProjectSection(SolutionItems) = preProject 22 | README.md = README.md 23 | EndProjectSection 24 | EndProject 25 | Global 26 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 27 | Debug|x86 = Debug|x86 28 | Release|x86 = Release|x86 29 | EndGlobalSection 30 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 31 | {BC5E50DE-5AE8-481C-B6BE-CC500490770C}.Debug|x86.ActiveCfg = Debug|x86 32 | {BC5E50DE-5AE8-481C-B6BE-CC500490770C}.Debug|x86.Build.0 = Debug|x86 33 | {BC5E50DE-5AE8-481C-B6BE-CC500490770C}.Release|x86.ActiveCfg = Release|x86 34 | {BC5E50DE-5AE8-481C-B6BE-CC500490770C}.Release|x86.Build.0 = Release|x86 35 | {BF55FED0-B51D-420A-84A0-F63F677CCA2C}.Debug|x86.ActiveCfg = Debug|x86 36 | {BF55FED0-B51D-420A-84A0-F63F677CCA2C}.Debug|x86.Build.0 = Debug|x86 37 | {BF55FED0-B51D-420A-84A0-F63F677CCA2C}.Release|x86.ActiveCfg = Release|x86 38 | {BF55FED0-B51D-420A-84A0-F63F677CCA2C}.Release|x86.Build.0 = Release|x86 39 | {5579955A-C61D-4FBD-80DB-95C20E2AE00D}.Debug|x86.ActiveCfg = Debug|x86 40 | {5579955A-C61D-4FBD-80DB-95C20E2AE00D}.Debug|x86.Build.0 = Debug|x86 41 | {5579955A-C61D-4FBD-80DB-95C20E2AE00D}.Release|x86.ActiveCfg = Release|x86 42 | {5579955A-C61D-4FBD-80DB-95C20E2AE00D}.Release|x86.Build.0 = Release|x86 43 | {CDA74E9B-E9EE-493A-BD2E-C93AA250EF3B}.Debug|x86.ActiveCfg = Debug|x86 44 | {CDA74E9B-E9EE-493A-BD2E-C93AA250EF3B}.Debug|x86.Build.0 = Debug|x86 45 | {CDA74E9B-E9EE-493A-BD2E-C93AA250EF3B}.Release|x86.ActiveCfg = Release|x86 46 | {CDA74E9B-E9EE-493A-BD2E-C93AA250EF3B}.Release|x86.Build.0 = Release|x86 47 | {1916BB62-D744-4EB8-86B2-33D0B9315A15}.Debug|x86.ActiveCfg = Debug|x86 48 | {1916BB62-D744-4EB8-86B2-33D0B9315A15}.Debug|x86.Build.0 = Debug|x86 49 | {1916BB62-D744-4EB8-86B2-33D0B9315A15}.Release|x86.ActiveCfg = Release|x86 50 | {1916BB62-D744-4EB8-86B2-33D0B9315A15}.Release|x86.Build.0 = Release|x86 51 | {846B0CC1-2F97-4E7D-BEEB-3E9869AC76A0}.Debug|x86.ActiveCfg = Debug|x86 52 | {846B0CC1-2F97-4E7D-BEEB-3E9869AC76A0}.Debug|x86.Build.0 = Debug|x86 53 | {846B0CC1-2F97-4E7D-BEEB-3E9869AC76A0}.Release|x86.ActiveCfg = Release|x86 54 | {846B0CC1-2F97-4E7D-BEEB-3E9869AC76A0}.Release|x86.Build.0 = Release|x86 55 | {412F037C-A922-4EB2-BB7B-5F1D509FC400}.Debug|x86.ActiveCfg = Debug|x86 56 | {412F037C-A922-4EB2-BB7B-5F1D509FC400}.Debug|x86.Build.0 = Debug|x86 57 | {412F037C-A922-4EB2-BB7B-5F1D509FC400}.Release|x86.ActiveCfg = Release|x86 58 | {412F037C-A922-4EB2-BB7B-5F1D509FC400}.Release|x86.Build.0 = Release|x86 59 | EndGlobalSection 60 | GlobalSection(SolutionProperties) = preSolution 61 | HideSolutionNode = FALSE 62 | EndGlobalSection 63 | EndGlobal 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hugoberry/PowerQueryExtensions/93ee49369829fe90bc8ce856b36ea8f87b65ce24/README.md -------------------------------------------------------------------------------- /build/Date.mez: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hugoberry/PowerQueryExtensions/93ee49369829fe90bc8ce856b36ea8f87b65ce24/build/Date.mez -------------------------------------------------------------------------------- /build/List.mez: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hugoberry/PowerQueryExtensions/93ee49369829fe90bc8ce856b36ea8f87b65ce24/build/List.mez -------------------------------------------------------------------------------- /build/Number.mez: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hugoberry/PowerQueryExtensions/93ee49369829fe90bc8ce856b36ea8f87b65ce24/build/Number.mez -------------------------------------------------------------------------------- /build/Splitter.mez: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hugoberry/PowerQueryExtensions/93ee49369829fe90bc8ce856b36ea8f87b65ce24/build/Splitter.mez -------------------------------------------------------------------------------- /build/Table.mez: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hugoberry/PowerQueryExtensions/93ee49369829fe90bc8ce856b36ea8f87b65ce24/build/Table.mez -------------------------------------------------------------------------------- /build/Text.mez: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hugoberry/PowerQueryExtensions/93ee49369829fe90bc8ce856b36ea8f87b65ce24/build/Text.mez -------------------------------------------------------------------------------- /build/Value.mez: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hugoberry/PowerQueryExtensions/93ee49369829fe90bc8ce856b36ea8f87b65ce24/build/Value.mez -------------------------------------------------------------------------------- /media/Modules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hugoberry/PowerQueryExtensions/93ee49369829fe90bc8ce856b36ea8f87b65ce24/media/Modules.png -------------------------------------------------------------------------------- /src/Date/Date.mproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Debug 4 | 2.0 5 | {5579955a-c61d-4fbd-80db-95c20e2ae00d} 6 | Exe 7 | MyRootNamespace 8 | MyAssemblyName 9 | False 10 | False 11 | False 12 | False 13 | False 14 | False 15 | False 16 | False 17 | False 18 | False 19 | 1000 20 | Yes 21 | Date 22 | 23 | 24 | false 25 | 26 | bin\Debug\ 27 | 28 | 29 | false 30 | ..\..\build\ 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | Code 41 | 42 | 43 | 44 | 45 | Content 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /src/Date/Date.pq: -------------------------------------------------------------------------------- 1 | section Date; 2 | ///////////////////////// 3 | // Date // 4 | ///////////////////////// 5 | // Basic calendar 6 | shared Date.Calendar = 7 | Document( 8 | "Date.Calendar", 9 | "Generate a calendar table for a given date span - can be text or proper dates. Current columns are Date, DayOfWeek, Month, MonthNum, WeekStartData, WeekStart, Year, YearMonth", 10 | {[ Description = "2016 calendar", Code ="PBI[Date.Calendar](""1/1/2016"", ""12/31/2016""", Result = "2016 calendar"]}, 11 | (start as any, end as any) => 12 | let 13 | StartDate = Date.From(start), 14 | EndDate = Date.From(end), 15 | Source = Date.DatesBetween(StartDate, EndDate), 16 | FromList = Table.FromList(Source, Splitter.SplitByNothing(), null, null, ExtraValues.Error), 17 | Date = Table.RenameColumns(FromList,{{"Column1", "Date"}}), 18 | DayOfWeek = Table.AddColumn(Date, "Day of Week", each Date.DayName([Date])), 19 | Month = Table.AddColumn(DayOfWeek, "Month", each Date.MonthName([Date])), 20 | MonthNum = Table.AddColumn(Month, "MonthNumber", each Date.Month([Date])), 21 | WeekStartDate = Table.AddColumn(MonthNum, "WeekStartDate", each Date.StartOfWeek([Date])), 22 | WeekStart = Table.AddColumn(WeekStartDate, "Week Start", each [Month] & " " & Text.From(Date.Day([WeekStartDate]))), 23 | Year = Table.AddColumn(WeekStart, "Year", each Date.Year([Date])), 24 | YearMonth = Table.AddColumn(Year, "YearMonth", each Number.From(Text.From([Year]) & (if [MonthNumber] < 10 then "0" else "") & Text.From([MonthNumber]))), 25 | Result = YearMonth 26 | in 27 | Result 28 | ); 29 | shared Date.DatesBetween = Document( 30 | "Date.DatesBetween", 31 | "Returns a list of dates in a given span (inclusive). Start and end parameters can be any order", 32 | {[Description = "Date range", Code = "PBI[Date.DatesBetween](""1/1/2016"", ""1/3/2016"")", Result="{""1/1/2016"", ""1/2/2016"", ""1/3/2016""}" ]}, 33 | (start as any, end as any) => 34 | let 35 | StartDate = Date.From(start), 36 | EndDate = Date.From(end), 37 | adjustedStart = List.Min({StartDate, EndDate}), 38 | adjustedEnd = List.Max({StartDate, EndDate}), 39 | GetDates = (start as date, end as date, dates as list)=> if start > end then dates else @GetDates(Date.AddDays(start, 1), end, List.Combine({dates, {start}})), 40 | Dates = GetDates(adjustedStart, adjustedEnd, {}) 41 | in Dates 42 | ); 43 | 44 | shared Date.DayName = Document( 45 | "Date.DayName", 46 | "Returns the English day of the week name for a date", 47 | {[ Description = "Get the day name", Code="Date.DayName(""9/9/2016"")", Result="Friday"]}, 48 | (date as any) => Switch(Date.DayOfWeek(DateTime.From(date)), {0, 1, 2, 3, 4, 5, 6}, {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, null) 49 | ); 50 | 51 | ///////////////////////// 52 | // Dependencies // 53 | ///////////////////////// 54 | 55 | Document = (name as text, description as text, valueOrExample as any, optional valueIfExample as any) => 56 | let 57 | value = if valueIfExample is null then valueOrExample else valueIfExample, 58 | examples = if valueIfExample is null then {} else valueOrExample 59 | in 60 | Value.ReplaceType(value, Value.Type(value) meta [ 61 | Documentation.Name = name, 62 | Documentation.Description = description, 63 | // [Description = "", Code="", Result =""] 64 | Documentation.Examples = examples 65 | ]); 66 | 67 | Switch = 68 | Document( 69 | "Switch", 70 | "Given a value, find it's paired item
"& 71 | "Switch(value as any, cases as list, results as list, optional default as any)
" & 72 | "Switch(value as any, pairs as list, optional default as any)", 73 | { 74 | [ Description = "Using separate lists", Code = "Switch(1, {1, 2, 3}, {""A"", ""B"", ""C""})", Result = "A"], 75 | [ Description = "Using one paired list", Code = "Switch(1, {{1, ""A""}, {2, ""B""}, {3, ""C""}})", Result = "A"] 76 | }, 77 | (value as any, casesOrPairs as list, optional resultsOrDefault as any, optional default as any) => 78 | let 79 | hasPairs = List.First(casesOrPairs) is list, 80 | usingPairs = 81 | let 82 | targetPosition = List.PositionOf(casesOrPairs, value, Occurrence.First, (case, theValue) => theValue = case{0}) 83 | in 84 | if targetPosition = -1 then resultsOrDefault else casesOrPairs{targetPosition}{1}, 85 | usingCases = 86 | let 87 | cases = casesOrPairs, 88 | results = resultsOrDefault 89 | in 90 | if List.IsEmpty(cases) or List.IsEmpty(results) then default else if value = List.First(cases) then List.First(results) else @Switch(value, List.Skip(cases, 1), List.Skip(results, 1), default) 91 | in 92 | if hasPairs then usingPairs else usingCases 93 | ); -------------------------------------------------------------------------------- /src/Date/README.md: -------------------------------------------------------------------------------- 1 | # Date Module 2 | 3 | ## Date.Calendar 4 | Generate a calendar table for a given date span - can be text or proper dates. Current columns are Date, DayOfWeek, Month, MonthNum, WeekStartData, WeekStart, Year, YearMonth 5 | ## Date.DatesBetween 6 | Returns a list of dates in a given span (inclusive). Start and end parameters can be any order 7 | ## Date.DayName 8 | Returns the English day of the week name for a date 9 | -------------------------------------------------------------------------------- /src/List/List.mproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Debug 4 | 2.0 5 | 6 | 7 | Exe 8 | MyRootNamespace 9 | MyAssemblyName 10 | False 11 | False 12 | False 13 | False 14 | False 15 | False 16 | False 17 | False 18 | False 19 | False 20 | 1000 21 | Yes 22 | List 23 | 24 | 25 | false 26 | 27 | bin\Debug\ 28 | 29 | 30 | false 31 | ..\..\build\ 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | Code 42 | 43 | 44 | 45 | 46 | Content 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /src/List/List.pq: -------------------------------------------------------------------------------- 1 | section List; 2 | ///////////////////////// 3 | // List // 4 | ///////////////////////// 5 | shared List.Flatten = Document( 6 | "List.Flatten", 7 | "Recursively flattens list elements. The end result is a single list", 8 | { [ Description = "Flattening nested lists into one", Code = "List.Flatten({ 1, 2, Table.FromRecords({[x=1]}), {3, 4, 5} })", Result = "{ 1, 2, Table.FromRecords({[x=1]}), 3, 4, 5}"] }, 9 | (list as list) => List.Accumulate(list, {}, (state, current) => 10 | let 11 | currentListContent = if current is list then @List.Flatten(current) else {current} 12 | in 13 | List.Combine({state, currentListContent}) 14 | ) 15 | ); 16 | shared List.From = Document( 17 | "List.From", 18 | "Converts a text representation of a list into a list of the elements. Items are considered to be split by ,", 19 | { [ Description = "Convert a text list", Code = "List.From(""{A, B, C}"")", Result = "{ ""A"", ""B"", ""C"" }"] }, 20 | (simpleTextList as text) => 21 | let 22 | trimWhitespace = Text.Trim(simpleTextList), 23 | listToSplit = Text.TrimEnd(Text.TrimStart(trimWhitespace, "{"), "}"), 24 | Result = List.Transform(Text.Split(listToSplit, ","), each Text.Trim(_)) 25 | in 26 | Result 27 | ); 28 | shared List.ToText = Document( 29 | "List.ToText", 30 | "Converts a list to a textual representation. Inverse of List.From", 31 | { [ Description = "Conver to text", Code = "List.ToText({ 1, 2, 3})", Result = """{1, 2, 3}"""] }, 32 | (list as list) => 33 | List.Accumulate(list, "{", (state, current) => current & Text.From(current)) & "}" 34 | ); 35 | 36 | shared List.Swap = (_,_from as number ,_to as number) => 37 | let 38 | from = List.Min({_from,_to}), 39 | to = List.Max({_from,_to}) 40 | in if from=to then _ else 41 | List.Range(_,0,from) 42 | &{_{to}} 43 | &List.Range(_,from+1,to-from-1) 44 | &{_{from}} 45 | &List.Range(_,to+1); 46 | 47 | shared List.Shuffle = (n) => 48 | List.Accumulate({0..(n-2)},{0..n},(_,iterator)=> 49 | List.Swap(List.Buffer(_), 50 | Number.Round(Number.RandomBetween(iterator,n)), 51 | iterator 52 | )); 53 | ///////////////////////// 54 | // Dependencies // 55 | ///////////////////////// 56 | 57 | Document = (name as text, description as text, valueOrExample as any, optional valueIfExample as any) => 58 | let 59 | value = if valueIfExample is null then valueOrExample else valueIfExample, 60 | examples = if valueIfExample is null then {} else valueOrExample 61 | in 62 | Value.ReplaceType(value, Value.Type(value) meta [ 63 | Documentation.Name = name, 64 | Documentation.Description = description, 65 | // [Description = "", Code="", Result =""] 66 | Documentation.Examples = examples 67 | ]); -------------------------------------------------------------------------------- /src/List/README.md: -------------------------------------------------------------------------------- 1 | # List Module 2 | 3 | ## List.Flatten 4 | Recursively flattens list elements. The end result is a single list 5 | 6 | ## List.From 7 | Converts a text representation of a list into a list of the elements. Items are considered to be split by , 8 | 9 | ## List.ToText 10 | Converts a list to a textual representation. Inverse of List.From 11 | 12 | ## List.Swap 13 | Swaping the elements of a list 14 | 15 | ## List.Shuffle 16 | Generate a random shuffle of the elements in a list -------------------------------------------------------------------------------- /src/Number/Number.mproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Debug 4 | 2.0 5 | 6 | 7 | Exe 8 | MyRootNamespace 9 | MyAssemblyName 10 | False 11 | False 12 | False 13 | False 14 | False 15 | False 16 | False 17 | False 18 | False 19 | False 20 | 1000 21 | Yes 22 | Number 23 | 24 | 25 | false 26 | 27 | bin\Debug\ 28 | 29 | 30 | false 31 | ..\..\build\ 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | Code 42 | 43 | 44 | 45 | 46 | Content 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /src/Number/Number.pq: -------------------------------------------------------------------------------- 1 | section Number; 2 | ///////////////////////// 3 | // Number // 4 | ///////////////////////// 5 | shared Number.Digits = {0,1,2,3,4,5,6,7,8,9}; 6 | shared Number.ParseText = Document( 7 | "Number.ParseText", 8 | "Returns the first number (1+ consecutive digits) in the given text. Optionally, provide allowed characters to ignore them", 9 | {[ Description = "Allow commas", Code="PBI[Number.ParseText](""It's over 9,000!"", 0, {"",""})", Result = "9000" ]}, 10 | (text as text, optional startIndex as number, optional allowCharacters as list) => 11 | let 12 | consider = if startIndex is null then text else Text.Range(text,startIndex), 13 | _allowCharacters = if allowCharacters is null then {} else allowCharacters, 14 | numberSeries = List.FirstN(List.Skip(Text.ToList(consider), each not Text.IsNumber(_)), each Text.IsNumber(_) or List.Contains(_allowCharacters, _)) 15 | in 16 | if text is null then null else Text.FromList(numberSeries) 17 | ); 18 | shared Number.ToLetters = Document( 19 | "Number.ToLetters", 20 | "Converts a number (starting at 1) to an alphabet representation. Works like column headers in Excel.", 21 | {[ 22 | Description = "Column 27", 23 | Code = "PBI[Number.ToLetters](27)", 24 | Result = "AB" 25 | ]}, 26 | (value as number) => 27 | let 28 | GetLetter = (num as number) => 29 | let 30 | number = Number.Mod(num, 26), 31 | val = if number = 0 then 26 else number, 32 | valid = number < 26 and number > 0 33 | in 34 | if valid then Text.At(Text.Alphabet, val - 1) else error "Can't get letter for " & Text.From(num), 35 | func = (value as number, factor as number) => 36 | let 37 | ThisLetter = GetLetter(Number.RoundDown(value/Number.Power(26, factor))), 38 | Result = if value <= Number.Power(26, factor) then "" else @func(value, factor+1) & ThisLetter 39 | in 40 | Result 41 | in 42 | if value <= 26 then GetLetter(value) else func(value, 1) & GetLetter(value) 43 | ); 44 | shared Number.Reverse8BitInt = (x) => 45 | Number.Mod(Number.BitwiseAnd((x* 0x0202020202),0x010884422010),1023); 46 | shared Number.Reverse32BitInt = (x) => 47 | let 48 | b0 = Number.BitwiseAnd(x,0xff), 49 | b1 = Number.BitwiseShiftRight(Number.BitwiseAnd(x,0xff00),8), 50 | b2 = Number.BitwiseShiftRight(Number.BitwiseAnd(x,0xff0000),16), 51 | b3 = Number.BitwiseShiftRight(Number.BitwiseAnd(x,0xff000000),24) 52 | in 53 | Number.BitwiseOr( 54 | Number.BitwiseOr( 55 | Number.BitwiseShiftLeft(Number.Reverse8BitInt(b0),24), 56 | Number.BitwiseShiftLeft(Number.Reverse8BitInt(b1),16)), 57 | Number.BitwiseOr( 58 | Number.BitwiseShiftLeft(Number.Reverse8BitInt(b2),8), 59 | Number.Reverse8BitInt(b3))); 60 | 61 | shared Number.DecToBin = (num as number)=> 62 | List.Last( 63 | List.Generate( 64 | () =>[reminder = num, 65 | binString= Number.ToText(Number.BitwiseAnd(reminder,1))], 66 | each [reminder]> 0, 67 | each [reminder = Number.BitwiseShiftRight([reminder],1), 68 | binString= Number.ToText(Number.BitwiseAnd(reminder,1))&[binString]], 69 | each [binString] 70 | )); 71 | 72 | shared Number.HexToDec = (hexString as text) 73 | => Expression.Evaluate("0x"&hexString); 74 | ///////////////////////// 75 | // Dependencies // 76 | ///////////////////////// 77 | 78 | Document = (name as text, description as text, valueOrExample as any, optional valueIfExample as any) => 79 | let 80 | value = if valueIfExample is null then valueOrExample else valueIfExample, 81 | examples = if valueIfExample is null then {} else valueOrExample 82 | in 83 | Value.ReplaceType(value, Value.Type(value) meta [ 84 | Documentation.Name = name, 85 | Documentation.Description = description, 86 | // [Description = "", Code="", Result =""] 87 | Documentation.Examples = examples 88 | ]); 89 | 90 | Text.Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; 91 | Text.FromList = (list as list) => List.Accumulate(list, "", (state, current) => state & Text.From(current)); 92 | Text.IsNumber = (text as text) => try Number.FromText(text) is number otherwise false; -------------------------------------------------------------------------------- /src/Number/README.md: -------------------------------------------------------------------------------- 1 | # Number Module 2 | 3 | ## Number.Digits 4 | 5 | ## Number.ParseText 6 | Returns the first number (1+ consecutive digits) in the given text. Optionally, provide allowed characters to ignore them 7 | 8 | ## Number.ToLetters 9 | Converts a number (starting at 1) to an alphabet representation. Works like column headers in Excel. 10 | 11 | ## Number.Reverse8BitInt 12 | Reverses the bits in an 8bit representation of a number 13 | 14 | ## Number.Reverse32BitInt 15 | Reverses the bits in a 32bit representation of a number 16 | 17 | ## Number.DecToBin 18 | Base 10 to binary conversion 19 | 20 | ## Number.HexToDec 21 | Changing hexadecimal representation of a number to decimal 22 | -------------------------------------------------------------------------------- /src/Splitter/README.md: -------------------------------------------------------------------------------- 1 | # Splitter Module 2 | 3 | ## Splitter.SplitTextByNonAlpha 4 | Splits text by characters that aren't [A-Za-z] 5 | 6 | ## Splitter.SplitTextByNotIn 7 | Splits text on any characters that aren't the provided 'safe' characters 8 | -------------------------------------------------------------------------------- /src/Splitter/Splitter.mproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Debug 4 | 2.0 5 | 6 | 7 | Exe 8 | MyRootNamespace 9 | MyAssemblyName 10 | False 11 | False 12 | False 13 | False 14 | False 15 | False 16 | False 17 | False 18 | False 19 | False 20 | 1000 21 | Yes 22 | Splitter 23 | 24 | 25 | false 26 | 27 | bin\Debug\ 28 | 29 | 30 | false 31 | ..\..\build\ 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | Code 42 | 43 | 44 | 45 | 46 | Content 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /src/Splitter/Splitter.pq: -------------------------------------------------------------------------------- 1 | section Splitter; 2 | ///////////////////////// 3 | // Splitters // 4 | ///////////////////////// 5 | shared Splitter.SplitTextByNonAlpha = Document( 6 | "Splitter.SplitTextByNonAlpha", 7 | "Splits text by characters that aren't [A-Za-z]", 8 | {[ Description = "Split text", Code="PBI[Splitter.SplitTextByNonAlpha](""A1B,C"")", Result = "{ ""A"", ""B"", ""C"" }"]}, 9 | (line as text) => Splitter.SplitTextByNotIn(Text.Alphabet) 10 | ); 11 | 12 | shared Splitter.SplitTextByNotIn = Document( 13 | "Splitter.SplitTextByNotIn", 14 | "Splits text on any characters that aren't the provided 'safe' characters", 15 | {[ Description = "Split on non-alphanumeric", Code = "PBI[Splitter.SplitTextByNotIn](PBI[Text.Alphanumeric])(""Power BI is #1"")", Result = "{""Power BI is "", ""1""}"]}, 16 | (safeCharacters as text) => (line as nullable text) => 17 | if line is null then 18 | {} 19 | else 20 | List.Accumulate(Text.ToList(line), {null} , (state, current) => 21 | let 22 | doSkip = not Text.Contains(safeCharacters, current), 23 | lastItem = List.Last(state), 24 | appendLast = lastItem<>null 25 | in 26 | if doSkip then 27 | if lastItem is null then 28 | state 29 | else 30 | List.Combine({state, {null}}) 31 | else if appendLast then 32 | List.Combine({List.RemoveLastN(state, 1), {lastItem & current}}) 33 | else 34 | List.Combine({List.RemoveLastN(state, 1), {current}})) 35 | ); 36 | 37 | ///////////////////////// 38 | // Dependencies // 39 | ///////////////////////// 40 | Document = (name as text, description as text, valueOrExample as any, optional valueIfExample as any) => 41 | let 42 | value = if valueIfExample is null then valueOrExample else valueIfExample, 43 | examples = if valueIfExample is null then {} else valueOrExample 44 | in 45 | Value.ReplaceType(value, Value.Type(value) meta [ 46 | Documentation.Name = name, 47 | Documentation.Description = description, 48 | // [Description = "", Code="", Result =""] 49 | Documentation.Examples = examples 50 | ]); 51 | Text.Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; -------------------------------------------------------------------------------- /src/Table/README.md: -------------------------------------------------------------------------------- 1 | # Table Module 2 | 3 | ## Table.AddBlankRow 4 | Transforms a column's value into its nested value -- if it eventually finds only one 5 | 6 | ## Table.DrillIntoColumn 7 | 8 | ## Table.EnterDataFormula 9 | Convert a table into a formula that's compatible with the Enter Data UI. This function returns an expression you can copy and paste to edit the table using Enter Data. Paste the formula, then click the gear icon next to the step name. This is an easy way to allow people to customize a table you've queried 10 | 11 | ## Table.FromListCrossJoin 12 | Perform a cross join of lists 13 | 14 | ## Table.JsonDecode 15 | 16 | ## Table.JsonEncode 17 | 18 | ## Table.ReplaceValueIf 19 | Replaces a value if it matches a predicate 20 | 21 | ## Table.SplitColumnNames 22 | Splits camelCased and PascalCased column names. 23 | 24 | ## Table.SplitColumnText 25 | Splits camelCased and PascalCased text in a column. 26 | 27 | ## Table.TransformColumn 28 | 29 | ## Table.RenameColumn 30 | 31 | ## Table.RenameAndTransformColumn 32 | 33 | ## Table.ToXml 34 | Transforms a table into an XML document -------------------------------------------------------------------------------- /src/Table/Table.mproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Debug 4 | 2.0 5 | 6 | 7 | Exe 8 | MyRootNamespace 9 | MyAssemblyName 10 | False 11 | False 12 | False 13 | False 14 | False 15 | False 16 | False 17 | False 18 | False 19 | False 20 | 1000 21 | Yes 22 | Table 23 | 24 | 25 | false 26 | 27 | bin\Debug\ 28 | 29 | 30 | false 31 | ..\..\build\ 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | Code 42 | 43 | 44 | 45 | 46 | Content 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /src/Table/Table.pq: -------------------------------------------------------------------------------- 1 | section Table; 2 | ///////////////////////// 3 | // Table // 4 | ///////////////////////// 5 | 6 | shared Table.AddBlankRow = (table as table) => Table.Combine({table, Table.FromRecords({[]})}); 7 | // Transforms a column's value into its nested value -- if it eventually finds only one. Consider the following column: 8 | // MyCol 9 | // ----- 10 | // "a" 11 | // {{{"b"}}} 12 | // Table.FromRecords({[MyCol=Table.FromRecords({[col=2]})}) 13 | // {} 14 | // 15 | // Table.DrillColumn(table, "MyCol") will convert it to 16 | // 17 | // MyCol 18 | // ----- 19 | // "a" 20 | // "b" 21 | // 2 22 | // null 23 | shared Table.DrillIntoColumn = (table as table, columnName as text) => 24 | let 25 | FindValue = (value as any) => 26 | if value is list then 27 | if List.Count(value) = 1 then @FindValue(List.First(value)) 28 | else if List.Count(value) = 0 then null 29 | else error "Couldn't find single value" 30 | else if value is table then 31 | if Table.RowCount(value) = 1 then @FindValue(List.First(Table.ToColumns(value))) 32 | else if Table.RowCount(value) = 0 then null 33 | else error "Couldn't find single value" 34 | else value, 35 | Result = Table.TransformColumns(table, {{columnName, FindValue}}) 36 | in 37 | Result; 38 | 39 | shared Table.EnterDataFormula = 40 | Document( 41 | "Table.EnterDataFormula", 42 | "Convert a table into a formula that's compatible with the Enter Data UI. This function returns an expression you can copy and paste to editthe " & 43 | " table using Enter Data. Paste the formula, then click the gear icon next to the step name. This is an easy way to allow people to customize a table you've queried", 44 | {[ 45 | Description = "Simple table", 46 | Code = "Web.Page(Web.Contents(""https://en.wikipedia.org/wiki/List_of_Super_Bowl_champions"")){3}[Data]", 47 | Result = "Table.FromRows(Json.Document(Binary.Decompress(Binary.FromText(""jVVtb9s2EP4rRD4aRqI3S9TH2Gm7ZWkb1MGGom6Aq83IhBkyoOik2a8fT6Ski6Fu+yDdWea9Pffc8du3M342P7uVzrU/jrbZs7UTQgnb+q+lfzL/nFeLxIu0ror55pglWcK8viA6J3o96vVwJksSqvNRT5Ognn2fh1yuQClo2cq8/DCvmMbCPzmmUWaLkEYyn2GkNIhFEFUQvBN1FkQexGI2uP8kXtg73SjQO3YLzkrjMEgRn/NFEmrlpL66JLmnRM+JXhC9IvWR82lxWqvQz8KypTV6a9pYJxZ5nlex1mpMg5dEp98p5OR7TWEmqaYLkgY2eQ2avbegt7LdGlbUof1dApgLz/MASQCcF0EEpH3wDuJiNkfXWUAabT9YITRbwqvHeXsIPovBZ4S5jDWl3kdZjbqHfBbKmXUkGd1iA78ae2AfJGg36ZRH46QzTqKIrtLR1Wc4IA82FzemZZe68cRv2ReQu5Bs3vO/7JOt5kixEt886d45Vp1kg8u/oN1L3Tij2Rexaw9ST3qqAj95FIGmPFC4Jhl+lPAo2ZVRT/vgKeuHoeg9pWT2MqLnhCDkO6ccROSWoJx8NFZsLn7XO48pPBklcQCV6yNmdDJKPrbJj+LIejol9UmUj1Jr0RoH7E/pQWnQc9LPXNJ7ronngEkVyFaVszHh48MDKMOWUqlJN3VCZoDAUxMY6pwkiIASCmwu1u6c3ZijRDY8tpFeHQh5nIUq0r4O3Iotw//WApxTgnm5h5fDhHW3DGdhKDtRBOuMdsNHfhZDy7vCYoEdo8dhzkghePQP0C3uT+le2WovxUOfQfqmieX8Hkeuvh8M/eEtNB5XAXbShoesk3JM93YvFewE0hPYO2g8ejHbjHaExxEkha6kXzdag5M+oG5ATRuS9nF+UukKrGeqBr9ftNuHmT3xgDs6LKZ4BaR0g1wL1xdKAUaK3w+n7+DxCbo1tjxut6CFsFNGuAXehvhslfC9YGuQejIOjslogkv4SgrfgNUebDOWk77hNh1f/OfS+RXmx+o9qK3R0zZ8jHIncA5bIdiddPCL8/VpDCv/Nh5nD/jO460mrPBSD1Hw10qJZ9Fdsf5qexmi4IOuUxzXT5uLy8HgSjhrpGM30vyP09f+PmmNfvYLQLBraI5g/9voN3NscS3fiZ/wbzG+/wM="", BinaryEncoding.Base64), Compression.Deflate)), let _t = ((type text) meta [Serialized.Text = true]) in type table [Appearances = _t, Team = _t, Wins = _t, Losses = _t, Winning 48 | percentage = _t, Season(s) = _t])" 49 | ]}, 50 | (table as table) => 51 | let 52 | Encoded = Table.JsonEncode(table), 53 | ColumnMeta= "[" & Text.Range(List.Accumulate(Table.ColumnNames(table), "", (state, current) => state & ", " & current & " = _t"), 2) & "]", 54 | Text = 55 | "Table.FromRows(Json.Document(Binary.Decompress(Binary.FromText(""" & Encoded & """, BinaryEncoding.Base64), Compression.Deflate))," & 56 | " let _t = ((type text) meta [Serialized.Text = true]) in type table " & ColumnMeta & ")" 57 | in 58 | Text 59 | ); 60 | // if fieldNames aren't specified, use the field names from the first row of the column. 61 | 62 | // Perform a cross join of lists. Example usage: 63 | // Table.FromListCrossJoin({ {ColorsTable[ColorName], "Color"}, {NumbersTable[Number], "Number"}}) 64 | // Will give me a new table with two columns, "Color" and "Number" which contains one row for each possible 65 | // combination of colors and numbers 66 | // Table.FromListCrossJoin({{"Red", "Blue"}, "Color"}, {{1,2,3}, "Number"}}) = 67 | // Table.FromRecords({[Color="Red", Number=1],[Color="Red", Number = 2],[Color="Red", Number = 3],[Color="Blue", Number=1],[Color="Blue", Number=2],[Color="Blue", Number=3]}) 68 | shared Table.FromListCrossJoin = (listColumnNamePairs as any) => 69 | let 70 | remainingPairs = List.Skip(listColumnNamePairs, 1), 71 | current = List.First(listColumnNamePairs), 72 | theList = List.First(current), 73 | columnName = List.First(List.Skip(current),1), 74 | firstTable = Table.FromList(theList, null, {columnName}), 75 | doStuff = (table as table, remainingPairs as list) => 76 | if List.Count(remainingPairs) <= 0 then table else 77 | let 78 | current = List.First(remainingPairs), 79 | theList = List.First(current), 80 | columnName = List.First(List.Skip(current), 1), 81 | nextTable = Table.ExpandListColumn(Table.AddColumn(table, columnName, each theList), columnName) 82 | in @doStuff(nextTable, List.Skip(remainingPairs, 1)), 83 | Result = doStuff(firstTable, remainingPairs) 84 | in 85 | Result; 86 | shared Table.JsonDecode = (encoded as text) => 87 | let 88 | Decompressed = Binary.Decompress(Binary.FromText(encoded, BinaryEncoding.Base64), Compression.Deflate), 89 | Decoded = Table.FromRows(Json.Document(Decompressed), let _t = ((type text) meta [Serialized.Text = true]) in type table [Name = _t, Number = _t]) 90 | in 91 | Decoded; 92 | shared Table.JsonEncode = (table as table) => 93 | let 94 | Rows = Table.ToRows(table), 95 | Json = Json.FromValue(Rows), 96 | Compressed = Binary.Compress(Json, Compression.Deflate), 97 | Encoded = Binary.ToText(Compressed, BinaryEncoding.Base64) 98 | in 99 | Encoded; 100 | // Replaces a value if it matches a predicate 101 | shared Table.ReplaceValueIf = (table as table, replaceIf as function, after as any, columnNameOrList as any) => 102 | Table.ReplaceValue(table, null,after, (text, old, new)=>if replaceIf(text) then new else text, if columnNameOrList is list then columnNameOrList else {columnNameOrList}); 103 | // Splits camelCased and PascalCased column names. 104 | shared Table.SplitColumnNames = (table as table) => Table.RenameColumns(table, List.Transform(Table.ColumnNames(table), each {_, Text.SplitCamelCase(_)})); 105 | // Splits camelCased and PascalCased text in a column. 106 | shared Table.SplitColumnText = (table as table, columns as list) => if List.Count(columns) = 0 then table else Table.TransformColumns(@Table.SplitColumnText(table, List.Skip(columns, 1)), {{List.First(columns), Text.SplitCamelCase}}); 107 | shared Table.TransformColumn = (table as table, column as text, transform as function) => Table.TransformColumns(table, {{column, transform}}); 108 | shared Table.RenameColumn = (table as table, column as text, newName as text) => Table.RenameColumns(table, {{column, newName}}); 109 | shared Table.RenameAndTransformColumn = (table, currentName as text, newName as text, transform as function) => Table.TransformColumn(Table.RenameColumns(table, {currentName, newName}), newName, transform); 110 | 111 | shared Table.ToXml = (t) => 112 | ""& 113 | {List.Accumulate( 114 | Table.ToRecords(t), 115 | "", 116 | (state,curr)=>state& 117 | ""& 118 | {List.Accumulate( 119 | Record.FieldNames(curr), 120 | "", 121 | (s,c)=>s&Text.Format("<#{0}>#{1}", 122 | {c, 123 | Record.Field(curr,c)}) 124 | )} 125 | &"" 126 | )} 127 | &"
"; 128 | ///////////////////////// 129 | // Dependencies // 130 | ///////////////////////// 131 | 132 | Document = (name as text, description as text, valueOrExample as any, optional valueIfExample as any) => 133 | let 134 | value = if valueIfExample is null then valueOrExample else valueIfExample, 135 | examples = if valueIfExample is null then {} else valueOrExample 136 | in 137 | Value.ReplaceType(value, Value.Type(value) meta [ 138 | Documentation.Name = name, 139 | Documentation.Description = description, 140 | // [Description = "", Code="", Result =""] 141 | Documentation.Examples = examples 142 | ]); 143 | Text.Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; 144 | Text.IsUpperCase = (text as text) => List.AllTrue(List.Transform(Text.ToList(text), (letter)=>Text.Contains(Text.Alphabet, letter) and letter = Text.Upper(letter))); 145 | Text.SplitCamelCase = (text as nullable text) => if text is null then null else List.Accumulate(Text.ToList(text),"", (state, current) => 146 | let 147 | PreviousLetter = Text.End(state, 1), 148 | Ignore = (text as text) => text = " " or text = "." 149 | in 150 | state & 151 | (if 152 | not Text.IsUpperCase(PreviousLetter) and 153 | not Ignore(PreviousLetter) and 154 | not Ignore(current) and 155 | Text.IsUpperCase(current) 156 | then 157 | " " else "" ) & 158 | current); -------------------------------------------------------------------------------- /src/Text/README.md: -------------------------------------------------------------------------------- 1 | # Text Module 2 | 3 | ## Text.Alphabet 4 | The alphabet 5 | 6 | ## Text.AlphaNumeric 7 | The alphabet and digits 8 | 9 | ## Text.IsUpperCase 10 | Is text all uppercase? returns false if any non-alpha characters are present 11 | 12 | ## Text.IsAlpha 13 | 14 | ## Text.RemoveExtraWhitespace 15 | 16 | ## Text.SplitCamelCase 17 | Splits camelCased and PascalCased text and separates by a space. Ex: "thisIsAColumn" -> "this Is A Column" 18 | 19 | ## Text.SplitOnNotIn 20 | 21 | ## Text.SplitOnNonAlpha 22 | 23 | ## Text.Substring 24 | 25 | ## Text.IsNumber 26 | 27 | ## Text.PositionAfter 28 | 29 | ## Text.Until 30 | 31 | ## Text.AsciiOnly 32 | Filters out all non-ascii characters from a string 33 | 34 | ## Text.Between 35 | Grabs the substring between the specified 'after' and 'before' strings 36 | 37 | ## Text.ContainsAny 38 | Check if a string contains any of the keywords from a given list 39 | 40 | ## Text.Count 41 | Returns the number of occurrences of a substring (needle) within another string (haystack). 42 | 43 | ## Text.EachBetween 44 | Grabs the substring between the specified 'after' and 'before' strings 45 | 46 | ## Text.EachFromTo 47 | Grabs the substring between the specified 'after' and 'before' strings 48 | 49 | ## Text.FromTo 50 | Grabs the first substring from the specified 'From' up to the 'UpTo' string 51 | 52 | ## Text.Like 53 | Allows doing fuzzy string comparisons akin to SQL's LIKE 54 | 55 | ## Text.PowerTrim 56 | Function removes double presents of specified characters.By default remove double spaces and leading + ending spaces. Like TRIM function in Excel 57 | 58 | ## Text.RemoveSymbols 59 | Remove all uicode symbols from text 60 | 61 | ## Text.ReplaceAll 62 | Do multiple text replacements in one function call, passing the replacements as a list of lists 63 | 64 | ## -------------------------------------------------------------------------------- /src/Text/Text.mproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Debug 4 | 2.0 5 | 6 | 7 | Exe 8 | MyRootNamespace 9 | MyAssemblyName 10 | False 11 | False 12 | False 13 | False 14 | False 15 | False 16 | False 17 | False 18 | False 19 | False 20 | 1000 21 | Yes 22 | Text 23 | 24 | 25 | false 26 | 27 | bin\Debug\ 28 | 29 | 30 | false 31 | ..\..\build\ 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | Code 42 | 43 | 44 | 45 | 46 | Content 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /src/Text/Text.pq: -------------------------------------------------------------------------------- 1 | section Text; 2 | 3 | ///////////////////////// 4 | // Text // 5 | ///////////////////////// 6 | shared Text.Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; 7 | shared Text.AlphaNumeric = Text.Alphabet & "0123456789"; 8 | shared Text.FromList = (list as list) => List.Accumulate(list, "", (state, current) => state & Text.From(current)); 9 | // Is text all uppercase? returns false if any non-alpha characters are present 10 | shared Text.IsUpperCase = (text as text) => List.AllTrue(List.Transform(Text.ToList(text), (letter)=>Text.Contains(Text.Alphabet, letter) and letter = Text.Upper(letter))); 11 | shared Text.IsAlpha = (text as text) => List.MatchesAll(Text.ToList(text), each Text.Contains(Text.Alphabet, _)); 12 | shared Text.RemoveExtraWhitespace = (text as text) => Text.Combine(Splitter.SplitTextByWhitespace()(text)," "); 13 | // Splits camelCased and PascalCased text and separates by a space. Ex: "thisIsAColumn" -> "this Is A Column" 14 | shared Text.SplitCamelCase = (text as nullable text) => if text is null then null else List.Accumulate(Text.ToList(text),"", (state, current) => 15 | let 16 | PreviousLetter = Text.End(state, 1), 17 | Ignore = (text as text) => text = " " or text = "." 18 | in 19 | state & 20 | (if 21 | not Text.IsUpperCase(PreviousLetter) and 22 | not Ignore(PreviousLetter) and 23 | not Ignore(current) and 24 | Text.IsUpperCase(current) 25 | then 26 | " " else "" ) & 27 | current); 28 | shared Text.SplitOnNotIn = (line as nullable text, validCharacters as text) => Splitter.SplitTextByNotIn(validCharacters)(line); 29 | shared Text.SplitOnNonAlpha = (line as nullable text) => 30 | if line is null then null else List.Accumulate(Text.ToList(line), {null} , (state, current) => 31 | let 32 | doSkip = not Text.Contains(Text.Alphabet, current), 33 | lastItem = List.Last(state), 34 | appendLast = lastItem<>null 35 | in 36 | if doSkip then 37 | if lastItem is null then 38 | state 39 | else 40 | List.Combine({state, {null}}) 41 | else 42 | if appendLast then 43 | List.Combine({List.RemoveLastN(state, 1), {lastItem & current}}) 44 | else 45 | List.Combine({List.RemoveLastN(state, 1), {current}})); 46 | shared Text.Substring = (text as text, start as number, optional count as number) => 47 | let 48 | start = if start >= 0 then start else error "start index should be >= 0", 49 | end = if count = null then Text.Length(text) else if count <= Text.Length(text) then count else error "count should be <= text length", 50 | textList = Text.ToList(text), 51 | substr = Text.FromList(List.FirstN(List.Skip(textList, start), end - start)) 52 | in substr; 53 | shared Text.IsNumber = (text as text) => try Number.FromText(text) is number otherwise false; 54 | shared Text.PositionAfter = (text as nullable text, substring as text) => 55 | let 56 | firstIndex = Text.PositionOf(text, substring), 57 | indexAfter = if firstIndex >=0 then firstIndex + Text.Length(substring) else -1 58 | in 59 | if text is null then -1 else if indexAfter >= 0 and indexAfter < Text.Length(text) then indexAfter else -1; 60 | shared Text.Until = (text as text, endDelimiter as text, optional startIndex as number) => 61 | let 62 | start = if startIndex = null then 0 else startIndex, 63 | textFromStart = Text.Substring(text, start), 64 | delimPosition = if Text.PositionOf(textFromStart, endDelimiter) >= 0 then Text.PositionOf(textFromStart, endDelimiter) else Text.Length(textFromStart) 65 | in 66 | if text is null then null else Text.Range(textFromStart, 0, delimPosition); 67 | shared Text.AsciiOnly = (s as text) as text => 68 | let 69 | Listified = Text.ToList(s), 70 | Numbered = List.Transform(Listified, each Character.ToNumber(_)), 71 | Filtered = List.Select(Numbered, each _ <= 255), 72 | Stringified = List.Transform(Filtered, each Character.FromNumber(_)), 73 | Joined = Text.Combine(Stringified, ""), 74 | Return = Joined 75 | in 76 | Return; 77 | 78 | shared Text.Between = (Haystack as text, After as text, Before as text) as text => 79 | let 80 | CutAfter = Text.Split(Haystack, After), 81 | CutBefore = Text.Split(CutAfter{1}, Before), 82 | Needle = if List.Count(CutAfter) > 1 83 | then (if List.Count(CutBefore) > 1 then CutBefore{0} else Error.Record("FindTextFailed","The text did not contain the keyword " & Before, Haystack)) 84 | else error Error.Record("FindTextFailed","The text did not contain the keyword " & After, Haystack) 85 | in Needle; 86 | 87 | shared Text.ContainsAny = (str as text, needles as list) as logical => 88 | let 89 | count = List.Count(needles) 90 | in 91 | 92 | List.AnyTrue( 93 | List.Generate( 94 | ()=>[i=0], 95 | each [i] < count, 96 | each [i=[i]+1], 97 | each Text.Contains(str,needles{[i]}) 98 | ) 99 | ); 100 | 101 | shared Text.Count = (Haystack as text, Needle as text) as number => 102 | List.Count(Text.Split(Haystack, Needle)) - 1; 103 | 104 | shared Text.EachBetween = (Haystack as text, After as text, Before as text) as list => 105 | let 106 | CutAfter = Text.Split(Haystack, After), 107 | SkipFirst = List.Skip(CutAfter), 108 | CutEach = List.Transform(SkipFirst, each Text.Split(_, Before){0}) 109 | in 110 | CutEach; 111 | 112 | shared Text.EachFromTo = 113 | (Haystack as text, After as text, Before as text) as text => 114 | let 115 | CutAfter = Text.Split(Haystack, After), 116 | SkipFirst = List.Skip(CutAfter), 117 | CutEach = List.Transform(SkipFirst, each After & Text.Split(_, Before){0} & Before) 118 | in 119 | CutEach; 120 | 121 | shared Text.FromTo = 122 | (Haystack as text, From as text, UpTo as text) as text => 123 | let 124 | CutAfter = Text.Split(Haystack, From), 125 | CutBefore = Text.Split(CutAfter{1}, UpTo), 126 | Needle = if List.Count(CutAfter) > 1 127 | then (if List.Count(CutBefore) > 1 then From & CutBefore{0} & UpTo else Error.Record("FindTextFailed","The text did not contain the keyword " & UpTo, Haystack)) 128 | else error Error.Record("FindTextFailed","The text did not contain the keyword " & From, Haystack) 129 | in Needle; 130 | 131 | shared Text.Like = (Phrase as text, Pattern as text) as logical => 132 | //Originally written by Chris Webb: https://cwebbbi.wordpress.com/2014/05/27/implementing-a-basic-likewildcard-search-function-in-power-query/ 133 | let 134 | //Split pattern up into a list using % as a delimiter 135 | PatternList = Text.Split(Pattern, "%"), 136 | //if the first character in the pattern is % then the first item in the list is an empty string 137 | StartsWithWc = (List.First(PatternList)=""), 138 | //if the last character in the pattern is % then the last item in the list is an empty string 139 | EndsWithWc = (List.Last(PatternList)=""), 140 | //if the first character is not % then we have to match the first string in the pattern with the opening characters of the phrase 141 | StartsTest = if (StartsWithWc=false) 142 | then Text.StartsWith(Phrase, List.First(PatternList)) 143 | else true, 144 | //if the last item is not % then we have to match the final string in the pattern with the final characters of the phrase 145 | EndsText = if (EndsWithWc=false) 146 | then Text.EndsWith(Phrase, List.Last(PatternList)) 147 | else true, 148 | //now we also need to check that each string in the pattern appears in the correct order in the phrase and to do this we need to declare a function PhraseFind 149 | PhraseFind = (Phrase as text, SearchString as list) => 150 | let 151 | //does the first string in the pattern appear in the phrase? 152 | StringPos = Text.PositionOf(Phrase, SearchString{0}, Occurrence.First), 153 | PhraseFindOutput = 154 | if 155 | //if string not find then return false 156 | (StringPos=-1) 157 | then false 158 | else if 159 | //we have found the string in the pattern, and if this is the last string in the pattern, return true 160 | List.Count(SearchString)=1 161 | then true 162 | else 163 | //if it isn't the last string in the pattern test the next string in the pattern by removing the first string from the pattern list and all text up to and including the string we have found in the phrase 164 | (true and 165 | @PhraseFind( 166 | Text.RemoveRange(Phrase, 0, StringPos + Text.Length(SearchString{0})), 167 | List.RemoveRange(SearchString, 0, 1))) 168 | in 169 | PhraseFindOutput, 170 | //return true if we have passed all tests 171 | Output = StartsTest and EndsText and PhraseFind(Phrase, PatternList) 172 | in 173 | Output; 174 | 175 | shared Text.PowerTrim = 176 | //Original is taked from Ken Puls's blog http://www.excelguru.ca/blog/2015/10/08/clean-whitespace-in-powerquery/ 177 | (text as text, optional char_to_trim as text) => 178 | let 179 | char = if char_to_trim = null then " " else char_to_trim, 180 | split = Text.Split(text, char), 181 | removeblanks = List.Select(split, each _ <> ""), 182 | result=Text.Combine(removeblanks, char) 183 | in 184 | result; 185 | 186 | shared Text.RemoveSymbols = 187 | //Originally written by Chris Webb: https://cwebbbi.wordpress.com/2014/08/18/removing-punctuation-from-text-in-power-query/ 188 | (inputtext as text) as text => 189 | let 190 | //get a list of lists containing the numbers of Unicode punctuation characters 191 | numberlists = {{0..31},{33..47},{58..64},{91..96},{123..191}}, 192 | //turn this into a single list 193 | combinedlist = List.Combine(numberlists), 194 | //get a list of all the punctuation characters that these numbers represent 195 | punctuationlist = List.Transform(combinedlist, each Character.FromNumber(_)), 196 | //some text to test this on 197 | //inputtext = "Hello! My name is Chris, and I'm hoping that this *cool* post will help you!", 198 | //the text with punctuation removed 199 | outputtext = Text.Remove(inputtext, punctuationlist) 200 | in 201 | outputtext; 202 | 203 | shared Text.ReplaceAll = 204 | (str as text, Replacements as list) as text => 205 | let 206 | count = List.Count(Replacements) 207 | in 208 | List.Last( 209 | List.Generate( 210 | ()=>[i=0, s=str], 211 | each [i] <= count, 212 | each [ 213 | s=Text.Replace([s],Replacements{[i]}{0},Replacements{[i]}{1}), 214 | i=[i]+1 215 | ], 216 | each [s] 217 | ) 218 | ); 219 | 220 | 221 | ///////////////////////// 222 | // Dependencies // 223 | ///////////////////////// 224 | 225 | 226 | Splitter.SplitTextByNotIn = (safeCharacters as text) => (line as nullable text) => 227 | if line is null then 228 | {} 229 | else 230 | List.Accumulate(Text.ToList(line), {null} , (state, current) => 231 | let 232 | doSkip = not Text.Contains(safeCharacters, current), 233 | lastItem = List.Last(state), 234 | appendLast = lastItem<>null 235 | in 236 | if doSkip then 237 | if lastItem is null then 238 | state 239 | else 240 | List.Combine({state, {null}}) 241 | else if appendLast then 242 | List.Combine({List.RemoveLastN(state, 1), {lastItem & current}}) 243 | else 244 | List.Combine({List.RemoveLastN(state, 1), {current}})); -------------------------------------------------------------------------------- /src/Value/README.md: -------------------------------------------------------------------------------- 1 | # Value Module 2 | 3 | ## Value.TypeText 4 | 5 | ## Value.ToText 6 | Returns a string representation of a value, which works even on containers, unlike the built-in Text.From() 7 | 8 | ## Value.TypeToText 9 | Returns a simple string representation of a value's type, which allows easy filtering, unlike the built-in Value.Type() 10 | 11 | ## Value.WaitFor 12 | Retry mechanism for function call -------------------------------------------------------------------------------- /src/Value/Value.mproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Debug 4 | 2.0 5 | 6 | 7 | Exe 8 | MyRootNamespace 9 | MyAssemblyName 10 | False 11 | False 12 | False 13 | False 14 | False 15 | False 16 | False 17 | False 18 | False 19 | False 20 | 1000 21 | Yes 22 | Value 23 | 24 | 25 | false 26 | 27 | bin\Debug\ 28 | 29 | 30 | false 31 | ..\..\build\ 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | Code 42 | 43 | 44 | 45 | 46 | Content 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /src/Value/Value.pq: -------------------------------------------------------------------------------- 1 | section Value; 2 | 3 | ///////////////////////// 4 | // Value // 5 | ///////////////////////// 6 | shared Value.TypeText = (value as any) => 7 | if value is binary then "binary" else 8 | if value is date then "date" else 9 | if value is datetime then "datetime" else 10 | if value is datetimezone then "datetimezone" else 11 | if value is duration then "duration" else 12 | if value is function then "function" else 13 | if value is list then "list" else 14 | if value is logical then "logical" else 15 | if value is none then "none" else 16 | if value is null then "null" else 17 | if value is number then "number" else 18 | if value is record then "record" else 19 | if value is table then "table" else 20 | if value is text then "text" else 21 | if value is time then "time" else 22 | if value is type then "type" else 23 | if value is any then "any" 24 | else error "unknown -- not a primitive type!"; 25 | 26 | shared Value.ToText = 27 | (Val as any, optional RecursTypes as logical) as text => 28 | let 29 | RecursTypes = if (RecursTypes<>null) then RecursTypes else false, 30 | Tried = (try Val), 31 | Value = if Tried[HasError] then Tried[Error] else Tried[Value], 32 | /* 33 | DurationVals = {Duration.Days, Duration.Hours, Duration.Minutes, Duration.Seconds}, 34 | DateVals = {Date.Year, Date.Month, Date.Day}, 35 | TimeVals = {Time.Hour, Time.Minute, Time.Second}, 36 | ZoneVals = {DateTimeZone.ZoneHours, DateTimeZone.ZoneMinutes}, 37 | GetNumbers = (vals as list, obj as any) as text => Text.Combine(List.Transform(vals, each Number.ToText(Function.Invoke(_, {obj}))), ","), 38 | */ 39 | CaseValues = { 40 | //{ (x)=> (try x)[HasError], "error " & @Value.ToText((try Value)[Error], RecursTypes) }, 41 | { (x)=> Value.Is(x, type type), Type.ToText(Value, RecursTypes) }, 42 | { (x)=> Value.Is(x, type function), 43 | let 44 | Type = Value.Type(Value), 45 | Params = Type.FunctionParameters(Type), 46 | Reqd = Type.FunctionRequiredParameters(Type), 47 | Ret = Type.FunctionReturn(Type) 48 | in 49 | "function (" & 50 | Record.TransformJoin(Params, (k,v) => 51 | (if List.PositionOf(Record.FieldNames(Params), k) >= Reqd then "optional " else "") & 52 | k & " as " & @Value.ToText(v, RecursTypes) 53 | ) 54 | & ") as " & @Value.ToText(Ret, RecursTypes) 55 | }, 56 | { (x)=> Value.Is(x, type table), "#table(" & @Value.ToText(Table.ColumnNames(Value), RecursTypes) & ", " & @Value.ToText(Table.ToRows(Value), RecursTypes) & ")"}, 57 | { (x)=> Value.Is(x, type record), "[" & 58 | Record.TransformJoin(Value, (k,v) => k & "=" & @Value.ToText(v, RecursTypes)) 59 | & "]" }, 60 | { (x)=> Value.Is(x, type list), "{" & Text.Combine(List.Transform(Value, each @Value.ToText(_, RecursTypes)), ", ") & "}" }, 61 | { (x)=> x = null, "null" }, 62 | /* 63 | { (x)=> Value.Is(x, type text), """" & Value & """" }, 64 | { (x)=> Value.Is(x, type binary), "#binary(""" & Binary.ToText(Value) & """)" }, 65 | { (x)=> Value.Is(x, type date), "#date(" & GetNumbers(DateVals, Value) & ")" }, //alt: Date.ToText(Value) 66 | { (x)=> Value.Is(x, type time), "#time(" & GetNumbers(TimeVals, Value) & ")" }, //alt: Time.ToText(Value) 67 | { (x)=> Value.Is(x, type datetime), 68 | let 69 | Date = DateTime.Date(Value), 70 | Time = DateTime.Time(Value) 71 | in 72 | "#datetime(" & GetNumbers(DateVals, Date) & ", " & GetNumbers(TimeVals, Time) & ")" 73 | }, //alt: DateTime.ToText(Value) 74 | { (x)=> Value.Is(x, type datetimezone), 75 | let 76 | DateTime = DateTimeZone.RemoveZone(Value), 77 | Date = DateTime.Date(DateTime), 78 | Time = DateTime.Time(DateTime) 79 | in 80 | "#datetimezone(" & GetNumbers(DateVals, Date) & ", " & GetNumbers(TimeVals, Time) & ", " & GetNumbers(ZoneVals, Value) & ")" 81 | }, //alt: DateTimeZone.ToText(Value) 82 | { (x)=> Value.Is(x, type duration), "#duration(" & GetNumbers(DurationVals, Value) & ")" }, //alt: Duration.ToText(Value) 83 | // { (x)=> Value.Is(x, type logical), Logical.ToText(Value) }, 84 | // { (x)=> Value.Is(x, type number), Number.ToText(Value) }, 85 | { (x)=> true, Text.From(Value) } 86 | */ 87 | { (x)=> true, Expression.Constant(Value) } 88 | }, 89 | Return = List.First(List.Select(CaseValues, each _{0}(Value))){1} 90 | in Return; 91 | 92 | shared Value.TypeToText = 93 | (Value as any, optional Recurs as logical) as text => 94 | let 95 | Recurs = if (Recurs<>null) then Recurs else false, 96 | Type = Value.Type(Value), 97 | ToText = if Value.Is(Value, type type) and Recurs then 98 | "type " & Type.ToText(Value, Recurs) 99 | else 100 | Type.ToText(Type, Recurs), 101 | Return = ToText 102 | in Return; 103 | 104 | shared Value.WaitFor = 105 | //author: Curt Hagenlocher https://gist.github.com/CurtHagenlocher/68ac18caa0a17667c805 106 | (producer as function, interval as function, optional count as number) as any => 107 | let 108 | list = List.Generate( 109 | //start: first try, no result 110 | () => {0, null}, 111 | //condition: stop if we have the result (try count null'd) or we've exceeded the max tries 112 | (state) => state{0} <> null and (count = null or state{0} < count), 113 | //next: stop try tally if we have our result, otherwise check again and tally a try 114 | (state) => if state{1} <> null 115 | then {null, state{1}} 116 | else {1 + state{0}, Function.InvokeAfter(() => producer(state{0}), interval(state{0}))}, 117 | //transformer: only return the result, not try tally 118 | (state) => state{1}) 119 | in 120 | List.Last(list); 121 | ///////////////////////// 122 | // Dependencies // 123 | ///////////////////////// 124 | 125 | Type.ToText = 126 | (Type as any, optional Recurs as logical) as text => 127 | let 128 | Recurs = if (Recurs<>null) then Recurs else false, 129 | 130 | CaseValues = { 131 | { (x)=> (try x)[HasError], "error" }, 132 | { (x)=> Type.Is(x, type type), "type"}, //if Recurs then else 133 | { (x)=> Type.Is(x, type function), "function"}, 134 | { (x)=> Type.Is(x, type table), if Recurs then "table " & @Type.ToText(Type.TableRow(NonNull), Recurs) else "table"}, 135 | { (x)=> Type.Is(x, type record), if Recurs then 136 | let 137 | Record = Type.RecordFields(NonNull) 138 | in "[" & Record.TransformJoin(Record, (k,v) => 139 | (if v[Optional] then "optional " else "") & Expression.Identifier(k) & " = " & @Type.ToText(v[Type], Recurs) 140 | ) & "]" 141 | else "record"}, 142 | { (x)=> Type.Is(x, type list), if Recurs then "{" & @Type.ToText(Type.ListItem(NonNull), Recurs) & "}" else "list"}, 143 | { (x)=> Type.Is(x, type binary), "binary"}, 144 | { (x)=> Type.Is(x, type logical), "logical"}, 145 | { (x)=> Type.Is(x, type number), "number"}, 146 | { (x)=> Type.Is(x, type text), "text"}, 147 | { (x)=> Type.Is(x, type date), "date"}, 148 | { (x)=> Type.Is(x, type time), "time"}, 149 | { (x)=> Type.Is(x, type datetime), "datetime"}, 150 | { (x)=> Type.Is(x, type datetimezone), "datetimezone"}, 151 | { (x)=> Type.Is(x, type duration), "duration"}, 152 | { (x)=> Type.Is(type anynonnull, x), "anynonnull"}, 153 | { (x)=> Type.Is(type null, x), "null"}, 154 | { (x)=> Type.Is(None.Type, x), "none"}, 155 | // { (x)=> Type.Is(type any, x), "any"}, 156 | { (x)=> true, "?"} 157 | }, 158 | NonNull = Type.NonNullable(Type), 159 | Return = if Type.Is(type any, Type) then "any" 160 | else (if Type.IsNullable(Type) then "nullable " else "") 161 | & List.First(List.Select(CaseValues, each _{0}(NonNull))){1} 162 | in Return; 163 | 164 | Record.TransformJoin = 165 | (Rec as record, Lambda as function, optional Delimiter as text) as text => 166 | let 167 | Delimiter = if (Delimiter<>null) then Delimiter else ", ", 168 | 169 | Keys = Record.FieldNames(Rec), 170 | Transformed = List.Transform(Keys, each Lambda(_, Record.Field(Rec,_))), 171 | Combined = Text.Combine(Transformed, Delimiter), 172 | 173 | Return = Combined 174 | in 175 | Return; --------------------------------------------------------------------------------