1

The idea is simple, I'd like a function I could do something like =MOD_DATE_OF(A1:A4) and when any of the cells in such range is modified, the cell I assigned that formula gets the current date.

I have found some similar questions on the web and even here, but none of them quite it.

The closest I've got was this code somewhere (sorry, lost track of the source):

Private Sub Worksheet_Change(ByVal Target As Excel.Range)
    If Target.Column = 1 Then
        Target.Offset(0, 1).Value = Date
    End If
End Sub

But it is still not a function..

I am using Excel from Office 2010

thanks

flpgdt
  • 467
  • 4
  • 11
  • 21

1 Answers1

4

Here's a full-fledged solution that will allow you to monitor change-dates of different ranges. Note, this uses a function from Chip Pearson's tools for using arrays in VBA and function from a StackOverflow answer by user Thomas.

The basic idea is that a global array, in which the addresses of all monitored ranges (past or present) are stored with their most recent update dates, allows the function and the Worksheet_Change Sub to interact. The Worksheet_Change Sub updates this array by checking the changed range against all stored ranges. The function looks for the monitored range in the array and returns the stored change-date if it's found. Otherwise, it will return today's date (which will then be added to the array).

Also, to prevent timestamps being lost when the workbook is closed and the array of timestamps is deallocated, the array must be written to a sheet on the Workbook_Close event, and then rewritten to the array on the Workbook_Open event.

In a module, paste the following code.

Public funcInstances() As Variant

Public Function MOD_DATE_OF(monitor As Range)
Application.Volatile True
Dim i As Long
Dim tmpArray() As Variant

If Not IsDimensioned(funcInstances) Then
    ReDim funcInstances(1 To 1, 1 To 2) As Variant
    funcInstances(1, 1) = monitor.Address
    funcInstances(1, 2) = Date
Else
    For i = 1 To UBound(funcInstances, 1)
        If funcInstances(i, 1) = monitor.Address Then
            MOD_DATE_OF = Format(funcInstances(i, 2), "yyyy-mm-dd")
            Exit Function
        End If
    Next i
    tmpArray = ExpandArray(funcInstances, 1, 1, "")
    Erase funcInstances
    funcInstances = tmpArray
    funcInstances(UBound(funcInstances, 1), 1) = monitor.Address
    funcInstances(UBound(funcInstances, 1), 2) = Date
End If
MOD_DATE_OF = Format(funcInstances(UBound(funcInstances, 1), 2), "yyyy-mm-dd")
End Function

'ExpandArray() is the work of Chip Pearson.  Code copied from http://www.cpearson.com/excel/vbaarrays.htm
Function ExpandArray(Arr As Variant, WhichDim As Long, AdditionalElements As Long, _
        FillValue As Variant) As Variant
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' ExpandArray
' This expands a two-dimensional array in either dimension. It returns the result
' array if successful, or NULL if an error occurred. The original array is never
' changed.
' Parameters:
' --------------------
' Arr                   is the array to be expanded.
'
' WhichDim              is either 1 for additional rows or 2 for
'                       additional columns.
'
' AdditionalElements    is the number of additional rows or columns
'                       to create.
'
' FillValue             is the value to which the new array elements should be
'                       initialized.
'
' You can nest calls to Expand array to expand both the number of rows and
' columns. E.g.,
'
' C = ExpandArray(ExpandArray(Arr:=A, WhichDim:=1, AdditionalElements:=3, FillValue:="R"), _
'    WhichDim:=2, AdditionalElements:=4, FillValue:="C")
' This first adds three rows at the bottom of the array, and then adds four
' columns on the right of the array.
'
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Dim Result As Variant
Dim RowNdx As Long
Dim ColNdx As Long
Dim ResultRowNdx As Long
Dim ResultColNdx As Long
Dim NumRows As Long
Dim NumCols As Long
Dim NewUBound As Long

Const ROWS_ As Long = 1
Const COLS_ As Long = 2


''''''''''''''''''''''''''''
' Ensure Arr is an array.
''''''''''''''''''''''''''''
If IsArray(Arr) = False Then
    ExpandArray = Null
    Exit Function
