Tìm Kiếm, Thay Thế và Trích Xuất Các Đoạn Chuỗi
Có lẽ các thao tác phổ biến nhất khi làm việc với chuỗi là tìm kiếm và thay thế các đoạn, cũng như trích xuất chúng. Trong phần này, chúng ta sẽ nghiên cứu các hàm API MQL5 giúp giải quyết những vấn đề này. Các ví dụ về cách sử dụng chúng được tổng hợp trong tệp StringFindReplace.mq5
.
int StringFind(string value, string wanted, int start = 0)
Hàm này tìm kiếm chuỗi con wanted
trong chuỗi value
, bắt đầu từ vị trí start
. Nếu chuỗi con được tìm thấy, hàm sẽ trả về vị trí bắt đầu của nó, với các ký tự trong chuỗi được đánh số bắt đầu từ 0. Nếu không, hàm sẽ trả về -1. Cả hai tham số đều được truyền theo giá trị, cho phép xử lý không chỉ các biến mà còn các kết quả trung gian của phép tính (biểu thức, lời gọi hàm).
Việc tìm kiếm được thực hiện dựa trên sự khớp chính xác của các ký tự, tức là có phân biệt chữ hoa chữ thường. Nếu bạn muốn tìm kiếm không phân biệt chữ hoa chữ thường, trước tiên bạn phải chuyển đổi chuỗi nguồn về một kiểu chữ duy nhất bằng cách sử dụng StringToLower
hoặc StringToUpper
.
Hãy thử đếm số lần xuất hiện của chuỗi con mong muốn trong văn bản bằng StringFind
. Để làm điều này, chúng ta sẽ viết một hàm trợ giúp CountSubstring
sẽ gọi StringFind
trong một vòng lặp, dần dần dịch chuyển vị trí bắt đầu tìm kiếm trong tham số cuối cùng start
. Vòng lặp tiếp tục miễn là vẫn tìm thấy các lần xuất hiện mới của chuỗi con.
int CountSubstring(const string value, const string wanted)
{
// lùi lại vì tăng dần ở đầu vòng lặp
int cursor = -1;
int count = -1;
do
{
++count;
++cursor; // tìm kiếm tiếp tục từ vị trí tiếp theo
// lấy vị trí của chuỗi con tiếp theo, hoặc -1 nếu không có kết quả khớp
cursor = StringFind(value, wanted, cursor);
}
while(cursor > -1);
return count;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Điều quan trọng cần lưu ý là cách triển khai được trình bày tìm kiếm các chuỗi con có thể chồng lấp nhau. Điều này là do vị trí hiện tại được thay đổi tăng thêm 1 (++cursor
) trước khi bắt đầu tìm kiếm lần xuất hiện tiếp theo. Kết quả là, khi tìm kiếm, chẳng hạn, chuỗi con "AAA" trong chuỗi "AAAAA", sẽ tìm thấy 3 kết quả khớp. Yêu cầu kỹ thuật cho việc tìm kiếm có thể khác với hành vi này. Đặc biệt, có một cách thực hành là tiếp tục tìm kiếm sau vị trí nơi đoạn được tìm thấy trước đó kết thúc. Trong trường hợp này, cần sửa đổi thuật toán để con trỏ di chuyển với bước bằng StringLen(wanted)
.
Hãy gọi CountSubstring
cho các đối số khác nhau trong hàm OnStart
.
void OnStart()
{
string abracadabra = "ABRACADABRA";
PRT(CountSubstring(abracadabra, "A")); // 5
PRT(CountSubstring(abracadabra, "D")); // 1
PRT(CountSubstring(abracadabra, "E")); // 0
PRT(CountSubstring(abracadabra, "ABRA")); // 2
...
}
2
3
4
5
6
7
8
9
int StringReplace(string &variable, const string wanted, const string replacement)
Hàm này thay thế tất cả các chuỗi con wanted
được tìm thấy bằng chuỗi con replacement
trong chuỗi variable
.
Hàm trả về số lượng thay thế đã thực hiện hoặc -1 trong trường hợp có lỗi. Mã lỗi có thể được lấy bằng cách gọi hàm GetLastError
. Cụ thể, đó có thể là lỗi hết bộ nhớ hoặc sử dụng chuỗi chưa được khởi tạo (NULL) làm đối số. Các tham số variable
và wanted
phải là chuỗi có độ dài khác không.
Khi một chuỗi rỗng "" được đưa vào làm đối số replacement
, tất cả các lần xuất hiện của wanted
sẽ đơn giản bị cắt khỏi chuỗi ban đầu.
Nếu không có thay thế nào được thực hiện, kết quả của hàm là 0.
Hãy sử dụng ví dụ của StringFindReplace.mq5
để kiểm tra StringReplace
trong thực tế.
string abracadabra = "ABRACADABRA";
...
PRT(StringReplace(abracadabra, "ABRA", "-ABRA-")); // 2
PRT(StringReplace(abracadabra, "CAD", "-")); // 1
PRT(StringReplace(abracadabra, "", "XYZ")); // -1, lỗi
PRT(GetLastError()); // 5040, ERR_WRONG_STRING_PARAMETER
PRT(abracadabra); // '-ABRA---ABRA-'
...
2
3
4
5
6
7
8
Tiếp theo, sử dụng hàm StringReplace
, hãy thử thực hiện một trong những nhiệm vụ thường gặp trong việc xử lý các văn bản bất kỳ. Chúng ta sẽ cố gắng đảm bảo rằng một ký tự phân cách nhất định luôn được sử dụng dưới dạng ký tự đơn, tức là các chuỗi gồm nhiều ký tự như vậy phải được thay thế bằng một ký tự. Thông thường, điều này liên quan đến khoảng trắng giữa các từ, nhưng có thể có các ký tự phân cách khác trong dữ liệu kỹ thuật. Hãy kiểm tra chương trình của chúng ta với ký tự phân cách '-'.
Chúng ta triển khai thuật toán dưới dạng một hàm riêng biệt NormalizeSeparatorsByReplace
:
int NormalizeSeparatorsByReplace(string &value, const ushort separator = ' ')
{
const string single = ShortToString(separator);
const string twin = single + single;
int count = 0;
int replaced = 0;
do
{
replaced = StringReplace(value, twin, single);
if(replaced > 0) count += replaced;
}
while(replaced > 0);
return count;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
Chương trình cố gắng thay thế chuỗi gồm hai ký tự phân cách bằng một ký tự trong vòng lặp do-while
, và vòng lặp tiếp tục miễn là hàm StringReplace
trả về giá trị lớn hơn 0 (tức là vẫn còn thứ gì đó để thay thế). Hàm trả về tổng số lần thay thế đã thực hiện.
Trong hàm OnStart
, hãy "dọn dẹp" dòng chữ của chúng ta khỏi nhiều ký tự '-'.
...
string copy1 = "-" + abracadabra + "-";
string copy2 = copy1;
PRT(copy1); // '--ABRA---ABRA--'
PRT(NormalizeSeparatorsByReplace(copy1, '-')); // 4
PRT(copy1); // '-ABRA-ABRA-'
PRT(StringReplace(copy1, "-", "")); // 1
PRT(copy1); // 'ABRAABRA'
...
2
3
4
5
6
7
8
9
int StringSplit(const string value, const ushort separator, string &result[])
Hàm này chia chuỗi value
được truyền vào thành các chuỗi con dựa trên ký tự phân cách đã cho và đặt chúng vào mảng result
. Hàm trả về số lượng chuỗi con nhận được hoặc -1 trong trường hợp có lỗi.
Nếu không có ký tự phân cách trong chuỗi, mảng sẽ có một phần tử bằng toàn bộ chuỗi.
Nếu chuỗi nguồn rỗng hoặc NULL, hàm sẽ trả về 0.
Để thể hiện hoạt động của hàm này, hãy giải quyết vấn đề trước đó theo cách mới bằng StringSplit
. Để làm điều này, hãy viết hàm NormalizeSeparatorsBySplit
.
int NormalizeSeparatorsBySplit(string &value, const ushort separator = ' ')
{
const string single = ShortToString(separator);
string elements[];
const int n = StringSplit(value, separator, elements);
ArrayPrint(elements); // debug
StringFill(value, 0); // kết quả sẽ thay thế chuỗi ban đầu
for(int i = 0; i < n; ++i)
{
// chuỗi rỗng nghĩa là ký tự phân cách, và chúng ta chỉ cần thêm chúng
// nếu dòng trước đó không rỗng (tức là cũng không phải ký tự phân cách)
if(elements[i] == "" && (i == 0 || elements[i - 1] != ""))
{
value += single;
}
else // tất cả các dòng khác được nối lại với nhau "nguyên trạng"
{
value += elements[i];
}
}
return n;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Khi các ký tự phân cách xuất hiện liên tiếp trong văn bản nguồn, phần tử tương ứng trong mảng đầu ra của StringSplit
sẽ là một chuỗi rỗng "". Ngoài ra, một chuỗi rỗng sẽ xuất hiện ở đầu mảng nếu văn bản bắt đầu bằng ký tự phân cách, và ở cuối mảng nếu văn bản kết thúc bằng ký tự phân cách.
Để có được văn bản "đã dọn dẹp", bạn cần thêm tất cả các chuỗi không rỗng từ mảng, "dán" chúng bằng các ký tự phân cách đơn. Hơn nữa, chỉ những phần tử rỗng mà phần tử trước đó của mảng cũng không rỗng mới nên được chuyển thành ký tự phân cách.
Tất nhiên, đây chỉ là một trong những cách triển khai có thể có cho chức năng này. Hãy kiểm tra nó trong hàm OnStart
.
...
string copy2 = "-" + abracadabra + "-"; // '--ABRA---ABRA--'
PRT(NormalizeSeparatorsBySplit(copy2, '-')); // 8
// đầu ra debug của mảng chia tách (bên trong hàm):
// "" "" "ABRA" "" "" "ABRA" "" ""
PRT(copy2); // '-ABRA-ABRA-'
2
3
4
5
6
string StringSubstr(string value, int start, int length = -1)
Hàm này trích xuất từ văn bản value
được truyền vào một chuỗi con bắt đầu từ vị trí được chỉ định start
, với độ dài length
. Vị trí bắt đầu có thể từ 0 đến độ dài chuỗi trừ 1. Nếu độ dài length
là -1 hoặc lớn hơn số ký tự từ start
đến cuối chuỗi, phần còn lại của chuỗi sẽ được trích xuất toàn bộ.
Hàm trả về một chuỗi con hoặc một chuỗi rỗng nếu các tham số không chính xác.
Hãy xem cách nó hoạt động.
PRT(StringSubstr("ABRACADABRA", 4, 3)); // 'CAD'
PRT(StringSubstr("ABRACADABRA", 4, 100)); // 'CADABRA'
PRT(StringSubstr("ABRACADABRA", 4)); // 'CADABRA'
PRT(StringSubstr("ABRACADABRA", 100)); // ''
2
3
4