首頁 > 軟體

Golang 統計字串中數位字母數量的實現方法

2022-06-01 14:01:40

1.需求說明

記錄一下專案對使用者 UGC 文字進行字數限制的具體實現。

不同的產品,出於種種原因,一般都會對使用者輸入的文字內容做字數限制。

  • 出於產品定位,比如 140 字元限制的 Twitter,讓內容保持簡潔凝練,易於閱讀;
  • 出於使用者的閱讀體驗,過多的文字會造成閱讀疲勞,合適的字數能夠提高閱讀舒適度;
  • 出於技術與成本的考慮,不設上限的 UGC 內容會引發一些潛在的問題,比如增加儲存的成本,降低檢索效率等。

回到自己的專案,是一個使用者發帖的業務場景。產品同學給到的要求是:

  • 貼文名稱,限制在 25 個字;
  • 貼文正文,限制在 1500 字;
  • 關於字的說明:1 個漢字為一個字,一個 Emoji 表情相當於 1 個字,2 個數位/英文字母相當於 1 個字。

正常情況下,漢字,Emoji 字元,數位與英文字母都是單獨的字元。這裡 2 個數位/英文算作 1 個字,所以在計算字串長度時,不能夠使用 []rune 強轉後來獲取其長度,而是需要統計出數位與英文字母的數量,再加上其他字元數量,作為其長度。所以,要想實現產品同學的要求,關鍵是需要統計出使用者輸入文字中的數位與英文字母的數量。

2.實現

在 Golang,一般有兩種方法。

2.1 ASCII 碼值法

數位和英文字母的 ASCII 碼值我們是知道的,通過對原字串遍歷,便可統計出數位/英文字母的數量。

// GetAlphanumericNumByASCII 根據 ASCII 碼值獲取字母數位數量。
func GetAlphanumericNumByASCII(s string) int {
	num := int(0)
	for i := 0; i < len(s); i++ {
		switch {
		case 48 <= s[i] && s[i] <= 57: // 數位
			fallthrough
		case 65 <= s[i] && s[i] <= 90: // 大寫字母
			fallthrough
		case 97 <= s[i] && s[i] <= 122: // 小寫字母
			num++
		default:
		}
	}
	return num
}

// 或者
// GetAlphanumericNumByASCIIV2 根據 ASCII 碼值獲取字母數位數量。
func GetAlphanumericNumByASCIIV2(s string) int {
	num := int(0)
	for _, c := range s {
		switch {
		case '0' <= c && c <= '9':
			fallthrough
		case 'a' <= c && c <= 'z':
			fallthrough
		case 'A' <= c && c <= 'Z':
			num++
		default:
		}
	}
	return num
}

2.2 正規表示式

我們可以利用 Golang 標準庫包 regexp 獲取指定表示式的字串數量。

// GetAlphanumericNumByRegExp 根據正規表示式獲取字母數位數量。
func GetAlphanumericNumByRegExp(s string) int {
	rNum := regexp.MustCompile(`d`)
	rLetter := regexp.MustCompile("[a-zA-Z]")
	return len(rNum.FindAllString(s, -1)) + len(rLetter.FindAllString(s, -1))
}

我們可以寫個單測來驗證下上面三個函數的正確性。

