Preparing for merged histories

Zig Zichterman [03-03-16 - 21:11]
Preparing for merged histories

Retain savedVariables history, don't blank it out after each Save Now.
Filename
GuildGoldDeposits.lua
diff --git a/GuildGoldDeposits.lua b/GuildGoldDeposits.lua
index 1220a36..2473940 100644
--- a/GuildGoldDeposits.lua
+++ b/GuildGoldDeposits.lua
@@ -7,15 +7,27 @@ GuildGoldDeposits.default = {
       enable_guild  = { true, true, true, true, true }
 }
 GuildGoldDeposits.max_guild_ct = 5
-GuildGoldDeposits.event_list = {} -- event_list[guild_index] = { list of event strings }
+
+                        -- event_list[guild_index] = { list of event strings }
+                        -- loaded from the current "Save Now" run.
+                        -- Eventually these become the front part of
+                        -- savedVariables.guild_history[guildName]
+GuildGoldDeposits.event_list = {}
 GuildGoldDeposits.guild_name = {} -- guild_name[guild_index] = "My Aweseome Guild"

-                                  -- retry_ct[guild_index] = how many retries after
-                                  -- distrusting "nah, no more history"
+                        -- retry_ct[guild_index] = how many retries after
+                        -- distrusting "nah, no more history"
 GuildGoldDeposits.retry_ct   = { 0, 0, 0, 0, 0 }
 GuildGoldDeposits.max_retry_ct = 3
 GuildGoldDeposits.max_day_ct = 30 -- how many days to store in SavedVariables

+local TIMESTAMPS_CLOSE_SECS = 10
+
+-- Indices into the 3-element "event" split.
+local I_TIMESTAMP = 1
+local I_AMOUNT    = 2
+local I_USER      = 3
+
 -- Init ----------------------------------------------------------------------

 function GuildGoldDeposits.OnAddOnLoaded(event, addonName)
@@ -104,7 +116,7 @@ function GuildGoldDeposits:CreateSettingsWindow()
             , self.OnPanelControlsCreated)
 end

--- Delayed initialization of options panel: don't waste time fetching
+-- Delay initialization of options panel: don't waste time fetching
 -- guild names until a human actually opens our panel.
 function GuildGoldDeposits.OnPanelControlsCreated(panel)
     self = GuildGoldDeposits
@@ -143,7 +155,7 @@ function GuildGoldDeposits.ConvertCheckboxToText(cb)
     desc.checkbox:SetHidden(true)
 end

--- Displaying Status ---------------------------------------------------------
+-- Display Status ------------------------------------------------------------

 -- Update the per-guild text label with what's going on with that guild data.
 function GuildGoldDeposits:SetStatus(guild_index, msg)
@@ -151,24 +163,24 @@ function GuildGoldDeposits:SetStatus(guild_index, msg)
     desc:SetText("  " .. msg)
 end

--- Set status to "Newest: @user 100,000g"
+-- Set status to "Newest: @user 100,000g  11 hours ago"
 function GuildGoldDeposits:SetStatusNewest(guild_index)
-    line = self:HistoryNewest(guild_index)
+    line = self:SavedHistoryNewest(guild_index)
     if not line then return end
-    event_ts, amt, user = self:split(line)
+    event = self:StringToEvent(line)
     now_ts = GetTimeStamp()
-    ago_secs = GetDiffBetweenTimeStamps(now_ts, event_ts)
+    ago_secs = GetDiffBetweenTimeStamps(now_ts, event.time)
     ago_str  = FormatTimeSeconds(ago_secs
-                    , TIME_FORMAT_STYLE_SHOW_LARGEST_UNIT_DESCRIPTIVE        -- "22 hours"
+                    , TIME_FORMAT_STYLE_SHOW_LARGEST_UNIT_DESCRIPTIVE -- "22 hours"
                     , TIME_FORMAT_PRECISION_SECONDS
                     , TIME_FORMAT_DIRECTION_DESCENDING
                     )

