Excel VBA или формула: объединение перекрывающихся дат и поиск неперекрывающихся для нескольких уникальных идентификаторов

У меня есть очень большая электронная таблица с заголовками (более 180 тысяч строк) с уникальными идентификаторами в A, датой начала в B и датой окончания в C. На каждый идентификатор приходится несколько строк, а даты начала и окончания беспорядочно перекрываются.

Мне нужно найти пробелы в диапазонах дат для каждого идентификатора. Я написал несколько разных формул и макросов, попробовал и подправил другие скрипты VBA, которые нашел здесь и в других местах. Я попытался выполнить запрос мощности и цепляться за соломинку, но если Excel не рухнет, я не получу пригодный для использования результат.

Вот пример данных, которые у меня есть:

ID start end
100 1/1/2015 3/1/2015
100 3/1/2015 1/1/2300
100 1/1/2018 1/1/2019
096 7/1/2020 1/1/2021
182 9/17/2017 1/1/2018
182 1/1/2018 1/1/2019
607 1/1/2015 9/1/2015
607 9/1/2015 1/1/2017
607 1/1/2018 1/1/2020
607 1/1/2021 1/1/2300

Я хотел бы запустить скрипт, который каким-то образом объединяет или объединяет их, чтобы удалить лишние строки для идентификаторов, у которых нет пробелов в диапазоне дат, но оставит дополнительную строку для идентификаторов, которые имеют:

ID start end
100 1/1/2015 1/1/2300
096 7/1/2020 1/1/2021
182 9/17/2017 1/1/2019
607 1/1/2015 1/1/2017
607 1/1/2018 1/1/2020
607 1/1/2021 1/1/2300

Мне не нужно это комбинировать; хотя для презентаций было бы неплохо. Кроме того, я бы согласился на что-то, что может сказать мне, какие идентификаторы имеют пробел в диапазоне, даже если он не объединяет даты и не удаляет лишние строки.

Новичок в VBA и формулах, хотя это первый случай, когда мне не удалось получить желаемый результат. Будем очень признательны за любую помощь или указание на лучший способ приблизиться к этому.

Я нашел скрипт с другого сайта, который почти справился со своей задачей, хотя, поскольку диапазоны дат не могут быть отсортированы в правильном порядке, например, ID 100 в примере, он создает дополнительную строку, когда этого не должно быть. Я вставлю это ниже.

Sub Consolidate_Dates()

Dim cell As Range
Dim Nextrow As Long
Dim Startdate As Date

Nextrow = Range("A" & Rows.Count).End(xlUp).Row + 2
Startdate = Range("B2").Value

Application.ScreenUpdating = False
For Each cell In Range("A2", Range("A2").End(xlDown))
    If cell.Value <> cell.Offset(1).Value Or _
       cell.Offset(0, 2).Value < cell.Offset(1, 1).Value - 1 Then
        Range("A" & Nextrow).Resize(1, 3).Value = cell.Resize(1, 3).Value
        Range("B" & Nextrow).Value = Startdate
        Nextrow = Nextrow + 1
        Startdate = cell.Offset(1, 1).Value
    End If
Next cell
Application.ScreenUpdating = True

Конец сабвуфера


person Tanner    schedule 17.04.2021    source источник
comment
Я не понимаю, что вы пытаетесь сделать. Можете ли вы показать некоторые желаемые выходные данные и объяснить дальше?   -  person Mark S    schedule 17.04.2021
comment
Вы хотите объединить две строки, где начало одной следует сразу за концом другой, например, с 01.01.2021 по 31.01.2021 и с 02.01.2021 по 28.02.2021, что дает 01.01.2021 для 28.02.2021   -  person CDP1802    schedule 18.04.2021


Ответы (3)


Вот решение Power Query:

Пожалуйста, прочитайте комментарии в коде и изучите окно примененных шагов, чтобы лучше понять алгоритм, но:

  • create a List of the included dates in each range for each ID
    • combine them into a single list
  • создать список ВСЕХ возможных дат от самой ранней до самой поздней даты для каждого идентификатора
  • Если все даты из диапазона ВСЕ включены в объединенный список, то пропусков нет.
  • Create two separate tables
    • one with a Group for the no gap list
    • секунда для списка с пробелами, который мы затем расширяем
  • Добавьте две таблицы.

