Go 時間交併集小工具

steak發表於2019-02-01

示例程式碼(含測試)在這裡

需求

在甘特圖的場景下,我們經常會遇到這種情況,五位員工A, B, C, D, E,可能他們的工作都是並行的,我們需要計算 某段時間內他們總的工作時長。

我們不能簡單得把五個人的工作時間都加起來,因為當中會有重疊的部分。 所以這時候我們就需要一個計算時間交併集的工具。

思路

將一組離散的時間段按照開始時間,從小到大排序。像這樣

[{2 7} {4 11} {10 19} {10 30} {16 18} {19 29} {23 35} {24 42} {25 30} {27 49}]
複製程式碼

我這裡將時間用十分小的秒來代替,方便理解。

迴圈排序後的陣列,如果下一個時間段開始時間介於上個時間段的開始時間和結束時間之間,那麼就進行合併,否則就分離。 可以看到我們這裡有兩個關鍵動作,合併分離,而這個就是我們要實現的核心程式碼。

實現

一段連續的工作時間都會有兩個點,開始時間結束時間。 所以我們可以把這個時間結構設計成:

type T struct {
    Start int64
    End   int64
}
複製程式碼

而一個人的工作時間是由多個 T 組成的,所以我們在定義一個切片型別

type TSlice []T
複製程式碼

為了能順序合併時間,我們需要將TSlice進行排序。 我們知道 Go 中有個 sort 包,我們只需要實現 sort 型別的介面,就能實現 TSlice 的排序了。 我們實現下:

func (t TSlice) Len() int { return len(t) }

func (t TSlice) Swap(i, j int) { t[i], t[j] = t[j], t[i] }

func (t TSlice) Less(i, j int) bool { return t[i].Start < t[j].Start }
複製程式碼

三個方法分別是,長度、交換位置、比小。

這樣一來,我們就能直接用 sort.Stable() 穩定排序,對我們的時間段切片排序了。

好,接下來我們實現並集的方法,我們取名為 Union

func (t TSlice) Union() TSlice {
    // 新建一個空的時間切片
    var s TSlice
    // 如果有至少兩個是時間段,我們才排序,否則直接返回
    if len(t) > 1 {
        // @todo 合併邏輯
    }

    return s
}
複製程式碼

Union 方法將會返回一個同樣的 TSlice 時間切片,只不過是經過並集處理的。

一旦 t 中的時間段個數大於1,我們就要執行處理邏輯了:

if len(t) > 1 {
    sort.Stable(t)
    s = append(s, t[0])

    // @todo 迴圈比較合併
}
複製程式碼

我們先對時間切片進行排序,然後把第一個時間段作為第一個元素放進我們的結果 TSlice 中,好讓我們開始進行循壞的比較。

if len(t) > 1 {
        sort.Stable(t)
        s = append(s, t[0])

        for k, v := range t {
            // 如果開始時間大於結束時間,那其實是錯誤資料,但是我們這裡正常返回
            // 你可以根據自己的需要定製錯誤處理邏輯
            if v.Start > v.End {
                return s
            }
            // 第一組元素我們不做任何操作
            if k == 0 {
                continue
            }

            // 當開始時間介於上一個時間段的開始時間和結束時間之間
            if v.Start >= s[len(s)-1].Start && v.Start <= s[len(s)-1].End {
                // 合併
                if v.End > s[len(s)-1].End {
                    s[len(s)-1].End = v.End
                }
            // 如果大於上一個時間段的結束時間
            } else if v.Start > s[len(s)-1].End {
                // 分離
                inner := T{Start: v.Start, End: v.End}
                s = append(s, inner)
            }
        }
    }
複製程式碼

來張圖其實就清楚了:

1

可以看到最後輸出的也是一個 TSlice 型別。 上面就是 union,求並集的過程,那交集的?

其實交集也很簡單,如果兩個時間段相交,我們只要判斷:開始時間取最大的那個,結束時間取兩個時間段中最小的那個。

func (t TSlice) Intersect() TSlice {
    var s TSlice

    if len(t) > 1 {
        sort.Stable(t)
        s = append(s, t[0])

        for k, v := range t {
            if v.Start > v.End {
                return s
            }

            if k == 0 {
                continue
            }
            // 兩個時間段相交
            if v.Start >= s[0].Start && v.Start <= s[0].End {
                // 開始時間取最大的那個
                s[0].Start = v.Start
                // 結束時間取最小的那個
                if v.End <= s[0].End {
                    s[0].End = v.End
                }
            } else {
                return s[:0]
            }
        }
    }

    return s
}
複製程式碼

一樣,我們來個圖:

2

需要注意的是,這個求交集的結果是全相交--只有當所有時間段都有共同時間才會有結果。 這樣的需求在實際過程中用到的是不是不太多??所以我想是不是能夠實現:一次相交,兩次相交...的條件篩選。

看看效果

我們隨機生成了一組時間切片

func makeTimes(t int) TSlice {
    var set TSlice
    rand.Seed(time.Now().Unix())

    for i := 0; i < t; i++ {
        randStart := rand.Int63n(50)
        randEnd := randStart + rand.Int63n(25) + 1
        set = append(set, T{Start: randStart, End: randEnd})
    }

    return set
}

testSet := makeTimes(10) // 生成10個時間段的時間切片
res := testSet.Union()   // 直接呼叫 Union() 或者 Intersect()
複製程式碼

輸入資料為:

[{10 21} {34 52} {49 54} {18 31} {26 44} {24 27} {43 51} {41 53} {20 41} {48 67}]
複製程式碼

輸出結果:

[{10 67}]
複製程式碼

結果還行~

額外

我發現在求並集的過程中,會要求求最終的時間之和,所以我們為 TSlice 加一個 Sum() 方法, 就是簡單的迴圈求和:

func (t TSlice) Sum() (sum int64) {
    for i := 0; i < len(t); i++ {
        sum += t[i].End - t[i].Start
    }

    return sum
}
複製程式碼

呼叫方法:TSlice.Union().Sum()

相關文章