-    self:SetStatus(guild_index, "Newest deposit: " .. user
-                     .. " " .. amt .. "g  " .. ago_str .. " ago")
+    self:SetStatus(guild_index, "Newest deposit: " .. event.user
+                     .. " " .. event.amount .. "g  " .. ago_str .. " ago")
 end

--- Parsing/Formatting SavedVariables history ---------------------------------
+-- Parse/Format SavedVariables history ----------------------------------------

 -- Lua lacks a split() function. Here's a cheesy hardwired one that works
 -- for our specific need.
@@ -180,8 +192,33 @@ function GuildGoldDeposits:split(str)
            , string.sub(str, 1 + t2)
 end

--- Return the one newest history line, if any. Return nil if not.
-function GuildGoldDeposits:HistoryNewest(guild_index)
+-- Convert an event to a compact string that a line-parser can easily consume.
+function GuildGoldDeposits:EventToString(event)
+                        -- tab-delimited fields
+                        -- date     seconds since the epoch
+                        -- amount
+                        -- user     unquoted, can contain all sorts of
+                        --          noise but unlikely to contian a
+                        --          tab character.
+                        --
+                        -- using tostring() here so that this function can work
+                        -- when debugging nil event elements.
+    return tostring(event.time)
+            .. '\t' .. tostring(event.amount)
+            .. '\t' .. tostring(event.user)
+end
+
+function GuildGoldDeposits:StringToEvent(str)
+    ts, amt, user = self:split(str)
+    return { time   = ts
+           , amount = amt
+           , user   = user
+           }
+end
+
+-- Return the one newest history line, if any, from our previous save.
+-- Return nil if not.
+function GuildGoldDeposits:SavedHistoryNewest(guild_index)
     guildName = GetGuildName(guildId)
     if not self.savedVariables then return nil end
     if not self.savedVariables.history then return nil end
@@ -191,10 +228,16 @@ function GuildGoldDeposits:HistoryNewest(guild_index)
     return history[1]
 end

--- Saving Guild Data ---------------------------------------------------------
+-- Fetch Guild Data from the server ------------------------------------------
+--
+-- Fetch _all_ events for each guild. Server holds no more than 10 days, no
+-- more than 500 events.
+--
+-- Defer per-event iteration until fetch is complete. This might help reduce
+-- the clock skew caused by the items using relative time, but relative
+-- to _what_?

 function GuildGoldDeposits:SaveNow()
-    self.savedVariables.history = {}
     self.event_list = {}
     for guild_index = 1, self.max_guild_ct do
         if self.savedVariables.enable_guild[guild_index] then
@@ -272,7 +315,9 @@ function GuildGoldDeposits:ServerDataComplete(guild_index)
     end
     self:SetStatus(guild_index, "scanned events: " .. event_ct
                    .. "  gold deposits: " .. found_ct)
-    self.savedVariables.history[guild_name] = self.event_list[guild_index]
+    self.savedVariables.history[guild_name]
+        = self:MergeHistories( self.event_list[guild_index]
+                             , self.savedVariables.history[guild_name])
 end

 function GuildGoldDeposits:RecordEvent(guild_index, event)
@@ -283,15 +328,107 @@ function GuildGoldDeposits:RecordEvent(guild_index, event)
     table.insert(t, self:EventToString(event))
 end