обратите внимание, что многие шаги нельзя выполнить из пользовательского интерфейса

М-код

Вставить в расширенный редактор

убедитесь, что вы изменили имя таблицы в строке 2 на ваше фактическое имя таблицы

let
    Source = Excel.CurrentWorkbook(){[Name="Table1"]}[Content],
    #"Changed Type" = Table.TransformColumnTypes(Source,{{"ID", Int64.Type}, {"start", type date}, {"end", type date}}),

//Turn each date range into a list
    #"Added Custom" = Table.AddColumn(#"Changed Type", "dateList", each 
        List.Dates([start],
                    Duration.Days([end]-[start])+1,
                    #duration(1,0,0,0))),

  //Group the rows by ID
  /*Generate columns where 
      actual date ranges are combined into a list,
      and a list of the Full date range for that ID*/
    #"Grouped Rows" = Table.Group(#"Added Custom", {"ID"}, 
        {{"All", each _, type table [ID=nullable number, start=nullable date, end=nullable date, dateList=list]},
        {"combinedDates", each List.Distinct(List.Combine([dateList]))},
        {"startToEnd", each List.Dates(List.Min([start]),
                                Duration.Days(List.Max([end])-List.Min([start]))+1,
                                #duration(1,0,0,0))}        
        }),

  //if the full list and the combined list Match, then there are no gaps and return True else False        
    #"Added Custom1" = Table.AddColumn(#"Grouped Rows", 
          "Custom", each List.IsEmpty(List.Difference([startToEnd],[combinedDates]))),
    #"Added Custom2" = Table.AddColumn(#"Added Custom1", 
          "start", each if [Custom] = false then null
                else List.Min([combinedDates])),
    #"Added Custom3" = Table.AddColumn(#"Added Custom2", 
          "end", each if [Custom] = false then null 
                else List.Max([combinedDates])),

  //create the table of Trues which we will NOT expand
  trueTbl = Table.SelectRows(#"Added Custom3", each [Custom] = true),
    trueRemoveColumns = Table.RemoveColumns(trueTbl,
            {"All", "combinedDates", "startToEnd","Custom"}),
    trueTyped = Table.TransformColumnTypes(trueRemoveColumns,
            {{"start", type date}, {"end", type date}}),

   //create the table of False which we WILL expand 
  falseTbl = Table.SelectRows(#"Added Custom3", each [Custom] = false),
    expandFalse = Table.ExpandTableColumn(falseTbl, "All", 
            {"start", "end"}, {"start.1", "end.1"}),
    falseRemoveColumns = Table.RemoveColumns(expandFalse,
            {"combinedDates", "startToEnd", "Custom", "start", "end"}),
    falseRenameColumns = Table.RenameColumns(falseRemoveColumns,
            {{"start.1", "start"}, {"end.1", "end"}}),

//Combine the tables
    comb = Table.Combine({trueTyped, falseRenameColumns})
in 
   comb

введите здесь описание изображения

person Ron Rosenfeld    schedule 17.04.2021
comment
Не следует ли объединить первые два диапазона 607 в период с 01.01.2015 по 01.01.2017? - person CDP1802; 17.04.2021
comment
@ CDP1802 В идеале да, хотя в его вопросе ОП указал, что он этого не требует. Я могу работать над этим дальше, если у меня есть время. Также приходится иметь дело с возможной проблемой перекрывающихся диапазонов, что усложняет ситуацию. Этот алгоритм будет обрабатывать любые перекрытия или записи не по порядку, но имеет проблему, о которой вы упомянули. - person Ron Rosenfeld; 17.04.2021
comment
Просто любопытно, почему это сработало для первых двух 100 предметов. - person CDP1802; 17.04.2021
comment
@ CDP1802 это также работает для 182 элементов. Алгоритм будет группировать, если Все элементы с одинаковым идентификатором создают диапазон дат без пропусков. Если это не так, каждая строка будет показана отдельно. - person Ron Rosenfeld; 17.04.2021
comment
Он долго загружается. Его около 3000 строк с тех пор, как я его загрузил. Хотя в превью ошибок не было. Обновлю, как только смогу. - person Tanner; 18.04.2021
comment
@Tanner Не уверен, насколько это поможет, и Excel может не отвечать при загрузке, но попробуйте File => Options and Settings => Query Options => Global =>Data Load => Enable Fast Data Load - person Ron Rosenfeld; 18.04.2021
comment
Спасибо, я оставил это сидеть и завершить. Я не мог много взаимодействовать с Excel, пока он работал, но, просматривая вывод по списку источников, он работал прекрасно. - person Tanner; 19.04.2021
comment
@Таннер. Рад помочь. Я также собираюсь попробовать другой метод. Может так будет быстрее (а может и нет). Сколько времени потребовалось для работы с вашим набором данных? - person Ron Rosenfeld; 19.04.2021

Попробуй это. Прежде чем начать, убедитесь, что диапазон данных отсортирован по идентификатору и дате начала.

Option Explicit

Public Enum ColId
    ColId_Id = 1
    ColId_Start_Date
    ColId_End_Date
End Enum

Public Sub Test()

    Dim row As Integer
    
    ' Skip the header row & the first data row. 
    ' Start on the second data row.
    row = 3
    
    With Worksheets("Sheet1")
        
        ' Loop until you run out of data
        Do While .Cells(row, ColId_Id) <> ""
            
            ' Compare the current row to the previous row.
            ' We're looking for the same id value and a start date that is 
            ' within or adjoins the previous row's date range.
            If .Cells(row, ColId_Id).Value = .Cells(row - 1, ColId_Id).Value _
            And .Cells(row, ColId_Start_Date).Value >= .Cells(row - 1, ColId_Start_Date).Value _
            And .Cells(row, ColId_Start_Date).Value <= .Cells(row - 1, ColId_End_Date).Value _
            And .Cells(row, ColId_End_Date).Value > .Cells(row - 1, ColId_End_Date).Value _
            Then

                ' Update the previous row and delete the current row.
                .Cells(row - 1, ColId_End_Date).Value = .Cells(row, ColId_End_Date).Value
                .Rows(row).Delete

            Else

                ' Next row.
                row = row + 1

            End If
        
        Loop
    
    End With

End Sub
person Nicholas Hunter    schedule 17.04.2021
comment
Проверяя это, я получаю ошибку времени выполнения 6 - переполнение под: Else 'Следующая строка. row = row + 1 Если я выберу конец, это покажет, что часть документа завершена, но не весь. - person Tanner; 19.04.2021
comment
@Tanner Изменить Dim row As Integer на Dim row As Long - person CDP1802; 19.04.2021

При этом используется объектно-ориентированный подход. Сначала он добавляет в словарь коллекцию объектов ID, по одному объекту для каждого уникального ID. К каждому объекту идентификатора он добавляет коллекцию диапазонов дат, которые имеет этот идентификатор. По мере добавления каждого промежутка начальные данные сравниваются с предыдущей конечной датой, чтобы решить, есть ли пробел или нет. Данные должны быть отсортированы по идентификатору, дате начала

Поместите входные данные на лист1, вывод идет на лист2. На нем показаны пробелы в столбцах D и E. Также показан сценарий для создания тестовых данных.

Option Explicit

Sub Consolidate_Dates()
    
    Const SHT_DATA = "Sheet1"
    Const SHT_OUTPUT = "Sheet2"

    Dim wb As Workbook, ws As Worksheet
    Dim iLastRow As Long, i As Long, n As Integer
    Dim dict As Object, id As String, objID As clsID
    Dim t0 As Single, ar As Variant, obj As Variant
    t0 = Timer
   
    Set dict = CreateObject("Scripting.Dictionary")
  
    ' scan data on sheet 1
    Set wb = ThisWorkbook
    Set ws = wb.Sheets(SHT_DATA)
    iLastRow = ws.Cells(rows.count, "A").End(xlUp).row
    ar = ws.Range("A2:C" & iLastRow).Value2 ' put data in array
    For i = 1 To UBound(ar)
        id = Trim(ar(i, 1))
        If Not dict.exists(id) Then
            Set objID = New clsID
            objID.id = id
            dict.Add id, objID
        End If
        dict(id).AddSpan CDate(ar(i, 2)), CDate(ar(i, 3))
    Next

    ' results sheet
    With wb.Sheets(SHT_OUTPUT)
        .Cells.Clear
        .Range("A1:E1") = Array("ID", "Start", "End", "Gap Start", "Gap End")
        .Columns("B:E").NumberFormat = "mm/dd/yyyy"
    End With
    ReDim ar(1 To iLastRow, 1 To 5) ' reuse part of array for output
    i = 1
    For Each obj In dict.items
        Set objID = obj
        ' output spans and gaps
        For n = 1 To obj.spansOut.count
            ar(i, 1) = objID.id
            ar(i, 2) = objID.spansOut(n).StartDate
            ar(i, 3) = objID.spansOut(n).EndDate
            ' show gaps
            If n > 1 Then
                ar(i - 1, 4) = objID.spansOut(n - 1).EndDate
                ar(i - 1, 5) = objID.spansOut(n).StartDate
            End If
            i = i + 1
        Next
    Next
    
    ' finish
    Set dict = Nothing
    With wb.Sheets(SHT_OUTPUT)
        .Range("A2:E" & i).Value2 = ar
        .Columns("A:E").AutoFit
        .Activate
        .Range("A1").Select
    End With
    Erase ar
   
    MsgBox Format(i - 1, "#,###") & " rows output to " & SHT_OUTPUT, vbInformation, Int(Timer - t0) & " seconds"
End Sub

Модуль класса с именем clsID

Option Explicit

Public id As String ' unique id
Public hasGaps As Boolean
Public spans As New Collection
Public spansOut As New Collection

Sub AddSpan(dtStart As Date, dtEnd As Date)
    
    Dim spNew As New clsSpan, spLast As clsSpan
    spNew.StartDate = dtStart
    spNew.EndDate = dtEnd
    spans.Add spNew, CStr(spans.count + 1)

    If spansOut.count = 0 Then
        spansOut.Add spNew, "1"
        hasGaps = False
    Else
        Set spLast = spansOut(spansOut.count)
        If spNew.StartDate < spLast.StartDate Then
            MsgBox "Start dates not sorted correctly for " & id, vbCritical
        ElseIf spNew.StartDate > spLast.EndDate Then
            ' add new span
            spansOut.Add spNew, CStr(spansOut.count + 1)
            hasGaps = True
        ElseIf spNew.EndDate > spLast.EndDate Then
            ' extend last span
            spLast.EndDate = spNew.EndDate
        Else
            ' no change
        End If
    End If
End Sub

Модуль класса с именем clsSpan

Option Explicit

Public StartDate As Date
Public EndDate As Date

Скрипт для генерации случайных тестовых данных

Sub testdata()
    Const ROW_COUNT = 200000
    Dim dt1 As Date, i As Long
    Sheet1.Cells.Clear
    For i = 2 To ROW_COUNT + 1
        Sheet1.Cells(i, 1) = 1000 + Int(9000 * Rnd)
        dt1 = CDate("1/1/2000") + Int(3650 * Rnd)
        Sheet1.Cells(i, 2) = dt1
        Sheet1.Cells(i, 3) = dt1 + Int(1000 * Rnd)
    Next

    With Sheet1.Sort
        .SortFields.Clear
        .SortFields.Add key:=Range("A1:A" & i)
        .SortFields.Add key:=Range("B1:B" & i)
        .SetRange Range("A1:C" & i)
        .Header = xlYes
        .MatchCase = False
        .Orientation = xlTopToBottom
        .SortMethod = xlPinYin
        .Apply
    End With
    Sheet1.Activate
    Sheet1.Range("A" & ROW_COUNT + 1).Select
   
    MsgBox Format(ROW_COUNT, "#,###") & " rows created and sorted"
End Sub
person CDP1802    schedule 18.04.2021