---
# System prepended metadata

title: 【Lua 筆記】字串（String） - part 8
tags: [Lua, 程式語言]

---

【Lua 筆記】字串（String） - part 8
===

目錄（Table of Contents）：

[TOC]

---

由於有款遊戲叫做 CSO（Counter-Strike Online），內建模式創世者模式（Studio）新增使用 Lua 及其遊戲的 API，所以突發奇想製作這個筆記。

這個筆記會在一開始先著重純粹的程式設計自學，在最後的章節才會與 CSO 遊戲 API 進行應用。

※本文篇幅較長，十分複雜，請小心閱讀，請務必收藏！！※

字串（String）
---

字串（String）是由許多字元（Character）所組成的。

字元（Character）：中文字、英文字母（a-z）、底線等等任何符號皆是一種字元。

字串可以用以下三種方式表示：

* 成對單引號括起來（'String'）
* 成對雙引號括起來（"String"）
* [[ ]] 裡面放字串 -> 多行字串

第三種方式舉例：

```lua=
a = [[
Hi, this is a string
aaaaaaaa
]]

print(a)
```

輸出結果：

```
Hi, this is a string
aaaaaaaa
```

### 取得字串長度的另一種方式
---

`string.len(裡面放字串)`，實際上這個方式與用 `#` 這個運算子的方式並沒有什麼區別，但 `string.len()` 的可讀性會比用 `#` 更高。

以下是用兩種方式的比較：

```lua=
a = [[
Hi, this is a string
aaaaaaaa
]]

print(string.len(a)) --> 30
```

```lua=
a = [[
Hi, this is a string
aaaaaaaa
]]

print(#a) --> 30
```

也可以用 utf8.len 函数：

```lua=
local myString = "Hello, 世界!"

-- utf8.len()
local length1 = utf8.len(myString)
print(length1) -- 输出 10

-- string.len()
local length2 = string.len(myString)
print(length2) -- 输出 14
```

