【问题标题】:Highlight series of dates that met conditions突出显示符合条件的日期系列
【发布时间】:2020-07-07 05:53:17
【问题描述】:

我在 Excel 表中有一个数据,其中包含客户 ID、结果日期和一些实验室测试的结果。每个客户的日期按升序排列。我想要一个 VBA 代码检查每个客户的日期并测试 每个日期之间的差异是否不超过 2 个月并找到最长的连续一组日期 和 Highlight 它用颜色,例如黄色。这组日期不一定是最旧的或最新的,但应该是最长持续时间不中断2个月以上的日期。

此外,如果计算结果列旁边的那个长集的持续时间会很好,这样我们就可以对数据进行相应的排序。

这是我的文件的link。 以下是要求的屏幕截图。 image for the excel sheet

从链接文件中提取的示例数据

        +----+----------+------------------------+---------+
        | #  |    A     |         B              |    C    |
        +----+----------+------------------------+---------+
        | 1  | ClientId | Results Date & Time    | Results |
        +----+----------+------------------------+---------+
        |... |    ...   |         ...            |    ...  |
        +----+----------+------------------------+---------+
        |105 |    1     | 12/06/2018 12:42:00 PM | 1.9     |
        +----+----------+------------------------+---------+
        |106 |    1     | 6/25/2018  1:55:00 PM  | 1.8     |
        +----+----------+------------------------+---------+
        |107 |    2     | 3/29/2016  9:11:00 AM  | 1       |
        +----+----------+------------------------+---------+
        |108 |    2     | 6/8/2016  12:50:00 PM  | 2       |
        +----+----------+------------------------+---------+
        |...

【问题讨论】:

  • 嗨 Ahmed,请考虑上传示例数据的图片,因为很多人(包括我自己)不愿意点击下载链接 stackoverflow.com/help/mcve
  • SO 不是代码编写服务。也许有人会很友善地提供帮助,但在您没有看到任何初步努力的情况下,机会就更渺茫了...
  • 嗨!欢迎来到堆栈溢出。这不是代码编写服务,但如果您edit 提出您的问题,添加您迄今为止尝试过的内容,我们可以帮助您实现目标。您可能还想阅读How to Ask
  • 大家好,我附上了一张excel表格的图片。
  • @user72343 - 由于所有您显示的日期差异是

标签: excel vba conditional-formatting


【解决方案1】:

通过数据字段数组解决

“我想要一个 VBA 代码检查每个客户的日期并测试每个日期之间的差异是否不超过 2 个月,并找到最长的连续日期集并用颜色突出显示它,例如黄色”

循环遍历一个范围总是耗时,所以我通过数据字段数组演示了一种方法,而不是将2个月条件简化为天差异 因为我不想让这个例子过于复杂。

由于“每个客户的日期按升序排列”,因此很容易检查下一个客户 ID,计算日差,将它们添加到当前持续时间变量中,然后将其与记住的变量进行比较,以找到同一 id 中最长的一组日期,然后更改为下一个 id。

最后将结果写入概览数组以收集要突出显示的项目编号。这可以通过条件格式

来完成

此外,我在代码模块的声明头中集成了一个 Enum 声明,只是为了显示使用有意义的变量而不是纯数字(此处替换数组“列”数字)。

0。代码模块的声明头

强烈建议使用Option Explicit 强制声明变量的类型,从而避免明显无法解释的类型不匹配或其他问题。

如果您使用从定义的第一个元素开始的自动枚举,那么已经提到的Enum 声明还有另一个功能,例如[_Zero]:您可以轻松地重构内部顺序,而无需更改仅包含纯数字的每一行代码。

提示:所有 Enum 元素都使用 IntelliSense 显示,[] 括号中的元素和以下划线字符 _ 开头的元素名称除外。

小改动 08/28 当前编辑没有枚举 data.Results 而不影响想要的输出,因为所有 data 成员都会自动重新编号+1 的额外增量(在[_Zero]=0 之后计算)。

Option Explicit                                         ' force declaration of variables

' assign meaningful number variables for your array columns
Enum data                                               ' automatically enumerates column numbers 1 to 5 (-> array v)
    [_Zero] = 0
      Id
      Date
      days
      Duration
End Enum
Enum Ov                                                ' automatically enumerates column numbers 1 to 6 (-> array overview)
    [_Zero] = 0
    Id
    StartDate
    EndDate
    duration
    StartItem
    enditem
End Enum

1.主程序GetLongestDuration()

编辑 1: 我将所有计算的日期变量的 TypeLong 更改为 Double(即 maxDAYS#、currDuration#、memDuration#)以防止类型不匹配,尤其是在计算中断天数。

编辑 2: 请参阅第 II 部分中的更改以避免 空日期 计算(例如,在评论中提到的最后一行)( ) 和最终错误 13 在第 III b) 节中写回持续时间。