package string
import "testing"
func TestGetAlphanumericNumByASCII(t *testing.T) {
	type args struct {
		s string
	}
	tests := []struct {
		name string
		args args
		want int
	}{
		{
			name: "包含數位",
			args: args{"108條梁山好漢"},
			want: 3,
		},
		{
			name: "包含字母",
			args: args{"一百條梁山man"},
			want: 3,
		},
		{
			name: "包含數位與字母",
			args: args{"108條梁山man"},
			want: 6,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := GetAlphanumericNumByASCII(tt.args.s); got != tt.want {
				t.Errorf("GetAlphanumericNumByASCII() = %v, want %v", got, tt.want)
			}
		})
	}
}
func TestGetAlphanumericNumByASCIIV2(t *testing.T) {
	type args struct {
		s string
	}
	tests := []struct {
		name string
		args args
		want int
	}{
		{
			name: "包含數位",
			args: args{"108條梁山好漢"},
			want: 3,
		},
		{
			name: "包含字母",
			args: args{"一百條梁山man"},
			want: 3,
		},
		{
			name: "包含數位與字母",
			args: args{"108條梁山man"},
			want: 6,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := GetAlphanumericNumByASCIIV2(tt.args.s); got != tt.want {
				t.Errorf("GetAlphanumericNumByASCII() = %v, want %v", got, tt.want)
			}
		})
	}
}
func TestGetAlphanumericNumByRegExp(t *testing.T) {
	type args struct {
		s string
	}
	tests := []struct {
		name string
		args args
		want int
	}{
		{
			name: "包含數位",
			args: args{"108條梁山好漢"},
			want: 3,
		},
		{
			name: "包含字母",
			args: args{"一百條梁山man"},
			want: 3,
		},
		{
			name: "包含數位與字母",
			args: args{"108條梁山man"},
			want: 6,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := GetAlphanumericNumByRegExp(tt.args.s); got != tt.want {
				t.Errorf("GetAlphanumericNumByRegExp() = %v, want %v", got, tt.want)
			}
		})
	}
}

執行go test main/string命令,其中 main/string 為單元測試所在包的路徑。輸出如下:

ok      main/string     0.355s

驗證無誤。

3.效能對比

上面提到的兩種方法都可以用來獲取字串中數位與英文字母的數量,那麼我們應該採用哪一種方法呢?

功能上沒有差別,那麼我們來看下效能對比吧。

func BenchmarkGetAlphanumericNumByASCII(b *testing.B) {
	for n := 0; n < b.N; n++ {
		GetAlphanumericNumByASCII("108條梁山man")
	}
}
func BenchmarkGetAlphanumericNumByASCIIV2(b *testing.B) {
	for n := 0; n < b.N; n++ {
		GetAlphanumericNumByASCIIV2("108條梁山man")
	}
}
func BenchmarkGetAlphanumericNumByRegExp(b *testing.B) {
	for n := 0; n < b.N; n++ {
		GetAlphanumericNumByRegExp("108條梁山man")
	}
}

執行上面的基準測試,輸出如下:

go test -bench=. -benchmem main/string

goos: windows
goarch: amd64
pkg: main/string
cpu: Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz
BenchmarkGetAlphanumericNumByASCII-8            89540210                12.67 ns/op            0 B/op          0 allocs/op
BenchmarkGetAlphanumericNumByASCIIV2-8          63227778                19.11 ns/op            0 B/op          0 allocs/op
BenchmarkGetAlphanumericNumByRegExp-8             465954              2430 ns/op            1907 B/op         27 allocs/op
PASS
ok      main/string     3.965s

不測不知道,一測嚇一跳。通過正規表示式的實現方式,程式碼雖然簡潔,但是涉及多次記憶體配分,效能與 ASCII 碼值法相比,差距非常之大,是 ASCII 碼值法的 200 倍左右。所以從效能的考慮,推薦使用 ASCII 碼值的方式獲取數位字母數量。

ASCII 碼值法有兩種遍歷方式,一種是按照位元組遍歷,一種是按照 rune 字元遍歷。因為後者涉及 rune 字元的判斷,所以效能會差一些。推薦使用按照位元組遍歷。

4.小結

本文給出了兩種從字串獲取數位與字母數量的方法:

  • ASCII 碼值。
  • 正規表示式。

出於效能的考慮,推薦使用 ASCII 碼值法,並使用位元組遍歷的方式。

此外,本文給出的兩種方法,三種實現方式,相關原始碼已放置開源庫 go-huge-util,可 import 直接使用。

package main
import (
	"fmt"
	huge "github.com/dablelv/go-huge-util"
)
func main() {
	fmt.Println(huge.GetAlphanumericNumByASCII("108條梁山man"))  	// 6
	fmt.Println(huge.GetAlphanumericNumByASCIIV2("108條梁山man"))  	// 6
	fmt.Println(huge.GetAlphanumericNumByRegExp("108條梁山man")) 	// 6
}

參考文獻

golang統計出其中英文字母、空格、數位和其它字元的個數

到此這篇關於Golang 統計字串中數位字母數量的文章就介紹到這了,更多相關Golang 統計字串內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com