註：utf8 是一種編碼，主要針對 Unicode 進行編碼，詳細具體內容筆者推薦看這篇文章：[UTF-8](https://openhome.cc/Gossip/Encoding/UTF8.html)

可以看出當我們字串是中文字時，使用 string.len()（註：使用 `#` 的效果同屬於 string.len()）便會使字串長度輸出不準確，這是因為中文字編碼的關係。

string.len() 跟 # 回傳的會是 ASCII 編碼字串的長度，而 utf8.len() 包含中文字與英文。

所以歸結出一個結論：

* 一般英文字母（ASCII）用 string.len() 或 #
* 中文字與英文字母混合用 utf8.len()

### 跳脫字元（Escape Character）
---

跳脫字元（Escape Character），又稱轉義字元（陸譯）、逃逸字元、逸出字元。是一種能夠針對字串進行特殊操作的字元，例如原本不能在字串裡面顯示單雙引號的字元，所以透過反斜線（`\`）結合單雙引號形成一個跳脫字元，就能夠變成一種能夠出現在字串內的字元。

話說這麼多，跳脫字元其實本身還是一個字元（廢話XD），總之他就是一種特殊的字元，能讓字串的操作變得更加多元、方便。

所以要使用跳脫字元之前，必須要有一個反斜線存在（`\`）。

以下有個表格是關於跳脫字元的使用（表格來源：[Lua 字符串 | 菜鸟教程](https://www.runoob.com/lua/lua-strings.html)）：

| 跳脫字元 | 意義 | ASCII碼（十進位） |
| -------- | -------- | -------- |
| \a | 響鈴（BEL：bell） | 007 |
| \b | 退格（BS：BackSpace），將目前位置移到前一列（換言之，就是刪除鍵） | 008 |
| \f | 換頁（FF：Form Feed），將目前位置移到下頁開頭。 | 012 |
| \n | 換行（LF：Line Feed），將目前位置移到下一行開頭。 | 010 |
| \r | 回車（CR：Carriage Return），將目前位置移到本行開頭 | 013 |
| \t | 水平製表（HT：Horizontal Tab）（跳到下一個 TAB 位置，也就是按一下 tab 縮排） | 009 |
| \v | 垂直製表（VT：Vertical Tab） | 011 |
| \\ | 代表一個反斜線字元''\' | 092 |
| \' | 代表一個單引號字元 | 039 |
| \" | 代表一個雙引號字元 | 034 |
| \0 | 空字元（NULL） | 000 |
| \ddd | 1 到 3 位八進位數所代表的任意字元 | 三位八進位 |
| \xhh | 1 到 2 位十六進位所代表的任意字元 | 二位十六進位 |

這個表格呢，等我們真的有需要用到它的功能時，再來看就好了，硬要說一個常用的話，或許就是 \n 了吧。

然後讓我們來練習一下：）

```lua=
print("str\ning")
print("\"string\"")
print("strin\0g")
print("st\bring")
print("s\ftring")
print("strin\rg")
```

輸出結果：

```
str
ing
"string"
string
sring
s
 tring
gtrin
```

不曉得各位有沒有發現，Lua 的 print() 好像會自動幫我們換行欸？如果我不想換行怎麼辦？沒關係，以下這個函數就能夠實現不換行的機制：`io.write()`

### string 的方法（Method）
---

方法（Method）是什麼？這個問題，我們需要牽涉到程式設計當中，一個很重要的概念，稱之為類（Class），通常有 class 的語言，在程式設計上都被稱之為物件導向程式設計（Object-oriented programming）。

而方法其實是一種函數，屬於 class 的一部分，也就是說一個 class 當中可以有很多方法，這些方法都具有不同的功能，目前只要先理解這樣就好。

以下表格來源：[Lua 字符串 | 菜鸟教程](https://www.runoob.com/lua/lua-strings.html)

| Number | 方法及用途 |
| -------- | -------- |
| 1 | **string.upper(argument)** : 字串全部轉為大寫字母。 |
| 2 | **string.lower(argument)** : 字串全部轉為小寫字母。 |
| 3 | **string.gsub(mainString,findString,replaceString,num)** : 在字串中替換。mainString 為要操作的字串，findString 為被替換的字元，replaceString 要替換的字元，num 替換次數（可以忽略，則全部替換）。 |
| 4 | **string.find(str, substr, [init, [plain]])** : 在一個指定的目標字串 str 中搜尋指定的內容 substr，如果找到了一個符合的子字串，就會回傳這個子字串的起始索引和結束索引，不存在則回傳 nil。`init` 指定了搜尋的起始位置，預設為 1，可以是一個負數，表示從後往前數的字元數。`plain` 表示是否使用簡單模式，預設為 false、true，只做簡單的查找子字串的操作，false 表示使用使用正規模式比對。 |
| 5 | **string.reverse(arg)** : 字串反轉 |
| 6 | **string.format(...)** : 回傳一個類似 printf（C 語言 print 函數，表示格式化）的格式化字串 |
| 7 | **string.char(arg) 和 string.byte(arg[,int])** : char 將整數數字轉成字元並連接，byte 轉換字元為整數值(可以指定某個字元，預設第一個字元)。 |
| 8 | **string.len(arg)** : 計算字串長度。 |
| 9 | **string.rep(string, n)** : 回傳字串 string 的 n 個拷貝 |
| 10 | **..** : 連接兩個子字串 |
| 11 | **string.gmatch(str, pattern)** : 回傳一個迭代器函數，每次呼叫這個函數，回傳一個在字串 str 找到的下一個符合 pattern 描述的子字串。如果參數 pattern 所描述的字串沒有找到，迭代函數回傳 nil。 |
| 12 | **string.match(str, pattern, init)** : string.match() 只尋找來源字串 str 中的第一個配對。參數 init 為可選，指定搜尋過程的起點，預設為 1。在成功配對時，函數將回傳配對運算式中的所有捕獲結果；如果沒有設置捕獲標記，則回傳整個配對字串。當沒有成功的配對時，回傳 nil。 |

:::spoiler 點開看以上方法的詳細範例
```lua=
-- 定義一個字串
local str = "Hello, Lua!"

-- 1. 將字串轉為大寫
local upperStr = string.upper(str)
print("Uppercase: " .. upperStr)

-- 2. 將字串轉為小寫
local lowerStr = string.lower(str)
print("Lowercase: " .. lowerStr)

-- 3. 替換字串中的字元
local replacedStr = string.gsub(str, "Lua", "World")
print("Replaced: " .. replacedStr)

-- 4. 搜尋字串中的子字串
local startIdx, endIdx = string.find(str, "Lua")
if startIdx then
    print("Found 'Lua' at: " .. startIdx .. " to " .. endIdx)
else
    print("'Lua' not found")
end

-- 5. 反轉字串
local reversedStr = string.reverse(str)
print("Reversed: " .. reversedStr)

-- 6. 格式化字串
local formattedStr = string.format("Formatted: %s", str)
print(formattedStr)

-- 7. 將整數轉為字元，並將字元轉為整數
local charStr = string.char(76, 117, 97) -- 'Lua'
print("Char: " .. charStr)
local byteVal = string.byte(str, 1)
print("Byte of first character: " .. byteVal)

-- 8. 計算字串長度
local strLen = string.len(str)
print("Length: " .. strLen)

-- 9. 回傳字串的多個拷貝
local repeatedStr = string.rep(str, 2)
print("Repeated: " .. repeatedStr)

-- 10. 連接兩個子字串
local concatenatedStr = str .. " Let's learn Lua!"
print("Concatenated: " .. concatenatedStr)

-- 11. 使用迭代器函數匹配字串
for word in string.gmatch(str, "%a+") do
    print("Word: " .. word)
end

-- 12. 匹配字串中的第一個配對
local matchedStr = string.match(str, "L%a+")
print("Matched: " .. matchedStr)
```

輸出結果：

```
Uppercase: HELLO, LUA!
Lowercase: hello, lua!
Replaced: Hello, World!
Found 'Lua' at: 8 to 10
Reversed: !auL ,olleH
Formatted: Hello, Lua!
Char: Lua
Byte of first character: 72
Length: 11
Repeated: Hello, Lua!Hello, Lua!
Concatenated: Hello, Lua! Let's learn Lua!
Word: Hello
Word: Lua
Matched: Lua
```
:::

### 字串格式化（String-Formating）
---

字串格式化是一個相當重要的語法之一，幾乎是每個程式語言都不可或缺的東西。實際應用有：進度條、醫院掛號顯示號碼、字串+顯示目前數字等等。

總之讓字串格式化之後，就能有許多操作、運算的空間，能讓原本不能動的字串，變成活生生的字串。

> Lua 提供了 `string.format()` 函數來產生具有特定格式的字串，函數的第一個參數是格式， 之後是對應格式中每個代號的各種資料。

以下是格式化字串的 format specifiers（格式化規範符號），來源自 [Lua 字符串 | 菜鸟教程](https://www.runoob.com/lua/lua-strings.html)：

* %c - 接受一個數字，並將其轉換為 ASCII 碼表中對應的字元（將ASCII碼轉成字元）
* %d、%i - 接受一個數字並將其轉換為有號的整數格式
* %o - 接受一個數字並將其轉換為八進位數格式
* %u - 接受一個數字並將其轉換為無號整數格式
* %x - 接受一個數字並將其轉換為十六進位數格式，使用小寫字母
* %X - 接受一個數字並將其轉換為十六進位數格式，使用大寫字母
* %e - 接受一個數字並將其轉換為科學記號格式，使用小寫字母 e
* %E - 接受一個數字並將其轉換為科學記號格式，使用大寫字母 E
* %f - 接受一個數字並將其轉換為浮點數格式
* %g(%G) - 接受一個數字並將其轉換為 %e（%E，對應 %G）及 %f 中較短的一種格式
* %q - 接受一個字串並將其轉換為可安全被 Lua 編譯器讀入的格式
* %s - 接受一個字串並按照給定的參數格式化該字串

:::info
註：%u 被稱為是無號整數，是因為 C 語言有資料型態為 unsigned int、unsigned char 等等。unsigned（u） 就是沒有負數的意思，只有自然數。
:::

> 為進一步細化格式，可以在 % 號後新增參數，參數將以如下的順序讀入：

1. 符號：一個 + 號表示其後的數字 format specifiers 會讓正數顯示正號。預設只有負數顯示符號。
2. 占位修飾符號：一個 0，在後面指定了字串寬度時佔位用。不填時的預設占位修飾符號是空格。
3. 對齊標識：在指定了字串寬度時，預設為向右對齊，增加 - 號可以改為向左對齊。
4. 寬度數值：寬度數值用於指定輸出內容的最小寬度。如果實際內容的寬度小於指定的寬度數值，則會使用占位修飾符號（預設為空格）進行填充，以達到指定的寬度。（如：%05d、%5d）
5. 小數位數 / 字串裁切：在寬度數值後增加的小數部分 n，若後接 f（浮點數 format specifiers，如 %6.3f）則設定該浮點數的小數只保留 n 位，若後接 s（字串 format specifiers，如 %5.3s）則設定該字串只顯示前 n 位。

以下是個範例：

```lua=
local number = 123.456
local integer = 42
local str = "Hello, Lua!"

-- 1. 符號：顯示正號
print(string.format("%+d", integer))  -- +42

-- 2. 占位修飾符號：使用 0 進行填充
print(string.format("%05d", integer))  -- 00042

-- 3. 對齊標識：向左對齊
print(string.format("%-5d", integer))  -- 42   (後面有三個空格)

-- 4. 寬度數值：指定最小寬度
print(string.format("%10s", str))  -- Hello, Lua!

-- 5. 小數位數 / 字串裁切
-- 小數位數：保留 2 位小數
print(string.format("%.2f", number))  -- 123.46

-- 字串裁切：只顯示前 5 位
print(string.format("%.5s", str))  -- Hello

-- 綜合範例：同時使用多個參數
print(string.format("%+010.2f", number))  -- +0000123.46
print(string.format("%-10.5s", str))  -- Hello     (後面有五個空格)
```

輸出結果：

```
+42
00042
42   
Hello, Lua!
123.46
Hello
+000123.46
Hello
```

再來一個練習範例：

```lua=
local number = 12345
local float_number = 123.456
local str = "Hello, Lua!"

print(string.format("%%c: %c", number))  -- 將數字轉換為 ASCII 字元
print(string.format("%%d: %d", number))  -- 將數字轉換為有號整數
print(string.format("%%i: %i", number))  -- 將數字轉換為有號整數
print(string.format("%%o: %o", number))  -- 將數字轉換為八進位數
print(string.format("%%u: %u", number))  -- 將數字轉換為無號整數
print(string.format("%%x: %x", number))  -- 將數字轉換為十六進位數（小寫）
print(string.format("%%X: %X", number))  -- 將數字轉換為十六進位數（大寫）
print(string.format("%%e: %e", float_number))  -- 將數字轉換為科學記號（小寫 e）
print(string.format("%%E: %E", float_number))  -- 將數字轉換為科學記號（大寫 E）
print(string.format("%%f: %f", float_number))  -- 將數字轉換為浮點數
print(string.format("%%g: %g", float_number))  -- 將數字轉換為 %e 或 %f 中較短的一種格式
print(string.format("%%G: %G", float_number))  -- 將數字轉換為 %E 或 %f 中較短的一種格式
print(string.format("%%q: %q", str))  -- 將字串轉換為可被 Lua 編譯器讀入的格式
print(string.format("%%s: %s", str))  -- 將字串按照給定的參數格式化

-- 複合型格式化
print(string.format("數字 %d 的八進位表示為 %o", number, number))
print(string.format("數字 %d 的十六進位表示為 %x", number, number))
print(string.format("浮點數 %f 的科學記號表示為 %e", float_number, float_number))
```

輸出結果：

```
%c: 9
%d: 12345
%i: 12345
%o: 30071
%u: 12345
%x: 3039
%X: 3039
%e: 1.234560e+02
%E: 1.234560E+02
%f: 123.456000
%g: 123.456
%G: 123.456
%q: "Hello, Lua!"
%s: Hello, Lua!
數字 12345 的八進位表示為 30071
數字 12345 的十六進位表示為 3039
浮點數 123.456000 的科學記號表示為 1.234560e+02
```

### 字串匹配模式
---

> Lua 中的匹配模式直接用常規的字串來描述。它用於模式匹配函數 `string.find`、 `string.gmatch`、`string.gsub`、`string.match`。

以下是一個範例，呈現 dd/mm/yyyy 格式的日期（來自：[Lua 字符串 | 菜鸟教程](https://www.runoob.com/lua/lua-strings.html)）：

```lua=
s = "Deadline is 30/05/1999, firm"
date = "%d%d/%d%d/%d%d%d%d"
print(string.sub(s, string.find(s, date)))    --> 30/05/1999
```

輸出結果：

```
30/05/1999
```

有關於 Lua 字元類別種類，可看以下條列：

* .（點）：與任何字元配對 
* %a：與任何字母配對 
* %c：與任何控制符配對（例如 \n） 
* %d：與任何數字配對 
* %l：與任何小寫字母配對
* %p：與任何標點（punctuation）配對 
* %s：與空白字元配對
* %u：與任何大寫字母配對
* %w：與任何字母 / 數字配對
* %x：與任何十六進制數配對
* %z：與任何代表 0 的字元配對
* %x（此處 x 是非字母非數字字元）：與字元 x 配對。主要用來處理運算式中有功能的字元（^$()%.[]*+-?）的配對問題，例如 %% 與 % 配對。
* [數個字元類別]：與任何 [] 中包含的字元類別配對。例如 [%w_] 與任何字母 / 數字，或底線符號(_)配對。
* [^數個字元類別]：與任何"不"包含在 [] 中的字元類別配對。例如 [^%s] 與任何非空白字元配對。

> 當上述的字元類別以大寫書寫時，表示與非此字元類別的任何字元配對。例如，%S 表示與任何非空白字元配對。例如，'%A'非字母的字元：

```lua=
> print(string.gsub("hello, up-down!", "%A", "."))
hello..up.down.    4
```

註：有一個 > 表示程式碼是在 Shell 底下執行。

接下來，同樣也是做練習啦，不練習不會進步～：

```lua=
local test_string = "Hello, World! 1234\n"

local function test_pattern(pattern, description)
    if string.match(test_string, pattern) then
        print(description .. ": 匹配成功")
    else
        print(description .. ": 匹配失敗")
    end
end

test_pattern(".", "與任何字元配對")
test_pattern("%a", "與任何字母配對")
test_pattern("%c", "與任何控制符配對")
test_pattern("%d", "與任何數字配對")
test_pattern("%l", "與任何小寫字母配對")
test_pattern("%p", "與任何標點配對")
test_pattern("%s", "與空白字元配對")
test_pattern("%u", "與任何大寫字母配對")
test_pattern("%w", "與任何字母 / 數字配對")
test_pattern("%x", "與任何十六進制數配對")
test_pattern("%z", "與任何代表 0 的字元配對")
test_pattern("%%", "與字元 % 配對")
test_pattern("[%w_]", "與任何字母 / 數字或底線符號配對")
test_pattern("[^%s]", "與任何非空白字元配對")

test_pattern("%a+", "與一個或多個字母配對")
test_pattern("%d+", "與一個或多個數字配對")
test_pattern("%s+", "與一個或多個空白字元配對")
test_pattern("%u%l+", "與一個大寫字母後跟隨一個或多個小寫字母配對")

print("測試字串: " .. test_string)
```

輸出結果：

```
與任何字元配對: 匹配成功
與任何字母配對: 匹配成功
與任何控制符配對: 匹配成功
與任何數字配對: 匹配成功
與任何小寫字母配對: 匹配成功
與任何標點配對: 匹配成功
與空白字元配對: 匹配成功
與任何大寫字母配對: 匹配成功
與任何字母 / 數字配對: 匹配成功
與任何十六進制數配對: 匹配成功
與任何代表 0 的字元配對: 匹配失敗
與字元 % 配對: 匹配失敗
與任何字母 / 數字或底線符號配對: 匹配成功
與任何非空白字元配對: 匹配成功
與一個或多個字母配對: 匹配成功
與一個或多個數字配對: 匹配成功
與一個或多個空白字元配對: 匹配成功
與一個大寫字母後跟隨一個或多個小寫字母配對: 匹配成功
測試字串: Hello, World! 1234
```

> 在模式匹配中有一些特殊字元，他們有特殊的意義，Lua中的特殊字元如下：

`( ) . % + - * ? [ ^ $`

> '%' 用作特殊字元的跳脫字元，因此 '%.' 匹配點；'%%' 匹配字元 '%'。跳脫字元 '%' 不僅可以用來跳脫特殊字元，還可以用於所有的非字母的字元。

以下節錄自：[Lua 字符串 | 菜鸟教程](https://www.runoob.com/lua/lua-strings.html)

**模式條目可以是：**

* 單一字元類別符合該類別中任意單一字元； 
* 單一字元類別跟一個 '*'，將匹配 0 或多個該類別的字元。這個條目總是匹配盡可能長的字串；
* 單一字元類別跟一個 '+'，將匹配 1 或更多該類別的字元。這個條目總是匹配盡可能長的字串；
* 單一字元類別跟一個 '-'，將匹配 0 或更多該類別的字元。和 '*' 不同，這個條目總是匹配盡可能短的字串；
* 單一字元類別跟一個 '?'，將匹配 0 或一個該類別的字元。只要有可能，它就會匹配一個；
* %n，這裡的 n 可以從 1 到 9；這個條目匹配一個等於 n 號捕獲物（後面會描述）的子字串。
* %bxy，這裡的 x 和 y 是兩個明確的字元；這個條目匹配以 x 開始 y 結束，並且其中 x 和 y 保持平衡的字串。意思是，如果從左到右讀這個字串，對每次讀到一個 x 就 +1，讀到一個 y 就 -1，最終結束處的那個 y 是第一個記數到 0 的 y。舉個例子，條目 %b() 可以匹配到括號平衡的運算式。
* %f[set]，邊境模式；這個條目會匹配到一個位於 set 內某個字元之前的一個空字串，且這個位置的前一個字元不屬於 set。集合 set 的意思如前面所述。配對出的那個空字串之開始和結束點的計算就看成該處有個字元 '\0' 一樣。

以下是一個範例：

```lua=
-- 測試字串 1
local test_str = "abc123xyz"

-- 單一字元類別匹配
local pattern1 = "[a-z]"  -- 匹配任意小寫字母
print(test_str:match(pattern1))  -- a

-- 單一字元類別跟 '*'
local pattern2 = "[a-z]*"  -- 匹配0或多個小寫字母
print(test_str:match(pattern2))  -- abc

-- 單一字元類別跟 '+'
local pattern3 = "[a-z]+"  -- 匹配1或多個小寫字母
print(test_str:match(pattern3))  -- abc

-- 單一字元類別跟 '-'
local pattern4 = "[a-z]-"  -- 匹配0或多個小寫字母（盡可能短）
print(test_str:match(pattern4))  -- a

-- 單一字元類別跟 '?'
local pattern5 = "[a-z]?"  -- 匹配0或1個小寫字母
print(test_str:match(pattern5))  -- a

-- %n 捕獲物匹配
local pattern6 = "(%d)(%a)%1"  -- 匹配數字後跟字母，再跟相同的數字
local match1, match2 = test_str:match(pattern6)
print(match1, match2)  -- nil nil (因為沒有符合的模式)

-- %bxy 平衡匹配
local balanced_str = "a(b(c)d)e"
local pattern7 = "%b()"  -- 匹配平衡的括號
print(balanced_str:match(pattern7))  -- (b(c)d)

-- %f[set] 邊境模式匹配
local pattern8 = "%f[%d]"  -- 匹配位於數字前的空字串
local start_pos = test_str:find(pattern8)
print(start_pos)  -- 4 (因為 '1' 是第一個數字)

-- 測試字串 2
local test_str2 = "hello123world"

-- 單一字元類別匹配
local pattern9 = "[a-z]"  -- 匹配任意小寫字母
print(test_str2:match(pattern9))  -- h

-- 單一字元類別跟 '*'
local pattern10 = "[a-z]*"  -- 匹配0或多個小寫字母
print(test_str2:match(pattern10))  -- hello

-- 單一字元類別跟 '+'
local pattern11 = "[a-z]+"  -- 匹配1或多個小寫字母
print(test_str2:match(pattern11))  -- hello

-- 單一字元類別跟 '-'
local pattern12 = "[a-z]-"  -- 匹配0或多個小寫字母（盡可能短）
print(test_str2:match(pattern12))  -- h

-- 單一字元類別跟 '?'
local pattern13 = "[a-z]?"  -- 匹配0或1個小寫字母
print(test_str2:match(pattern13))  -- h

-- %n 捕獲物匹配
local pattern14 = "(%d)(%a)%1"  -- 匹配數字後跟字母，再跟相同的數字
local match3, match4 = test_str2:match(pattern14)
print(match3, match4)  -- nil nil (因為沒有符合的模式)

-- %bxy 平衡匹配
local balanced_str2 = "a(b(c)d)e"
local pattern15 = "%b()"  -- 匹配平衡的括號
print(balanced_str2:match(pattern15))  -- (b(c)d)

-- %f[set] 邊境模式匹配
local pattern16 = "%f[%d]"  -- 匹配位於數字前的空字串
local start_pos2 = test_str2:find(pattern16)
print(start_pos2)  -- 6 (因為 '1' 是第一個數字)
```

:::warning
在 Lua 中，冒號 (:) 是一種語法糖（Syntactic sugar，暫且別管這啥）。

str:match("%d") 等價於 string.match(str, "%d")。冒號語法自動將 str 作為第一個參數傳遞給 string.match 函數。（總之有語法糖就能夠使程式碼變得更加簡潔有力）
:::

:::info
[a-z] 是一個字元類別（character class），表示匹配任意一個從 a 到 z 的小寫字母。這種寫法稱為範圍（range），用來指定一組連續的字元。

也有衍生以下這些寫法：

* [a-z0-9]：匹配任意一個小寫字母或數字。
* [aeiou]：匹配任意一個母音字母（a、e、i、o、u）。
:::

:::info
在 %n 捕獲物匹配的地方，小括號 () 用於捕獲匹配到的內容。這裡的 (%d) 表示捕獲一個數字，並將其儲存為第一個捕獲組。

**(%a)**：這裡的 (%a) 表示捕獲一個字母，並將其儲存為第二個捕獲組。

**%1**：反向引用（backreference），表示匹配第一個捕獲組的內容。在這個模式中，%1 會匹配之前捕獲的數字。

所以 `(%d)(%a)%1` 是一個包含捕獲組（capture groups）和反向引用（backreference）的模式。

`(%d)(%a)%1` 可作以下三點總結：

* 匹配一個數字，並將其捕獲為第一個捕獲組。
* 匹配一個字母，並將其捕獲為第二個捕獲組。
* 最後匹配與第一個捕獲組相同的數字。

而至於都沒匹配到，看一下字串："hello123world"

第一個匹配到數字 1，但第二個不是字母。所以只可能是 3、w，但是 3、w、o 匹配第三個 o 並不是與 3 同樣的數字（因為反向引用 %1 必須與第一個捕獲組相同數字），所以 nil。
:::

:::info
%bxy 平衡（平衡：指的是成對的符號，包括括號）匹配的部分：

`local pattern15 = "%b()"`：x、y 都是括號，所以 `%b()` 用於匹配平衡的小括號對。

在字串 `"a(b(c)d)e"` 中，第一個平衡的括號對是 `"(b(c)d)"`，它從第二個字元開始，到第八個字元結束。需要注意的是，這個匹配包括了括號內的所有內容，包括巢狀的括號對 `"(c)"`。
:::

:::info
%f[set] 邊境模式匹配（frontier pattern）的部分：

%f[set] 會匹配一個位置，這個位置的前一個字元不在指定的集合 set 中，而位置本身的字元在集合中。集合 set 是由一組字元組成的，可以用一系列的字元或者字元類別（如 %d 表示數字，%s 表示空白字符等）來指定。

```lua=
-- 測試字串 2
local test_str2 = "hello123world"

-- %f[set] 邊境模式匹配
local pattern16 = "%f[%d]"  -- 匹配位於數字前的空字串
local start_pos2 = test_str2:find(pattern16)
print(start_pos2)  -- 6 (因為 '1' 是第一個數字)
```

在這個範例當中，%f[%d] 用來匹配位於數字前的空字串位置。表示它會尋找一個位置，這個位置的前一個字元不是數字，而位置本身的字元是數字。對於識別字串中數字的開始位置非常有用。

如果 test_str2 是 "Hello 123"，則 %f[%d] 會匹配到 " " 和 "1" 之間的位置，因為 " " 不是數字，而 "1" 是數字。由於 Lua 的字串索引從 1 開始，"1" 在字符串中的位置是 7，所以 start_pos2 的值會是 7，表示 %f[%d] 匹配到的位置。

講這麼多複雜的還有舉例，總之，%f[set] 的 set 主要是找第一個出現的字元。
:::

總結
---

:::info
**字串表示方式**：

* 單引號：'String'
* 雙引號："String"
* 多行字串：[[ 多行字串 ]]
:::

:::info
**取得字串長度**：

* string.len(字串)：取得字串長度
* `#`字串：取得字串長度
* utf8.len(字串)：取得包含中文字的字串長度
:::

:::info
**跳脫字元**：

* 使用反斜線 \ 來表示特殊字元，如 \n 表示換行，\" 表示雙引號等。
:::

:::info
**字串方法（Method）**：

* string.upper(字串)：轉為大寫
* string.lower(字串)：轉為小寫
* string.gsub(字串, 查找字串, 替換字串, [次數])：替換字串
* string.find(字串, 子字串, [起始位置], [簡單模式])：查找子字串
* string.reverse(字串)：反轉字串
* string.format(格式, ...)：格式化字串
* string.char(數字)：將數字轉為字元
* string.byte(字串, [位置])：將字元轉為數字
* string.len(字串)：計算字串長度
* string.rep(字串, 次數)：重複字串
* ..：連接兩個字串
* string.gmatch(字串, 模式)：返回迭代器函數
* string.match(字串, 模式, [起始位置])：匹配字串
:::

:::info
**字串格式化**：

* 使用 string.format() 進行格式化，支持多種格式化規範符號，如 %d、%f、%s 等。
:::

:::info
**字串匹配模式**：

* 使用 string.find、string.gmatch、string.gsub、string.match 進行模式匹配。
* 支援多種字元類別，如 %a（字母）、%d（數字）、%s（空白字元）等。
* 支援捕獲組和反向引用，如 (%d)(%a)%1。

**常用模式條目**：

* [a-z]：匹配任意小寫字母
* [a-z]*：匹配0或多個小寫字母
* [a-z]+：匹配1或多個小寫字母
* [a-z]-：匹配0或多個小寫字母（盡可能短）
* [a-z]?：匹配0或1個小寫字母
* %b()：匹配平衡的括號
* %f[set]：匹配位於 set 內某個字元之前的空字串
:::

參考資料
---

[lua-users wiki: Frontier Pattern](http://lua-users.org/wiki/FrontierPattern)

[Lua pattern matching - Series of same character - Stack Overflow](https://stackoverflow.com/questions/48541679/lua-pattern-matching-series-of-same-character)

[【30天Lua重拾筆記08】基礎1: 類型 - 字串 | 又LAG隨性筆記](https://www.lagagain.com/post/30%E5%A4%A9lua%E9%87%8D%E6%8B%BE%E7%AD%86%E8%A8%9808%E5%9F%BA%E7%A4%8E1-%E9%A1%9E%E5%9E%8B-%E5%AD%97%E4%B8%B2/)

[簡單記錄一下 lua 中 string.gmatch 的用法 | Medium](https://medium.com/learn-or-die/string-gmatch-in-lua-3f4162f0e850)

[Lua 字符串 | 菜鸟教程](https://www.runoob.com/lua/lua-strings.html)