--- Convert an event to a compact string that a line-parser can easily consume.
-function GuildGoldDeposits:EventToString(event)
-                        -- tab-delimited fields
-                        -- date     seconds since the epoch
-                        -- amount
-                        -- user     unquoted, can contain all sorts of
-                        --          noise but unlikely to contian a
-                        --          tab character.
-    return string.format("%d\t%d\t%s", event.time, event.amount, event.user)
+-- Merging saved and fetched history -----------------------------------------
+
+-- Return a new list composed of all of "fetched", and the latter portion of
+-- "saved" that comes after "fetched", but not older than max_day_ct
+function GuildGoldDeposits:MergeHistories(fetched, saved)
+    -- Where in "saved" does "fetch" end?
+
+                        -- No saved events? Just use whatever we fecthed.
+    if 0 == #saved then
+        return fetched
+    end
+                        -- Create a short list of the last few fetched events.
+                        -- We'll scan saved for these events to match up the
+                        -- two lists.
+    last_rows = self:LastRows(fetched, 5)
+    f_events = {}
+    for i,f_row in ipairs(last_rows) do
+        f_event = self:StringToEvent(f_row)
+        table.insert(f_events, f_event)
+        d("f_event["..#f_events.."]: " .. f_row)
+    end
+                        -- If we fetched nothing at all, retain saved
+                        -- unchanged. Don't even bother to strip older events.
+                        -- Something's probably gone wrong (or the guild has
+                        -- gone very, very, quiet).
+    if 0 == #f_events then return fetched end
+
+    s_i_found = self:Find(f_events, saved)
+    if not s_i_found then
+        d("Not Found, retaining all of saved")
+        s_overlap_end_i = 0
+    else
+        s_overlap_end_i = s_i_found + #f_events - 1
+        d("Found, ending in s_i:"..s_overlap_end_i)
+    end
+    for s_i = s_overlap_end_i + 1,#saved do
+        table.insert(fetched, saved[s_i])
+    end
+    return fetched
+end
+
+-- Return the index into saved that matches f_events.
+-- Return nil if not found.
+function GuildGoldDeposits:Find(f_events, saved)
+    if (0 == #f_events) or (0 == #saved) then return nil end
+    --for i = 1,#saved
+    do
+        i=#saved
+        if self:PatternMatch(i, f_events, saved) then
+            return i - #f_events + 1
+        end
+    end
+    return nil
+end
+
+-- If saved[s_i] and its precursors match f_events, return true.
+-- If not, return false.
+function GuildGoldDeposits:PatternMatch(s_i, f_events, saved)
+    s_event = {}
+    for i = 0, math.min(s_i, #f_events) - 1 do
+        s_ii    = s_i - i
+        f_ii    = #f_events - i
+        s_row   = saved[s_ii]
+        s_event = self:StringToEvent(saved[s_i - i])
+        f_event = f_events[f_ii]
+        match   = self:EventsMatch(f_event, s_event)
+        d("pm " ..tostring(match)
+            .. " s_i:" .. s_i
+            .." i:"..i
+            .." f_ii:"..f_ii.." "..self:EventToString(f_event)
+            .." s_ii:"..s_ii.." "..s_row
+            )
+        if not match then return false end
+    end
+    return true
+end
+
+-- Return the last "ct" rows from "list".
+-- Or fewer if list doesn't have that many rows.
+function GuildGoldDeposits:LastRows(list, ct)
+    r = {}
+    for i = math.min(ct, #list),1,-1 do
+        list_i = #list-i+1
+        d("lr list[" .. list_i .."] " ..tostring(list[list_i]))
+        table.insert(r, list[list_i])
+    end
+    return r
+end
+
+-- Do these rows "match"?
+--
+-- User and amount must be exact.
+-- Time must be within N seconds.
+function GuildGoldDeposits:EventsMatch(f_event, s_event)
+    d("f=" .. self:EventToString(f_event))
+    d("s=" .. self:EventToString(s_event))
+    m1 =  math.abs(f_event.time - s_event.time) < TIMESTAMPS_CLOSE_SECS
+    m2 = f_event.amount == s_event.amount
+    m3 = f_event.user   == s_event.user
+    d("m=" .. tostring(m1) .. " " .. tostring(m2) .. " " .. tostring(m3))
+    return m1 and m2 and m3
 end

 -- Postamble -----------------------------------------------------------------