End If

'''''''''''''''''''''''''''''''''
' Ensure the dimension is 1 or 2.
'''''''''''''''''''''''''''''''''
Select Case WhichDim
    Case 1, 2
    Case Else
        ExpandArray = Null
        Exit Function
End Select

''''''''''''''''''''''''''''''''''''
' Ensure AdditionalElements is > 0.
' If AdditionalElements  < 0, return NULL.
' If AdditionalElements  = 0, return Arr.
''''''''''''''''''''''''''''''''''''
If AdditionalElements < 0 Then
    ExpandArray = Null
    Exit Function
End If
If AdditionalElements = 0 Then
    ExpandArray = Arr
    Exit Function
End If

NumRows = UBound(Arr, 1) - LBound(Arr, 1) + 1
NumCols = UBound(Arr, 2) - LBound(Arr, 2) + 1

If WhichDim = ROWS_ Then
    '''''''''''''''
    ' Redim Result.
    '''''''''''''''
    ReDim Result(LBound(Arr, 1) To UBound(Arr, 1) + AdditionalElements, LBound(Arr, 2) To UBound(Arr, 2))
    ''''''''''''''''''''''''''''''
    ' Transfer Arr array to Result
    ''''''''''''''''''''''''''''''
    For RowNdx = LBound(Arr, 1) To UBound(Arr, 1)
        For ColNdx = LBound(Arr, 2) To UBound(Arr, 2)
            Result(RowNdx, ColNdx) = Arr(RowNdx, ColNdx)
        Next ColNdx
    Next RowNdx
    '''''''''''''''''''''''''''''''
    ' Fill the rest of the result
    ' array with FillValue.
    '''''''''''''''''''''''''''''''
    For RowNdx = UBound(Arr, 1) + 1 To UBound(Result, 1)
        For ColNdx = LBound(Arr, 2) To UBound(Arr, 2)
            Result(RowNdx, ColNdx) = FillValue
        Next ColNdx
    Next RowNdx
Else
    '''''''''''''''
    ' Redim Result.
    '''''''''''''''
    ReDim Result(LBound(Arr, 1) To UBound(Arr, 1), UBound(Arr, 2) + AdditionalElements)
    ''''''''''''''''''''''''''''''
    ' Transfer Arr array to Result
    ''''''''''''''''''''''''''''''
    For RowNdx = LBound(Arr, 1) To UBound(Arr, 1)
        For ColNdx = LBound(Arr, 2) To UBound(Arr, 2)
            Result(RowNdx, ColNdx) = Arr(RowNdx, ColNdx)
        Next ColNdx
    Next RowNdx
    '''''''''''''''''''''''''''''''
    ' Fill the rest of the result
    ' array with FillValue.
    '''''''''''''''''''''''''''''''
    For RowNdx = LBound(Arr, 1) To UBound(Arr, 1)
        For ColNdx = UBound(Arr, 2) + 1 To UBound(Result, 2)
            Result(RowNdx, ColNdx) = FillValue
        Next ColNdx
    Next RowNdx

End If
''''''''''''''''''''
' Return the result.
''''''''''''''''''''
ExpandArray = Result

End Function

'IsDimensioned() is the work of StackOverflow user @Thomas.  Code copied from https://stackoverflow.com/a/5480690/657668
Public Function IsDimensioned(vValue As Variant) As Boolean
    On Error Resume Next
    If Not IsArray(vValue) Then Exit Function
    Dim i As Integer
    i = UBound(vValue)
    IsDimensioned = Err.Number = 0
End Function

In the appropriate Worksheet module, paste the following code.

Private Sub Worksheet_Change(ByVal Target As Range)
Application.EnableEvents = False
Dim j As Long
If IsDimensioned(funcInstances) Then
    For j = 1 To UBound(funcInstances, 1)
        If Not Intersect(Target, Range(funcInstances(j, 1))) Is Nothing Then
            funcInstances(j, 2) = Date
        End If
    Next j
    Me.Calculate
End If
Application.EnableEvents = True
End Sub

Finally, in the ThisWorkbook module, paste the following code:

Private Sub Workbook_BeforeClose(Cancel As Boolean)
If IsDimensioned(funcInstances) Then
    Application.ScreenUpdating = False
    'Store array on a new temporary and hidden worksheet.
    Dim tmpS As Worksheet, tmpR As Range
    Set tmpS = Worksheets.Add
    tmpS.Name = "TEMP Record of Timestamps"
    tmpS.Visible = xlSheetHidden
    Set tmpR = tmpS.Range("A1:B1").Resize(UBound(funcInstances, 1), 2)
    tmpR.Value = funcInstances
    ThisWorkbook.Save
    Application.ScreenUpdating = True
End If
End Sub

Private Sub Workbook_Open()
Dim ws As Worksheet, tstamps As Range
Dim wsfound As Boolean
wsfound = False
Application.ScreenUpdating = False
For Each ws In ThisWorkbook.Worksheets
    If ws.Name = "TEMP Record of Timestamps" Then
        wsfound = True
        Exit For
    End If
Next ws
If wsfound Then
    Set tstamps = ws.UsedRange
    funcInstances = tstamps.Value
    Application.DisplayAlerts = False
    ws.Delete
    Application.DisplayAlerts = True
End If
Application.ScreenUpdating = True
End Sub

NOTE for anyone who stumbles across this page: Many of the comments are about previous, incomplete solutions, so don't be confused by them.

Excellll
  • 12,717
  • 1
    This works. If the time needs to be recorded as well, a Long will be problematic, when using Now, so making modDate a Variant will work to show time – datatoo Jun 06 '12 at 17:51
  • It works for one instance as you said. Though the idea of having it as a function was exactly so I could monitor multiple points separately. Any chance you could add that functionality? :) thanks. – flpgdt Jun 06 '12 at 18:03
  • @flpgdt Ha, of course that's what you wanted! I have an idea to test, but it may take a little while. If it's too time-consuming for me, I may post the basic idea here in a little while. – Excellll Jun 06 '12 at 18:05
  • @Excellll Thanks man. I will be tinkering with it myself when I have time, but will take a long while to get into a satisfactory solution. – flpgdt Jun 06 '12 at 18:11
  • @flpgdt I added a full solution. It took a little work, but I think it's complete. Let me know if it works for you. – Excellll Jun 06 '12 at 20:28
  • @Excellll Amazing man. You should get bounty for this, though I'm pretty sure you will get lots of votes from this answer (has quite a few google hits). Now I realize I'd never figure it out unless I really had put myself with it. Thanks a lot. – flpgdt Jun 06 '12 at 21:15
  • Ps. and yes, it works. :) – flpgdt Jun 06 '12 at 21:17
  • @Excellll I would vote this up again if I could. Nice job – datatoo Jun 07 '12 at 18:00
  • @Excellll Not sure if this is an intended behaviour or has to do with something local, but I'm ought to report. I just noticed all the instances of the function were updated to current date once I saved, closed and opened the spreadsheet. Also, after just opening, the files accuses to have been edited (the dates themselves?). Not saving and opening again will still show the current date and the document will still say it was edited. Should it be this way? – flpgdt Jun 11 '12 at 14:17
  • No that definitely wasn't intended. I'll take a look at it later today when I have some time. Sorry if that's caused any problems. – Excellll Jun 11 '12 at 14:30
  • Sure man. It is such a great work, will be nice to have it tidy! – flpgdt Jun 12 '12 at 15:23
  • @flpgdt Okay, okay, you turned the screw by un-accepting my answer! I've updated the answer, and tested it to get around the resetting problem you described. It's pretty much a full-fledged software package at this point, but that's really the only way I saw to do this. Let me know if you find any other bugs. – Excellll Jun 15 '12 at 18:34
  • @Excellll Haha, no man, don't get me wrong, you did an great job here, I'd mark your answer anyway! I just left it open so maybe that glitch it would get into someone's eyes - as I said would had been a pity let this cool function die ashore :) – flpgdt Jun 16 '12 at 03:38
  • @Excellll And yeah, it works now even after open-closes. I guess I couldn't ask any better. Great job. – flpgdt Jun 16 '12 at 03:46