diff --git a/Gemfile b/Gemfile index 2e880cdc6e..cf99c5a414 100644 --- a/Gemfile +++ b/Gemfile @@ -35,4 +35,5 @@ group :development do # Ruby 3 no longer ships with a web server gem 'puma' if RUBY_VERSION >= '3' gem 'shotgun' + gem 'rackup' end diff --git a/docs/Docker.md b/docs/Docker.md index 4670b31ed6..02b82efea8 100644 --- a/docs/Docker.md +++ b/docs/Docker.md @@ -75,7 +75,7 @@ Pretty sweet. Let's unpack this: Just replace the `bundle` command with `rake`: ```bash -docker run -t -v $PWD:/app -v /tmp/vendor:/vendor -w /app -e BUNDLE_PATH=/vendor ruby rake +docker run -t -v $PWD:/app -v /tmp/vendor:/vendor -w /app -e BUNDLE_PATH=/vendor ruby bundle exec rake ``` ### Running Rack diff --git a/lib/rouge/demos/dax b/lib/rouge/demos/dax new file mode 100644 index 0000000000..67aac9dfa7 --- /dev/null +++ b/lib/rouge/demos/dax @@ -0,0 +1,15 @@ +DEFINE MEASURE 'foo'[measure1] = + VAR variable = 4.5 + RETURN + SUMX( VALUES( 'bar'[col] ), 'bar'[col] + variable ) + +EVALUATE + ADDCOLUMNS( + VALUES( 'Date'[Year Month] ) + ,"@sumBar" + ,CALCULATE( + [measure1] + ,REMOVEFILTERS() + ,USERELATIONSHIP( 'Date'[Date], 'bar'[Order Date] ) + ) + ) diff --git a/lib/rouge/lexers/dax.rb b/lib/rouge/lexers/dax.rb new file mode 100644 index 0000000000..010c36a283 --- /dev/null +++ b/lib/rouge/lexers/dax.rb @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- # +# frozen_string_literal: true + +module Rouge + module Lexers + class DAX < RegexLexer + title "DAX" + desc "Data Analysis Expressions (DAX) https://learn.microsoft.com/en-us/dax/." + tag 'dax' + filenames '*.dax' + mimetypes + + def self.functions + # sources: + # https://learn.microsoft.com/en-us/dax/ + # https://learn.microsoft.com/en-us/dax/dax-function-reference + @functions ||= Set.new %w( + ABS ACCRINT ACCRINTM ACOS ACOSH ACOT ACOTH ADDCOLUMNS ADDMISSINGITEMS + ALL ALLCROSSFILTERED ALLEXCEPT ALLNOBLANKROW ALLSELECTED AMORDEGRC + AMORLINC AND APPROXIMATEDISTINCTCOUNT ASIN ASINH ATAN ATANH AVERAGE + AVERAGEA AVERAGEX BETA.DIST BETA.INV BITAND BITLSHIFT BITOR BITRSHIFT + BITXOR BLANK CALCULATE CALCULATETABLE CALENDAR CALENDARAUTO CEILING + CHISQ.DIST CHISQ.DIST.RT CHISQ.INV CHISQ.INV.RT CLOSINGBALANCEMONTH + CLOSINGBALANCEQUARTER CLOSINGBALANCEYEAR COALESCE COLLAPSE COLLAPSEALL + COLUMNSTATISTICS COMBIN COMBINA COMBINEVALUES CONCATENATE CONCATENATEX + CONFIDENCE.NORM CONFIDENCE.T CONTAINS CONTAINSROW CONTAINSSTRING + CONTAINSSTRINGEXACT CONVERT COS COSH COT COTH COUNT COUNTA COUNTAX + COUNTBLANK COUNTROWS COUNTX COUPDAYBS COUPDAYS COUPDAYSNC COUPNCD + COUPNUM COUPPCD CROSSFILTER CROSSJOIN CUMIPMT CUMPRINC CURRENCY + CURRENTGROUP CUSTOMDATA DATATABLE DATE DATEADD DATEDIFF DATESBETWEEN + DATESINPERIOD DATESMTD DATESQTD DATESYTD DATEVALUE DAY DB DDB DEGREES + DETAILROWS DISC DISTINCT DISTINCTCOUNT DISTINCTCOUNTNOBLANK DIVIDE + DOLLARDE DOLLARFR DURATION EARLIER EARLIEST EDATE EFFECT ENDOFMONTH + ENDOFQUARTER ENDOFYEAR EOMONTH ERROR EVALUATEANDLOG EVEN EXACT EXCEPT + EXP EXPAND EXPANDALL EXPON.DIST EXTERNALMEASURE FACT FALSE FILTER + FILTERS FIND FIRST FIRSTDATE FIRSTNONBLANK FIRSTNONBLANKVALUE FIXED + FLOOR FORMAT FV GCD GENERATE GENERATEALL GENERATESERIES GEOMEAN + GEOMEANX GROUPBY HASH HASONEFILTER HASONEVALUE HOUR IF IF.EAGER IFERROR + IGNORE INDEX INFO.ALTERNATEOFDEFINITIONS INFO.ANNOTATIONS + INFO.ATTRIBUTEHIERARCHIES INFO.ATTRIBUTEHIERARCHYSTORAGES + INFO.CALCULATIONGROUPS INFO.CALCULATIONITEMS INFO.CHANGEDPROPERTIES + INFO.COLUMNPARTITIONSTORAGES INFO.COLUMNPERMISSIONS INFO.COLUMNS + INFO.COLUMNSTORAGES INFO.CULTURES INFO.DATACOVERAGEDEFINITIONS + INFO.DATASOURCES INFO.DELTATABLEMETADATASTORAGES + INFO.DETAILROWSDEFINITIONS INFO.DICTIONARYSTORAGES + INFO.EXCLUDEDARTIFACTS INFO.EXPRESSIONS INFO.EXTENDEDPROPERTIES + INFO.FORMATSTRINGDEFINITIONS INFO.FUNCTIONS + INFO.GENERALSEGMENTMAPSEGMENTMETADATASTORAGES INFO.GROUPBYCOLUMNS + INFO.HIERARCHIES INFO.HIERARCHYSTORAGES INFO.KPIS INFO.LEVELS + INFO.LINGUISTICMETADATA INFO.MEASURES INFO.MODEL + INFO.OBJECTTRANSLATIONS INFO.PARQUETFILESTORAGES INFO.PARTITIONS + INFO.PARTITIONSTORAGES INFO.PERSPECTIVECOLUMNS + INFO.PERSPECTIVEHIERARCHIES INFO.PERSPECTIVEMEASURES INFO.PERSPECTIVES + INFO.PERSPECTIVETABLES INFO.QUERYGROUPS INFO.REFRESHPOLICIES + INFO.RELATEDCOLUMNDETAILS INFO.RELATIONSHIPINDEXSTORAGES + INFO.RELATIONSHIPS INFO.RELATIONSHIPSTORAGES INFO.ROLEMEMBERSHIPS + INFO.ROLES INFO.SEGMENTMAPSTORAGES INFO.SEGMENTSTORAGES + INFO.STORAGEFILES INFO.STORAGEFOLDERS INFO.STORAGETABLECOLUMNS + INFO.STORAGETABLECOLUMNSEGMENTS INFO.STORAGETABLES + INFO.TABLEPERMISSIONS INFO.TABLES INFO.TABLESTORAGES + INFO.VARIATIONS INT INTERSECT INTRATE IPMT ISAFTER ISATLEVEL ISBLANK + ISCROSSFILTERED ISEMPTY ISERROR ISEVEN ISFILTERED ISINSCOPE ISLOGICAL + ISNONTEXT ISNUMBER ISO.CEILING ISODD ISONORAFTER ISPMT + ISSELECTEDMEASURE ISSUBTOTAL ISTEXT KEEPFILTERS KEYWORDMATCH LAST + LASTDATE LASTNONBLANK LASTNONBLANKVALUE LCM LEFT LEN LINEST LINESTX LN + LOG LOG10 LOOKUP LOOKUPVALUE LOWER MATCHBY MAX MAXA MAXX MDURATION + MEDIAN MEDIANX MID MIN MINA MINUTE MINX MOD MONTH MOVINGAVERAGE MROUND + NAMEOF NATURALINNERJOIN NATURALLEFTOUTERJOIN NETWORKDAYS NEXT NEXTDAY + NEXTMONTH NEXTQUARTER NEXTYEAR NOMINAL NONVISUAL NORM.DIST NORM.INV + NORM.S.DIST NORM.S.INV NOT NOW NPER ODD ODDFPRICE ODDFYIELD ODDLPRICE + ODDLYIELD OFFSET OPENINGBALANCEMONTH OPENINGBALANCEQUARTER + OPENINGBALANCEYEAR OR ORDERBY PARALLELPERIOD PARTITIONBY PATH + PATHCONTAINS PATHITEM PATHITEMREVERSE PATHLENGTH PDURATION + PERCENTILE.EXC PERCENTILE.INC PERCENTILEX.EXC PERCENTILEX.INC PERMUT PI + PMT POISSON.DIST POWER PPMT PREVIOUS PREVIOUSDAY PREVIOUSMONTH + PREVIOUSQUARTER PREVIOUSYEAR PRICE PRICEDISC PRICEMAT PRODUCT + PRODUCTX PV QUARTER QUOTIENT RADIANS RAND RANDBETWEEN RANGE RANK + RANK.EQ RANKX RATE RECEIVED RELATED RELATEDTABLE REMOVEFILTERS REPLACE + REPT RIGHT ROLLUP ROLLUPADDISSUBTOTAL ROLLUPGROUP ROLLUPISSUBTOTAL + ROUND ROUNDDOWN ROUNDUP ROW ROWNUMBER RRI RUNNINGSUM SAMEPERIODLASTYEAR + SAMPLE SAMPLEAXISWITHLOCALMINMAX SAMPLECARTESIANPOINTSBYCOVER SEARCH + SECOND SELECTCOLUMNS SELECTEDMEASURE SELECTEDMEASUREFORMATSTRING + SELECTEDMEASURENAME SELECTEDVALUE SIGN SIN SINH SLN SQRT SQRTPI + STARTOFMONTH STARTOFQUARTER STARTOFYEAR STDEV.P STDEV.S STDEVX.P + STDEVX.S SUBSTITUTE SUBSTITUTEWITHINDEX SUM SUMMARIZE SUMMARIZECOLUMNS + SUMX SWITCH SYD T.DIST T.DIST.2T T.DIST.RT T.INV T.INV.2T TAN TANH + TBILLEQ TBILLPRICE TBILLYIELD TIME TIMEVALUE TOCSV TODAY TOJSON TOPN + TOPNPERLEVEL TOPNSKIP TOTALMTD TOTALQTD TOTALYTD TREATAS TRIM TRUE + TRUNC UNICHAR UNICODE UNION UPPER USERCULTURE USERELATIONSHIP USERNAME + USEROBJECTID USERPRINCIPALNAME UTCNOW UTCTODAY VALUE VALUES VAR.P VAR.S + VARX.P VARX.S VDB WEEKDAY WEEKNUM WINDOW XIRR XNPV YEAR YEARFRAC YIELD + YIELDDISC YIELDMAT + ) + end + + def self.statements + # sources: + # https://learn.microsoft.com/en-us/dax/ + # https://learn.microsoft.com/en-us/dax/statements-dax + @statements ||= Set.new(%w( + AXIS ROWS COLUMN DEFINE DENSIFY EVALUATE GROUP MEASURE MPARAMETER ORDER BY + RETURN START AT TABLE TOTAL VAR WITH VISUAL SHAPE IN ASC DESC SKIP DENSE + BLANKS FIRST LAST WEEK BOTH NONE ONEWAY ONEWAY_RIGHTFILTERSLEFT ONEWAY_LEFTFILTERSRIGHT + KEEP REL + )) + end + + def self.data_types + # sources: + # https://dax.guide/datatypes/ + @data_types ||= Set.new(%w( + BINARY BOOLEAN CURRRENCY DATETIME DECIMAL INTEGER STRING VARIANT + )) + end + + state :root do + rule (/\s+/m), Text::Whitespace + rule (/\/{2}.*/), Comment::Single + rule (/\/\*([\s\S]*).*\*\//m), Comment::Multiline + rule (/\b-?\d+(e\d+)?(\.\d+)?/), Literal::Number + rule (/'(?:[^']|'')*'(?!')(?:\[[ \w]+\])?|\w+\[[ \w]+\]/), Name::Entity #Tables & Column + rule (/\[[ \w]+\]/), Name::Entity #Measure + rule (/"(?:[^"]|"")*"(?!")/), Literal::String + + rule %r/\w+/ do |m| + if self.class.functions.include? m[0].upcase + token Keyword + elsif self.class.statements.include? m[0].upcase + token Keyword + elsif self.class.data_types.include? m[0].upcase + token Keyword::Type + else + token Name + end + end + + rule (/(?:[:\-+*\/=^><]|&{1,2}|\|{2}|\b(IN|NOT))/i), Operator + rule (/[;:()\[\]\{\},.]/), Punctuation + end + + end + end +end diff --git a/spec/lexers/dax_spec.rb b/spec/lexers/dax_spec.rb new file mode 100644 index 0000000000..a881f0f9f8 --- /dev/null +++ b/spec/lexers/dax_spec.rb @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- # +# frozen_string_literal: true + +describe Rouge::Lexers::DAX do + let(:subject) { Rouge::Lexers::DAX.new } + + describe 'guessing' do + include Support::Guessing + + it 'guesses by filename' do + assert_guess :filename => 'foo.dax' + end + + end + +end diff --git a/spec/visual/samples/dax b/spec/visual/samples/dax new file mode 100644 index 0000000000..6a4a2bf1bb --- /dev/null +++ b/spec/visual/samples/dax @@ -0,0 +1,81 @@ +// Measure +Measure1 = +VAR currentState = SELECTEDVALUE( States [State]) +VAR currentDate = SELECTEDVALUE( 'Date'[Date] ) + 1 +VAR dataInScope = + CALCULATETABLE( + DISTINCT( data ) + ,data[DateTime] < currentDate + ,REMOVEFILTERS( 'Date'[Date] ) + ,REMOVEFILTERS( 'States'[State] ) + ) +RETURN +COUNTROWS( + CALCULATETABLE( + FILTER( + INDEX( 1, dataInScope, ORDERBY( data[DateTime], DESC), , PARTITIONBY( data[TestID] )) + ,[State] = currentState + ) + ,REMOVEFILTERS( 'Calendar'[Date] ) + ) +) + +// Query +EVALUATE +var OuterDateFilter = FILTER( 'Date', 'Date'[Fiscal Month] in {"FY20-May", "FY20-Jul", "FY21-Jul"} || 'Date'[Fiscal Year] in {"FY18", "FY19"} || 'Date'[Fiscal Week] in {"FY17-W01", "FY17-W02", "FY17-W03"} ) +var Dates = + ADDCOLUMNS( + SUMMARIZE( + OuterDateFilter + ,'Date'[Fiscal Year] + ,'Date'[Fiscal Quarter] + ,'Date'[Fiscal Month] + ,'Date'[Fiscal Week] + ,'Date'[Date] + ) + ,"@Islands" // Calcualtes the difference between the date and the row number of the table, if there is a date gap then we start a new island + ,DATEDIFF( + RANKX ( OuterDateFilter, 'Date'[Date], , 1 ) + ,'Date'[Date] + , DAY + ) + ) +var Islands = + SUMMARIZE( + Dates + , [@Islands] + , "IslandString" + , var MinDate = MIN ( 'Date'[Date] ) + var MaxDate = MAX ( 'Date'[Date] ) + var MinFiscalYear = CALCULATE( MIN( 'Date'[Date] ), ALLEXCEPT( 'Date', 'Date'[Fiscal Year] )) + var MaxFiscalYear = CALCULATE( MAX( 'Date'[Date] ), ALLEXCEPT( 'Date', 'Date'[Fiscal Year] )) + var MinFiscalQuarter = CALCULATE( MIN( 'Date'[Date] ), ALLEXCEPT( 'Date', 'Date'[Fiscal Quarter] )) + var MaxFiscalQuarter = CALCULATE( MAX( 'Date'[Date] ), ALLEXCEPT( 'Date', 'Date'[Fiscal Quarter] )) + var MinFiscalMonth = CALCULATE( MIN( 'Date'[Date] ), ALLEXCEPT( 'Date', 'Date'[Fiscal Month] )) + var MaxFiscalMonth = CALCULATE( MAX( 'Date'[Date] ), ALLEXCEPT( 'Date', 'Date'[Fiscal Month] )) + var MinFiscalWeek = CALCULATE( MIN( 'Date'[Date] ), ALLEXCEPT( 'Date', 'Date'[Fiscal Week] )) + var MaxFiscalWeek = CALCULATE( MAX( 'Date'[Date] ), ALLEXCEPT( 'Date', 'Date'[Fiscal Week] )) + var GranularityCheck = + SWITCH( + true + , MinDate = MinFiscalYear && MaxDate = MaxFiscalYear + , CALCULATE( MAX( 'Date'[Fiscal Year] ), 'Date'[Date] = MinDate ) & "--" & CALCULATE( MAX( 'Date'[Fiscal Year] ), 'Date'[Date] = MaxDate ) + , MinDate = MinFiscalQuarter && MaxDate = MaxFiscalQuarter + , CALCULATE( MAX( 'Date'[Fiscal Quarter] ), 'Date'[Date] = MinDate ) & "--" & CALCULATE( MAX( 'Date'[Fiscal Quarter] ), 'Date'[Date] = MaxDate ) + , MinDate = MinFiscalMonth && MaxDate = MaxFiscalMonth + , CALCULATE( MAX( 'Date'[Fiscal Month] ), 'Date'[Date] = MinDate ) & "--" & CALCULATE( MAX( 'Date'[Fiscal Month] ), 'Date'[Date] = MaxDate ) + , MinDate = MinFiscalWeek && MaxDate = MaxFiscalWeek + , CALCULATE( MAX( 'Date'[Fiscal Week] ), 'Date'[Date] = MinDate ) & "--" & CALCULATE( MAX( 'Date'[Fiscal Week] ), 'Date'[Date] = MaxDate ) + ,MinDate & "--" & MaxDate + ) + return + GranularityCheck + ) +var ReturnString = + CONCATENATEX ( + Islands + , [IslandString] + , UNICHAR ( 10 ) + ) +return + {ReturnString}