From 1ee022c237de4c4f3d30a9211764d4313c2487fe Mon Sep 17 00:00:00 2001 From: "daniel.w" Date: Thu, 5 Sep 2024 04:24:31 +0800 Subject: [PATCH] add bitmap --- Makefile | 2 +- go.mod | 8 -- go.work | 1 + utils/bitmap/bitmap.go | 152 ++++++++++++++++++++++++++ utils/bitmap/bitmap_benchmark_test.go | 69 ++++++++++++ utils/bitmap/bitmap_test.go | 137 +++++++++++++++++++++++ utils/bitmap/go.mod | 11 ++ utils/bitmap/usecase.go | 16 +++ utils/invited_code/convert_test.go | 3 +- 9 files changed, 389 insertions(+), 10 deletions(-) create mode 100644 utils/bitmap/bitmap.go create mode 100644 utils/bitmap/bitmap_benchmark_test.go create mode 100644 utils/bitmap/bitmap_test.go create mode 100644 utils/bitmap/go.mod create mode 100644 utils/bitmap/usecase.go diff --git a/Makefile b/Makefile index 238a31a..78032f7 100644 --- a/Makefile +++ b/Makefile @@ -9,4 +9,4 @@ test: # 進行測試 fmt: # 格式優化 $(GOFMT) -w $(GOFILES) goimports -w ./ - + golangci-lint run \ No newline at end of file diff --git a/go.mod b/go.mod index 9c45fcb..aadd916 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,3 @@ module code.30cm.net/digimon/library-go go 1.22.3 - -require github.com/stretchr/testify v1.9.0 - -require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/go.work b/go.work index fd73690..581badf 100644 --- a/go.work +++ b/go.work @@ -7,4 +7,5 @@ use ( ./errs . ./utils/invited_code + ./utils/bitmap ) diff --git a/utils/bitmap/bitmap.go b/utils/bitmap/bitmap.go new file mode 100644 index 0000000..5218eef --- /dev/null +++ b/utils/bitmap/bitmap.go @@ -0,0 +1,152 @@ +package usecase + +// Bitmap 基礎結構 +// Bitmap 是一個位圖結構,使用 byte slice 來表示大量的位(bit)。 +// 每個 byte 由 8 個位組成,因此可以高效地管理大量的開關狀態(true/false)。 +// Bitmap 的優點在於它能節省空間,尤其是在需要大量布爾值的場合。 +// 缺點是,如果需要動態擴充 Bitmap 的大小,會導致效率下降,因為需要重新分配和移動內存。 +// 因此,最好在初始化時就規劃好所需的位數大小,避免在之後頻繁擴充。 + +type Bitmap []byte + +// MakeBitmapWithBitSize 通過指定的 bit 數創建一個新的 Bitmap。 +// 參數 nBits 表示所需的位(bit)數。 +// 如果指定的位數少於 64,則默認將 Bitmap 初始化為 64 位(這是最低的限制)。 +// 此外,位數(nBits)會被自動調整為 8 的倍數,以適配 byte 的長度(每 8 位為一個 byte)。 +// 返回值是一個 Bitmap(byte slice),其大小根據位數確定。 +func MakeBitmapWithBitSize(nBits int) Bitmap { + // 如果指定的位數少於 64,則設置為 64 位(8 個 byte) + if nBits < 64 { + nBits = 64 + } + // 計算需要的 byte 數,確保每 8 位為一個 byte,並調整 nBits 以達到 8 的倍數 + return MustBitMap((nBits + 7) / 8) +} + +// MustBitMap 根據指定的 byte 數創建一個 Bitmap(byte slice)。 +// 參數 nBytes 表示所需的 byte 數。 +// 返回值是一個長度為 nBytes 的 Bitmap。 +func MustBitMap(nBytes int) Bitmap { + // 使用 make 函數創建一個 byte slice,大小為 nBytes。 + return make([]byte, nBytes) +} + +// SetTrue 設置指定位置的 bit 為 true(1)。 +// 參數 bitPos 是需要設置的位的位置(以 0 為基準的位索引)。 +// 這個操作會找到該 bit 所在的 byte,然後通過位運算將該位置的 bit 設置為 1。 +func (b Bitmap) SetTrue(bitPos uint32) { + // |= 是一種位運算的複合賦值運算符,表示將左邊的變數與右邊的值進行 位或運算(bitwise OR),並將結果賦值 + b[bitPos/8] |= 1 << (bitPos % 8) +} + +// SetFalse 設置指定位置的 bit 為 false(0)。 +// 參數 bitPos 是需要設置的位的位置(以 0 為基準的位索引)。 +// 這個操作會找到該 bit 所在的 byte,然後通過位運算將該位置的 bit 設置為 0。 +func (b Bitmap) SetFalse(bitPos uint32) { + // 取出對應 byte,使用位與和取反運算將對應的 bit 設置為 0 + // 假設我們有以下情況: + + // • b[1](即 b[bitPos/8])是 10101111(十進制 175)。 + // • bitPos = 10,也就是我們想清除第 10 位的值。 + // + // 操作步驟: + // + // 1. bitPos/8 = 1:所以我們要修改 b[1] 這個 byte。 + // 2. bitPos % 8 = 2:表示我們要清除的位是這個 byte 中的第 3 位(從右數起第 3 位)。 + // 3. 1 << (bitPos % 8) = 1 << 2 = 00000100:生成位掩碼 00000100。 + // 4. 取反:^(1 << 2) = ^00000100 = 11111011,這樣的掩碼表示除了第 3 位,其他位都是 1。 + // 5. 位與運算:10101111 & 11111011 = 10101011,結果將第 3 位清除,其餘位保持不變。即,b[1] 變成了 10101011(十進制 171)。 + // &= 是一種 位與運算的複合賦值運算符,表示將左邊的變數與右邊的值進行 位與運算(bitwise AND),然後將結果賦值給左邊的變數。 + b[bitPos/8] &= ^(1 << (bitPos % 8)) +} + +// IsTrue 檢查指定位置的 bit 是否為 true(1)。 +// 參數 bitPos 是要檢查的位的位置(以 0 為基準的位索引)。 +// 如果該 bit 是 1,則返回 true;否則返回 false。 +func (b Bitmap) IsTrue(bitPos uint32) bool { + /* + 這一行程式碼 b[bitPos/8]&(1<<(bitPos%8)) != 0 是用來檢查 指定位(bit) 是否為 true(1), + 它的核心是位運算。讓我們逐步拆解並解釋這一行程式碼: + + 1. 背景知識: + + • 位運算 是在二進制層面操作數字。每個 byte 有 8 個位(bit),所以位圖結構是以 byte 來表示位的集合。 + • b 是一個 Bitmap 結構,也就是 []byte,即 byte 的切片。 + • bitPos 是一個 uint32 類型的變數,表示我們想要檢查的位(bit)在整個位圖中的索引(位置)。 + 3. 完整流程舉例: + 假設: + • b = []byte{0b10101010, 0b01010101} (即二進制表示的兩個 byte,分別是 10101010 和 01010101)。 + • bitPos = 10(我們要檢查第 10 位是否為 1)。 + 操作順序: + + 1. 計算 bitPos/8 = 10/8 = 1,所以我們要檢查的是第二個 byte:0b01010101。 + 2. 計算 bitPos%8 = 10%8 = 2,所以我們要檢查的是該 byte 中的第 3 位(從右數起第 3 位)。 + 3. 位移:1 << 2 = 00000100。 + 4. 位與:0b01010101 & 0b00000100 = 0b00000100(因為該 byte 的第 3 位是 1,結果不等於 0)。 + 5. 判斷結果:結果不等於 0,因此第 10 位是 1(true)。 + + 4. 總結: + + • b[bitPos/8]&(1<<(bitPos%8)) != 0 是一個經典的位操作,用來檢查位圖中某一個位是否為 1。 + • bitPos/8 找到對應的 byte,bitPos % 8 找到該位在這個 byte 中的具體位置。 + • 最後的位與運算和比較用來確定該位的狀態是 true(1)還是 false(0)。 + */ + return b[bitPos/8]&(1<<(bitPos%8)) != 0 +} + +// Reset 重置 Bitmap,使所有的 bit 都設置為 false(0)。 +// 這個操作會將整個 Bitmap 的所有 byte 都設置為 0,從而達到重置的效果。 +func (b Bitmap) Reset() { + // 迭代 Bitmap 中的每個 byte,並將其設置為 0 + for i := range b { + b[i] = 0 + } +} + +// ByteSize 返回 Bitmap 的 byte 長度。 +// 這個函數返回 Bitmap 目前占用的 byte 數量,該值等於 Bitmap 的長度(slice 的長度)。 +func (b Bitmap) ByteSize() int { + return len(b) +} + +// BitSize 返回 Bitmap 的位(bit)長度。 +// 這個函數通過將 byte 長度乘以 8 來計算 Bitmap 中的總位數。 +func (b Bitmap) BitSize() int { + // 每個 byte 包含 8 個 bit,因此將 byte 長度乘以 8 + return len(b) * 8 +} + +// Resize 重新調整 Bitmap 的大小,將其擴展為指定的 bit 大小。 +// 原來的數據會被保留,新增加的部分位將初始化為 0。 +// 參數 newBitSize 是 Bitmap 需要擴展到的位大小。 +func (b Bitmap) Resize(newBitSize int) Bitmap { + newByteSize := (newBitSize + 7) / 8 + if newByteSize <= len(b) { + // 如果新大小比原大小小或相等,不做任何操作,直接返回原 Bitmap + return b + } + + // 創建新的 Bitmap + newBitmap := make([]byte, newByteSize) + + // 將原 Bitmap 複製到新 Bitmap 中 + copy(newBitmap, b) + + // 返回新的 Bitmap + return newBitmap +} + +// UpdateFrom 會將傳入的 Bitmap 的值更新到當前 Bitmap 中。 +// 如果傳入的 Bitmap 大於當前 Bitmap,則當前 Bitmap 會自動擴展並保存新 Bitmap 的數據。 +// 如果傳入的 Bitmap 小於當前 Bitmap,僅更新前面部分的數據。 +func (b Bitmap) UpdateFrom(newBitmap Bitmap) Bitmap { + // 如果 newBitmap 比當前 Bitmap 長,則擴展當前 Bitmap 的大小 + if len(newBitmap) > len(b) { + // 創建一個擴展過的 Bitmap + expandedBitmap := make([]byte, len(newBitmap)) + copy(expandedBitmap, b) // 將當前 Bitmap 的數據複製到擴展的 Bitmap 中 + b = expandedBitmap // 更新當前 Bitmap 的引用 + } + + return b +} diff --git a/utils/bitmap/bitmap_benchmark_test.go b/utils/bitmap/bitmap_benchmark_test.go new file mode 100644 index 0000000..54eeed4 --- /dev/null +++ b/utils/bitmap/bitmap_benchmark_test.go @@ -0,0 +1,69 @@ +package usecase + +import "testing" + +// 基準測試 SetTrue 函數,測試在不同大小的 Bitmap 上設置位元為 true 的效能 +func BenchmarkBitmapSetTrue(b *testing.B) { + // 以 10^6 位元作為基準測試的 Bitmap 大小 + bitmap := MakeBitmapWithBitSize(1000000) + b.ResetTimer() // 重設計時器,排除初始化的時間 + + for i := 0; i < b.N; i++ { + bitmap.SetTrue(uint32(i % 1000000)) // 設置位元為 true + } +} + +// 基準測試 SetFalse 函數,測試在不同大小的 Bitmap 上清除位元為 false 的效能 +func BenchmarkBitmapSetFalse(b *testing.B) { + // 以 10^6 位元作為基準測試的 Bitmap 大小 + bitmap := MakeBitmapWithBitSize(1000000) + b.ResetTimer() // 重設計時器,排除初始化的時間 + + for i := 0; i < b.N; i++ { + bitmap.SetFalse(uint32(i % 1000000)) // 清除位元為 false + } +} + +// 基準測試 IsTrue 函數,測試在不同大小的 Bitmap 上檢查位元狀態的效能 +func BenchmarkBitmapIsTrue(b *testing.B) { + // 以 10^6 位元作為基準測試的 Bitmap 大小 + bitmap := MakeBitmapWithBitSize(1000000) + b.ResetTimer() // 重設計時器,排除初始化的時間 + + for i := 0; i < b.N; i++ { + _ = bitmap.IsTrue(uint32(i % 1000000)) // 檢查位元是否為 true + } +} + +// 基準測試 Reset 函數,測試重置不同大小的 Bitmap 的效能 +func BenchmarkBitmapReset(b *testing.B) { + // 以 10^6 位元作為基準測試的 Bitmap 大小 + bitmap := MakeBitmapWithBitSize(1000000) + b.ResetTimer() // 重設計時器,排除初始化的時間 + + for i := 0; i < b.N; i++ { + bitmap.Reset() // 重置 Bitmap + } +} + +// 基準測試 BitSize 函數,測試返回位圖的 bit 長度的效能 +func BenchmarkBitmapBitSize(b *testing.B) { + // 以 10^6 位元作為基準測試的 Bitmap 大小 + bitmap := MakeBitmapWithBitSize(1000000) + b.ResetTimer() // 重設計時器,排除初始化的時間 + + for i := 0; i < b.N; i++ { + _ = bitmap.BitSize() // 測試返回位圖的 bit 長度 + } +} + +// 基準測試 ByteSize 函數,測試返回位圖的 byte 長度的效能 +func BenchmarkBitmapByteSize(b *testing.B) { + // 以 10^6 位元作為基準測試的 Bitmap 大小 + bitmap := MakeBitmapWithBitSize(1000000) + b.ResetTimer() // 重設計時器,排除初始化的時間 + + for i := 0; i < b.N; i++ { + _ = bitmap.ByteSize() // 測試返回位圖的 byte 長度 + } +} diff --git a/utils/bitmap/bitmap_test.go b/utils/bitmap/bitmap_test.go new file mode 100644 index 0000000..d743d38 --- /dev/null +++ b/utils/bitmap/bitmap_test.go @@ -0,0 +1,137 @@ +package usecase + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBitmap_SetTrueAndIsTrue(t *testing.T) { + tests := []struct { + name string + bitPos uint32 + expected bool + }{ + {"Set bit 0 to true", 0, true}, + {"Set bit 1 to true", 1, true}, + {"Set bit 63 to true", 63, true}, + {"Set bit 64 to true", 64, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bitmap := MakeBitmapWithBitSize(128) // 初始化一個 128 位的 Bitmap + bitmap.SetTrue(tt.bitPos) + result := bitmap.IsTrue(tt.bitPos) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestBitmap_SetFalse(t *testing.T) { + tests := []struct { + name string + bitPos uint32 + expected bool + }{ + {"Set bit 0 to false", 0, false}, + {"Set bit 1 to false", 1, false}, + {"Set bit 63 to false", 63, false}, + {"Set bit 64 to false", 64, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bitmap := MakeBitmapWithBitSize(128) // 初始化一個 128 位的 Bitmap + bitmap.SetTrue(tt.bitPos) // 先設置該 bit 為 true + bitmap.SetFalse(tt.bitPos) // 然後設置該 bit 為 false + result := bitmap.IsTrue(tt.bitPos) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestBitmap_Reset(t *testing.T) { + bitmap := MakeBitmapWithBitSize(128) // 初始化一個 128 位的 Bitmap + bitmap.SetTrue(0) + bitmap.SetTrue(64) + + // 確認 bit 0 和 bit 64 是 true + assert.True(t, bitmap.IsTrue(0)) + assert.True(t, bitmap.IsTrue(64)) + + bitmap.Reset() // 重置位圖 + + // 確認所有的位都已經重置為 false + assert.False(t, bitmap.IsTrue(0)) + assert.False(t, bitmap.IsTrue(64)) +} + +func TestBitmap_ByteSize(t *testing.T) { + bitmap := MakeBitmapWithBitSize(64) // 初始化一個 64 位的 Bitmap + assert.Equal(t, 8, bitmap.ByteSize()) // 64 位應該佔用 8 個 byte +} + +func TestBitmap_BitSize(t *testing.T) { + bitmap := MakeBitmapWithBitSize(128) // 初始化一個 128 位的 Bitmap + assert.Equal(t, 128, bitmap.BitSize()) // 128 位應該有 128 個 bit +} + +func TestBitmap_Resize(t *testing.T) { + tests := []struct { + name string + initialSize int + newBitSize int + expectedLen int + }{ + {"Resize to larger size", 64, 128, 16}, + {"Resize to smaller size", 128, 64, 16}, // 大小應保持不變 + {"Resize to equal size", 64, 64, 8}, // 大小應保持不變 + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 初始化一個 Bitmap + bitmap := MakeBitmapWithBitSize(tt.initialSize) + + // 調用 Resize + resizedBitmap := bitmap.Resize(tt.newBitSize) + + // 檢查結果的 byte 大小 + assert.Equal(t, tt.expectedLen, len(resizedBitmap)) + }) + } +} + +func TestBitmap_UpdateFrom(t *testing.T) { + tests := []struct { + name string + initialSize int + newBitmapSize int + expectedBitPos uint32 + expectedResult bool + }{ + {"Update to larger bitmap", 64, 128, 50, true}, + {"Update to smaller bitmap", 128, 64, 50, true}, // 新的 Bitmap 將只更新前面部分 + {"Update to equal size bitmap", 64, 64, 20, true}, // 將相同大小的 Bitmap 進行合併 + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 初始化一個初始大小的 Bitmap + bitmap := MakeBitmapWithBitSize(tt.initialSize) + bitmap.SetTrue(tt.expectedBitPos) + + // 初始化一個新的 Bitmap + newBitmap := MakeBitmapWithBitSize(tt.newBitmapSize) + newBitmap.SetTrue(tt.expectedBitPos) + + // 更新 Bitmap + updatedBitmap := bitmap.UpdateFrom(newBitmap) + + // 檢查預期的位是否為 true + result := updatedBitmap.IsTrue(tt.expectedBitPos) + assert.Equal(t, tt.expectedResult, result) + }) + } +} diff --git a/utils/bitmap/go.mod b/utils/bitmap/go.mod new file mode 100644 index 0000000..c3ac5cb --- /dev/null +++ b/utils/bitmap/go.mod @@ -0,0 +1,11 @@ +module code.30cm.net/digimon/library-go/utils/bitmap + +go 1.22.3 + +require github.com/stretchr/testify v1.9.0 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/utils/bitmap/usecase.go b/utils/bitmap/usecase.go new file mode 100644 index 0000000..05379b8 --- /dev/null +++ b/utils/bitmap/usecase.go @@ -0,0 +1,16 @@ +package usecase + +type BitMapUseCase interface { + // SetTrue 設定該 Bit 狀態為 true + SetTrue(bitPos uint32) + // SetFalse 設定該Bit 狀態為 false + SetFalse(bitPos uint32) + // IsTrue 確認是否為真 + IsTrue(bitPos uint32) bool + // Reset 重設 BitMap + Reset() + // ByteSize 最大 Byte 數 + ByteSize() int + // BitSize 最大 Byte * 8 + BitSize() int +} diff --git a/utils/invited_code/convert_test.go b/utils/invited_code/convert_test.go index 2ab1a76..a19bdae 100644 --- a/utils/invited_code/convert_test.go +++ b/utils/invited_code/convert_test.go @@ -1,8 +1,9 @@ package invited_code import ( - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) // 測試 ConvertUseCase 的功能