编辑 3:请参阅第 II 部分中对非数字项的额外检查()

编辑 4: 原始方法没有假设数据行数超过 65,536 的数量,这是使用 ► 的绝对限制 Index 函数(尝试在此处隔离数组列)。

希望最终的编辑避免了 Error 13 Type mismatch 使用带有所有相关持续时间数据的额外数组 d(在定义的 2 个月范围内累积的天数差异),并纠正了其他一些小问题。在第二节 和第三节

中进行了更正
Sub GetLongestDuration()
' Purpose:    Highlight longest set of dates <= 64 days
' Condition:  Client IDs and Dates are sorted in ascending order.
' Edit 8/16:  Enumerated type changes of maxDAYS#, currDuration#, memDuration# changed to DOUBLE (#)
' Edit 8/17:  Edit in section II <-- Edit #13         -->
' Edit 8/22:  Edit in section II <-- Edit #14 and #15 -->
' Edit 8/28:  Edit in section II <-- Edit #101 -->, section III <-- Edit #102 to #122 -->
  Const maxDAYS# = 64#                                ' << <--#1 Double--> change maximal difference to next date
  Const DATASHEET$ = "LABs and Diagnostics"           ' << replace with your data sheet name
  Const OVSHEET$ = "Overview"                         ' << replace with your Overview sheet name
  Const OVTITLES$ = "ID,Start Date,End Date,Duration,Start Item, End Item"
' declare variables
  Dim ws As Worksheet, ws2 As Worksheet               ' declare object variables as worksheet
  Set ws = ThisWorkbook.Worksheets(DATASHEET)         ' set data sheet object to memory

  Dim v As Variant, overview As Variant               ' variant datafield array and results array
  Dim Id            As String                         ' current state
  Dim StartItem     As Long
  Dim StartDate     As Double, EndDate      As Double '
  Dim days          As Double, currDuration As Double '   <-- #2 Double -->

  Dim memStartDate#, memEndDate#                      ' remember highest findings
  Dim memDuration#                                    '   <-- #3 Double -->
  Dim memStartItem&, memLastItem&                     ' remember highest findings
  Dim i As Long, ii As Long, n As Long, iOv As Long   ' counters

' 0. get last row number n and assign values to a 2-dim array v
  ws.Columns("D:D") = ""                              ' clear column D (duration)

  n = ws.Range("A" & ws.Rows.Count).End(xlUp).Row + 2 ' find last row number n plus 2 more rows
  v = ws.Range("A2:E" & n).Value2                     ' create 2-dim datafield array omitting headers
  ReDim overview(1 To n, 1 To 6)                      ' create a helper array with results

' =======================
' loop through data array
' =======================
' remember first ID (for later comparation with changing array item id)
  Id = v(1, data.Id) & ""
  For i = LBound(v) To UBound(v)                      ' loop through items 1 to items count UBound(v) in data array v

    ' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    ' I. check new ID in first 'column' of each array item
    ' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      If v(i, data.Id) & "" & "" <> Id Then           ' check current id against remembered id
        ' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        '... complete analytics of preceding id in overview
        ' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
         If i > 1 Then
            ii = ii + 1
            overview(ii, Ov.Id) = Id
            overview(ii, Ov.StartDate) = memStartDate
            overview(ii, Ov.EndDate) = memEndDate
            overview(ii, Ov.Duration) = memDuration
            overview(ii, Ov.StartItem) = memStartItem
            overview(ii, Ov.enditem) = memLastItem
         Else
            overview(ii, Ov.StartItem) = 1
         End If
        '... and switch to new current id
         Id = v(i, data.Id) & ""
         currDuration = 0#: memDuration = 0#             ' <-- #4 Double --> reset to zero
         memStartItem = 0&: memLastItem = 0&
      End If

    ' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    ' II. calculate days and check coherent periods
    ' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      If i < UBound(v) Then                              ' stop calculation one item before last item row
         If Len(Trim(v(i + 1, data.Date))) = 0 Then      ' avoid type mismatch if empty
            days = 0#
         ElseIf Not IsNumeric(v(i, data.Date)) Then      ' <-- #14 not numeric -->
            days = 0#
            MsgBox "Item no " & i & " " & v(i, data.Date) & " is not numeric!"
         Else
            If IsNumeric(v(i + 1, data.Date)) Then       ' <-- #15 not numeric -->
               days = v(i + 1, data.Date) - v(i, data.Date) ' get days difference to next date

               v(i, data.days) = days                    ' <-- #101 remind days difference -->

            End If
         End If
      Else                                               ' there's nothing more to add
         days = 0#                                       ' <-- #5 Double -->
      End If
    ' avoid negative day counts in last row
      If days < 0 Then days = 0#                         ' <-- #6 Double -->
    ' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    ' a) days till next date within two months (i.e. <=64)
    ' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      If days <= maxDAYS And days > 0 Then
         v(i, data.days) = days                          '    assign days to column 5
         currDuration = currDuration + days              '    add days in current set to cumulated duration
         If i > 1 Then
            If v(i - 1, data.days) = 0 Then
                StartItem = i                            '    StartItem number in current data set
                StartDate = v(i, data.Date)              '    StartDate current data set
            End If
         End If
    ' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    ' b) days till next date exceed two months
    ' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      Else
         v(i, data.days) = 0#                            ' <-- #7 Double -->   therefore no count

         ' if longer duration then remember this set within current id
         If currDuration > memDuration Then
            memDuration = currDuration
            memStartDate = StartDate
            memEndDate = v(i, data.Date)
            memStartItem = StartItem
            memLastItem = i
         End If

         ' start new set
         currDuration = 0#                                     ' <-- #8 Double --> reset to zero
      End If
  Next i
  v(UBound(v), data.days) = 0#                                 ' <-- #9 Double --> days in last row
' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
' III. calculate durations for longest coherent periods and write it to new column D
' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
' a) loop through all overview items

Dim d: ReDim d(1 To UBound(v), 1 To 1)                            ' <-- #102 create separate duration array -->

If overview(1, Ov.enditem) > 0 Then overview(1, Ov.StartItem) = 1 ' <-- #103 set startitem of 1st to 1 if relevant date range -->
For iOv = 1 To ii
      currDuration = 0#                                           ' <--  #10 Double --> reset to 0 (Double!)
      '''      If overview(iOv, Ov.StartItem) = 0 Then Exit For   ' <-- #104 DELETE last Edit #0/Aug 14th 18) -->
      memStartItem = overview(iOv, Ov.StartItem)                  ' <-- #105 remember start item              -->
      If memStartItem = 0 Then                                    ' <-- #106/107/108 clear not relevant dates -->
          overview(iOv, Ov.StartDate) = ""                        '
          overview(iOv, Ov.EndDate) = ""                          '
      Else                                                        ' <-- #109 relevant dates                   -->
        ''' v(overview(iOv, Ov.StartItem), data.Duration) = 0#    ' <-- #110 DELETE last Edit #11 Double      -->
          d(memStartItem, 1) = currDuration                       ' <-- #111 write current duration to array  -->

          For i = memStartItem To overview(iOv, Ov.enditem) - 1   ' <-- #112 first item no to last item no    -->
              currDuration = currDuration + CDbl(v(i, data.days)) ' <--  #12 CDbl --> add days to cumulated sum currDuration
              v(i + 1, data.Duration) = currDuration              ' <-- #113 (unchanged) --> assign duration to source array v in column 4
              d(i + 1, 1) = currDuration                          ' <-- #114
          Next i                                                  ' <-- #115 (unchanged)                      -->
      End If                                                      ' <-- #116 closing IF to #106               -->

  Next iOv                                                        ' <-- #117 (unchanged)                      -->

  ' b) write cumulated duration into column D

  '  **********************************************************
  '  avoid ERROR 13 type mismatch, ii 6379 **ISSUE 2018/08/22**
  '  **********************************************************
  '  Caveat: Index function (trying to isolate single array column) has limitation to 65,536 rows only!
   '''  ws.Range("D2").Resize(UBound(v), 1) = Application.Index(v, 0, data.Duration) <-- #118 uncomment/DELETE -->

   ws.Range("D2").Resize(UBound(d), 1) = d                        ' <-- #119 write relevant durations to column D -->

    ws.Range("D1") = "Duration"                                   ' <-- #120 add title                           -->
    ws.Range("D:D").NumberFormat = "# ??/24"                      ' <-- #121 fraction format shows days + hours  -->

' IV. set Conditional Format in order to highlight found items (condition: existing value in column D)
'    (calls helper function SetConditionalFormat with arguments range and condition)
  SetConditionalFormat ws.Range("A:D"), "=LEN(TRIM($D1 & """"))>0" ' <--#122 (unchanged)                         -->

' V.  optional display of results in sheet 'Overview', see below

End Sub

结果的可选显示

如果您想在单独的“概览”表中显示找到的项目数据,您可以将其添加到上面的代码中:

' V. optional display of separate Overview sheet with results
' a) add Overview sheet if it doesn't exist yet
  If Not SheetExists(OVSHEET) Then
     With ThisWorkbook.Worksheets.Add
          .Name = OVSHEET                                       ' baptize it e.g. "Overview"
          .Columns("B:C").NumberFormat = "dd/mm/yyyy;@"         ' << change columns B:C do wanted local format
     End With
  End If
  Set ws2 = ThisWorkbook.Worksheets(OVSHEET)                     ' set overview sheet object to memory
' b) write titles and results to Overview sheet
  ws2.Range("A:F") = ""                                          ' clear columns
  ws2.Range("A1:F1") = Split(OVTITLES, ",")                      ' write titles to overview!A1:F1

  If ii < 1 Then
    ws2.Range("A2") = "No duration sets identified!"
  Else
    ws2.Range("A2").Resize(ii, UBound(overview, 2)) = overview     ' write array overview back to Overview sheet
  End If

2。帮助程序SetConditionalFormat()

此过程在主过程的 [IV.] 部分中调用,并突出显示 D 列中包含数据的所有单元格的已找到日期集。一种可能的情况是询问修剪后的字符串长度是否为零。 国际使用:必须考虑到条件格式(CF)需要►本地公式 - 因此集成了辅助函数getLocalFormula()。 *

 Sub SetConditionalFormat( _
                   ByRef rng As Range, _
                   ByVal sFormula As String, _
                   Optional ByVal myColor As Long = 65535, _
                   Optional bDelFormerFormats As Boolean = True)
 ' Author:  T.M.
 ' Purpose: set conditional format to given range using sFormula
 ' Note:    former formats are deleted by default unless last argument isn't set to False
 ' Hint:    Formula1 always needs the LOCAL formula, so the string argument sFormula
 '          has to be translated via helper function getLocalFormula() using a work around
     With rng
        ' a) delete existing conditional formats in A:D
             If bDelFormerFormats Then .FormatConditions.Delete
        ' b) add new condition with needed LOCAL formula
             .FormatConditions.Add _
                    Type:=xlExpression, _
                    Formula1:=getLocalFormula(sFormula)  ' << get local formula via helper function
             .FormatConditions(.FormatConditions.Count).SetFirstPriority
        With .FormatConditions(1).Interior
             .PatternColorIndex = xlAutomatic
             .color = myColor                         ' yellow by default parameter
             .TintAndShade = 0
        End With
     .FormatConditions(1).StopIfTrue = False
     End With
 End Sub

3 a) 辅助函数getLocalFormula()

此函数由上述辅助过程调用,因为条件格式总是需要本地公式,因此考虑国际化

 Function getLocalFormula(ByVal sFormula As String) As String
 ' Author:  T.M.
 ' Purpose: work around to translate English formula to local formula
 ' Caveat:  assumes there is no value in last cell (e.g. $XFD$1048576 in more recent versions)
     With ActiveSheet.Range("A1").Offset(ActiveSheet.Rows.Count - 1, ActiveSheet.Columns.Count - 1)
       ' assign formula to temporary cell in order to get local formula string
         .Formula = sFormula
       ' get local formula
         getLocalFormula = .FormulaLocal
         .Value = ""                              ' delete temporary formula
     End With
 End Function

3 b) 辅助函数SheetExists()

由主过程的可选部分 [V.] 调用:

 Function SheetExists(SheetName As String, Optional wb As Workbook) As Boolean
 ' Author:  Tim Williams
 ' Purpose: check if worksheet exists (returns True or False)
 ' cf Site: https://stackoverflow.com/questions/6688131/test-or-check-if-sheet-exists
 Dim ws As Worksheet
 If wb Is Nothing Then Set wb = ThisWorkbook
 On Error Resume Next
 Set ws = wb.Worksheets(SheetName)
 On Error GoTo 0
 SheetExists = Not ws Is Nothing
 End Function

【讨论】:

  • 你好 T.M.并感谢您分享此答案。我尝试在 VBA 编辑器中复制代码并运行宏 Sheet1.GetLongestDuration,但出现错误“下标超出范围”!我错过了什么吗?
  • 除了我上面的评论:a)代码在哪一行停止?将鼠标移到任何循环变量上 - 你看到哪个值? - 请告诉我导致错误 9 的 代码行! (b) 您还可以通过在任何点插入 Stop` 语句来调试代码,并通过键入 ist 名称和 Enter 键直接在 VBA 编辑器 (VBE) 的即时窗口 (Crtl+G) 中检查可疑变量以获取最后一个值,按 F8 继续或通过单击 VBE 顶部的蓝色方形按钮或 F5 结束测试(参见 cpearson.com/excel/DebuggingVBA.aspx) - @user72343
  • 错误 9 的常见原因是工作表名称错误 - 因此您可以检查是否将现有工作表名称分配给 DATASHEET 常量。
  • 单行检查表是否存在:SheetExists = Not IsError(Application.Evaluate(ws &amp; "!A1"))。 `
  • @T.M.你好,作为一个魅力,这太棒了!我们非常感谢您的帮助。非常感谢:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-05-22
  • 1970-01-01
  • 1970-01-01
  • 2015-04-21
相关资源
最近更新 更多