How to add a dash between running numbers and comma between non-running numbers

血红的双手。 提交于 2021-02-08 12:07:55

问题


I would like to replace a set of running and non running numbers with commas and hyphens where appropriate.

Using STUFF & XML PATH I was able to accomplish some of what I want by getting something like 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 13, 15, 19, 20, 21, 22, 24.

WITH CTE AS (  
SELECT DISTINCT t1.ORDERNo, t1.Part, t2.LineNum  
FROM [DBName].[DBA].Table1 t1    
JOIN Table2 t2 ON t2.Part = t1.Part    
WHERE t1.ORDERNo = 'AB12345') 

SELECT c1.ORDERNo, c1.Part, STUFF((SELECT ', ' + CAST(LineNum AS VARCHAR(5))  
FROM CTE c2  
WHERE c2.ORDERNo= c1.ORDERNo
FOR XML PATH('')), 1, 2, '') AS [LineNums]  
FROM CTE c1  
GROUP BY c1.ORDERNo, c1.Part

Here is some sample output:

ORDERNo Part        LineNums
ON5650  PT01-0181   5, 6, 7, 8, 12
ON5652  PT01-0181   1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 13, 15, 19, 20, 21, 22, 24
ON5654  PT01-0181   1, 4
ON5656  PT01-0181   1, 2, 4
ON5730  PT01-0181   1, 2
ON5253  PT16-3934   1, 2, 3, 4, 5
ON1723  PT02-0585   1, 2, 3, 6, 8, 9, 10

Would like to have:

OrderNo Part        LineNums
ON5650  PT01-0181   5-8, 12
ON5652  PT01-0181   1-10, 13, 15, 19-22, 24
ON5654  PT01-0181   1, 4
ON5656  PT01-0181   1-2, 4
ON5730  PT01-0181   1-2
ON5253  PT16-3934   1-5
ON1723  PT02-0585   1-3, 6, 8-10

回答1:


This is a classic gaps-and-islands problem.
(a good read on the subject is Itzik Ben-Gan's Gaps and islands from SQL Server MVP Deep Dives)

The idea is that you first need to identify the groups of consecutive numbers. Once you've done that, the rest is easy.

First, create and populate sample table (Please save us this step in your future questions):

DECLARE @T AS TABLE
(
    N int
);

INSERT INTO @T VALUES
(1), (2), (3), (4), 
(6), 
(8), 
(10), (11), 
(13), (14), (15), 
(17), 
(19), (20), (21), 
(25);

Then, use a common table expression to identify the groups.

With Grouped AS
(
    SELECT N,
           N - ROW_NUMBER() OVER(ORDER BY N) As Grp
    FROM @T
)

The result if this cte is this:

N   Grp
1   0
2   0
3   0
4   0
6   1
8   2
10  3
11  3
13  4
14  4
15  4
17  5
19  6
20  6
21  6
25  9

As you can see, while the numbers are consecutive, the grp value stays the same.
When a row has a number that isn't consecutive with the previous number, the grp value changes.

Then you select from that cte, using a case expression to either select a single number (if it's the only one in it's group) or the start and end of the group, separated by a dash:

SELECT STUFF(
(
    SELECT ', ' +
           CASE WHEN MIN(N) = MAX(N) THEN CAST(MIN(N) as varchar(11))
           ELSE CAST(MIN(N) as varchar(11)) +'-' + CAST(MAX(N) as varchar(11)) 
           END
    FROM Grouped   
    GROUP BY grp
    FOR XML PATH('')
), 1, 2, '')  As GapsAndIslands

The result:

GapsAndIslands
1-4, 6, 8, 10-11, 13-15, 17, 19-21, 25



回答2:


For fun I put together another way using Window Aggregates (e.g. SUM() OVER ...). I also use some newer T-SQL functionality such as CONCAT (2012+) and STRING_AGG (2017+). This using Zohar's sample data.

DECLARE @T AS TABLE(N INT PRIMARY KEY CLUSTERED);    
INSERT INTO @T VALUES (1),(2),(3),(4),(6),(8),(10),(11),(13),(14),(15),(17),(19),(20),(21),(25);

WITH 
a AS (
  SELECT t.N,isNewGroup = SIGN(t.N-LAG(t.N,1,t.N-1) OVER (ORDER BY t.N)-1)
  FROM @t AS t),
b AS (
  SELECT a.N, GroupNbr = SUM(a.isNewGroup) OVER (ORDER BY a.N)
  FROM a),
c AS (
  SELECT b.GroupNbr, 
         txt = CONCAT(MIN(b.N), REPLICATE(CONCAT('-',MAX(b.N)), SIGN(MAX(b.N)-MIN(b.N))))
  FROM b
  GROUP BY b.GroupNbr)
SELECT STRING_AGG(c.txt,', ')  WITHIN GROUP (ORDER BY c.GroupNbr) AS Islands
FROM c;

Returns:

Islands
1-4, 6 , 8, 10-11, 13-15, 17, 19-21, 25



回答3:


And here an approach using a recursive CTE.

DECLARE @T AS TABLE(N INT PRIMARY KEY CLUSTERED);    
INSERT INTO @T VALUES (1),(2),(3),(4),(6),(8),(10),(11),(13),(14),(15),(17),(19),(20),(21),(25);


WITH Numbered AS
(
    SELECT N, ROW_NUMBER() OVER(ORDER BY N) AS RowIndex FROM @T 
)
,recCTE AS
(
    SELECT N
          ,RowIndex
          ,CAST(N AS VARCHAR(MAX)) AS OutputString
          ,(SELECT MAX(n2.RowIndex) FROM Numbered n2) AS MaxRowIndex
    FROM Numbered WHERE RowIndex=1
    UNION ALL
    SELECT n.N
          ,n.RowIndex
          ,CASE WHEN A.TheEnd  =1                  THEN CONCAT(r.OutputString,CASE WHEN IsIsland=1 THEN '-' ELSE ',' END, n.N)
                WHEN A.IsIsland=1 AND A.IsWithin=0 THEN CONCAT(r.OutputString,'-')
                WHEN A.IsIsland=1 AND A.IsWithin=1 THEN r.OutputString
                WHEN A.IsIsland=0 AND A.IsWithin=1 THEN CONCAT(r.OutputString,r.N,',',n.N)
                ELSE                                    CONCAT(r.OutputString,',',n.N)
           END
          ,r.MaxRowIndex
    FROM Numbered n
    INNER JOIN recCTE r ON n.RowIndex=r.RowIndex+1
    CROSS APPLY(SELECT CASE WHEN n.N-r.N=1 THEN 1 ELSE 0 END AS IsIsland
                      ,CASE WHEN RIGHT(r.OutputString,1)='-' THEN 1 ELSE 0 END AS IsWithin
                      ,CASE WHEN n.RowIndex=r.MaxRowIndex THEN 1 ELSE 0 END AS TheEnd) A

)
SELECT TOP 1 OutputString FROM recCTE ORDER BY RowIndex DESC;

The idea in short:

  • First we create a numbered set.
  • The recursive CTE will use the row's index to pick the next row, thus iterating through the set row-by-row
  • The APPLY determines three BIT values:
    • Is the distance to the previous value 1, then we are on the island, otherwise not
    • Is the last character of the growing output string a hyphen, then we are waiting for the end of an island, otherwise not.
    • ...and if we've reached the end
  • The CASE deals with this four-field-matrix:
    • First we deal with the end to avoid a trailing hyphen at the end
    • Reaching an island we add a hyphen
    • Staying on the island we just continue
    • Reaching the end of an island we add the last number, a comma and start a new island
    • any other case will just add a comma and start a new island.

Hint: You can read island as group or section, while the commas mark the gaps.




回答4:


Combining what I already had and using Zohar Peled's code I was finally able to figure out a solution:

WITH cteLineNums AS (
SELECT TOP 100 PERCENT t1.OrderNo, t1.Part, t2.LineNum
, (t2.line_number - ROW_NUMBER() OVER(PARTITION BY t1.OrderNo, t1.Part ORDER BY t1.OrderNo, t1.Part, t2.LineNum)) AS RowSeq
FROM [DBName].[DBA].Table1 t1    
JOIN Table2 t2 ON t2.Part = t1.Part    
WHERE t1.OrderNo = 'AB12345')
GROUP BY t1.OrderNo, t1.Part, t2.LineNum
ORDER BY t1.OrderNo, t1.Part, t2.LineNum)

SELECT OrderNo, Part
,  STUFF((SELECT ', ' +
       CASE WHEN MIN(line_number) = MAX(line_number) THEN CAST(MIN(line_number) AS VARCHAR(3))
             WHEN MIN(line_number) = (MAX(line_number)-1) THEN CAST(MIN(line_number) AS VARCHAR(3)) + ', ' + CAST(MAX(line_number) AS VARCHAR(3)) 
       ELSE CAST(MIN(line_number) AS VARCHAR(3)) + '-' + CAST(MAX(line_number) AS VARCHAR(3)) 
       END
    FROM cteLineNums c1
        WHERE c1.OrderNo = c2.OrderNo
        AND c1.Part = c2.Part
    GROUP BY OrderNo, Part
        ORDER BY OrderNo, Part
    FOR XML PATH('')), 1, 2, '') AS [LineNums]
FROM cteLineNums c2
GROUP BY OrderNo, Part

I used the ROW_NUMBER() OVER PARTITION BY since I returned multiple records with different Order Numbers and Part Numbers. All this lead to me still having to do the self join in the second part in order to get the correct LineNums to show for each record. The second WHEN in the CASE statement is due to the code defaulting to having something like 2, 5, 8-9, 14 displayed when it should be 2, 5, 8, 9, 14.



来源:https://stackoverflow.com/questions/56527768/how-to-add-a-dash-between-running-numbers-and-comma-between-non-running-numbers

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!