字符串比较

要在 MQL5 中比较字符串,可使用标准 比较运算符,尤其是 '=='、'!='、'>'、'<'。所有这些运算符以逐字符、区分大小写的方式执行比较。

每个字符都有一个 ushort 类型整数的 Unicode 码。相应地,首先,比较两个字符串的第一个字符的代码,然后比较第二个字符的代码,以此类推,直至出现第一个不匹配项或达到其中一个字符串的末尾。

例如,"ABC" 字符串小于 "abc",因为在字符表中大写字母的代码低于相应小写字母的代码(对于第一个字符,我们已经得到 "A" < "a")。如果字符串在开头具有匹配字符,但其中一个长于另一个,则较长的字符串被视为更大 ("ABCD" > "ABC")。

这些字符串关系形成词典序。当字符串 "A" 小于字符串 "B" ("A" < "B"),"A" 被视为在 "B" 之前。

要熟悉字符代码,可使用标准 Windows 应用程序“字符映射表”。在该字符映射表中,字符按增加代码的顺序排列。除了包括多种国家语言的常规 Unicode 表,还有代码页:含单字节字符代码的 ANSI 标准表 - 对于每种语言或语言组都各不相同。我们将在以下章节详细探讨该问题: 处理符号和代码页

字符映射表的初始部分(代码 0 到 127)对于所有语言均相同。该部分显示在下表中。

ASCII 字符代码表

ASCII 字符代码表

要获取字符代码,取左边的十六进制数字(该字符所在行的行号),然后与顶部的数字(该字符所在列的列号)相加:结果是一个十二进制数字。例如,对于 '!',左边为 2,顶上为 1,意味着字符代码为 0x21 或 33(十进制)。

至 32 的代码为控制代码。其中,尤其你可以找到制表符(代码 0x9)、换行(代码 0xA)以及回车(代码 0xD)。

Windows 文本文件中使用彼此跟随的一对字符 0xD 0xA,用于中断至新行。我们在 字符类型 章节熟悉了相应的 MQL5 字面量:0xA 可表示为 '\n',而 0xD 表示为 '\r'。制表符 0x9 也有自己的表示方法:'\t'。

MQL5 API 提供了 StringCompare 函数,可用于在比较字符串时禁用区分大小写。

int StringCompare(const string &string1, const string &string2, const bool case_sensitive = true)

该函数比较两个字符串并返回三个值之一:如果第一个字符串“大于”第二个,则返回 +1;如果字符串“相等”,则返回 0;如果第一个字符串“小于”第二个,则返回 -1。“大于”、“小于”和“等于”的概念取决于 case_sensitive 参数。

case_sensitive 参数等于 true(默认),比较区分大小写,大写字母视为大于对应小写字母。这与根据字符代码的标准词典序相反。

若区分大小写,StringCompare 函数使用不同于词典序的大小写字母序。例如,我们知道关系 "A" < "a" 为 true,其中运算符 '<' 以字符代码为依据。因此,在假设的字典(数组)中,首字母大写的单词应出现在具有相同小写字母的单词之前。但是,当使用 StringCompare("A", "a") 函数比较 "A" 和 "a" 时,我们得到 +1,表示 "A" 大于 "a"。因此,在排序词典中,以小写字母开始的单词将在先,而以大写字母开始的单词在后。

换言之,该函数按字母顺序对字符串排序。此外,在区分大小写模式下,还有一条额外规则:如果字符串仅存在大小写的差别,包含大写字母的字符串会排在对应小写字母字符串的后面(在单词中相同位置的情况下)。

如果 case_sensitive 参数等于 false,则字母不区分大小写,因此字符串 "A" 和 "a" 视为相等,函数返回 0。

可以使用 StringCompare.mq5 脚本检查由 StringCompare 函数和运算符比较的不同比较结果。

void OnStart()
{
   PRT(StringCompare("A""a"));        // 1, which means "A" > "a" (!)
   PRT(StringCompare("A""a"false)); // 0, which means "A" == "a"
   PRT("A" > "a");                      // false,   "A" < "a"
   
   PRT(StringCompare("x""y"));        // -1, which means "x" < "y"
   PRT("x" > "y");                      // false,    "x" < "y"
   ...
}

函数模板 章节中,我们创建了模板化快速排序算法。我们将其转换为模板类,并将其用于若干排序选择:在区分和不区分大小写的情况下使用比较运算符以及使用 StringCompare 函数。我们将新的 QuickSortT 类放入 QuickSortT.mqh 头文件,并将其连接到 StringCompare.mq5 测试脚本。

排序 API 几乎没有变化。

template<typename T>
class QuickSortT
{
public:
   void Swap(T &array[], const int iconst int j)
   {
      ...
   }
   
   virtual int Compare(T &aT &b)
   {
      return a > b ? +1 : (a < b ? -1 : 0);
   }
   
   void QuickSort(T &array[], const int start = 0int end = INT_MAX)
   {
      ...
         for(int i = starti <= endi++)
         {
            //if(!(array[i] > array[end]))
            if(Compare(array[i], array[end]) <= 0)
            {
               Swap(arrayipivot++);
            }
         }
      ...
   }
};

主要的不同之处在于我们添加了 Compare 虚方法,其默认包含使用 '>' 和 '<' 运算符的比较,并以与 StringCompare 相同的方式返回 +1、-1 或 0。Compare 方法现在用于 QuickSort 方法中,代替简单比较,必须在子类中被重写,以使用 StringCompare 函数或任何其它比较方法。

尤其在 StringCompare.mq5 文件中,我们实现了派生自 QuickSortT<string> 的以下“比较器”类:

class SortingStringCompare : public QuickSortT<string>
{
   const bool caseEnabled;
public:
   SortingStringCompare(const bool sensitivity = true) :
      caseEnabled(sensitivity) { }
      
   virtual int Compare(string &astring &boverride
   {
      return StringCompare(abcaseEnabled);
   }
};

构造函数采用 1 个参数,用于指定字符串比较符号,同时考虑 (true) 或忽略 (false) 寄存器。字符串比较本身在重新定义的 Compare 虚方法中完成,该方法调用具有给定自变量和设置的 StringCompare 函数。

为测试排序,我们需要一系列组合了大小写字母的字符串。我们可以自行生成这些字符串:只需开发一个类,对预定义集合(字母表)中的字符按给定集合长度(字符串)执行排列组合(可重复)。例如,可以限定使用小写字母表 "abcABC",即大小写的前三个英语字母,然后据此生成所有可能的 2 个字符的字符串。

PermutationGenerator 类在 PermutationGenerator.mqh 文件中提供,稍后再单独探讨。在这里我们仅了解其公共接口。

class PermutationGenerator
{
public:
   struct Result
   {
      int indices[]; // indexes of elements in each position of the set, i.e.
   };                // for example, the numbers of the letters of the "alphabet" in each position of the string 
   PermutationGenerator(const int lengthconst int elements);
   SimpleArray<Result> *run();
};

创建生成器对象时,必须指定生成的集合的长度 length(在本例中,这将是字符串的长度,比如 2)以及将组成集合的不同元素的数量(在本例中,这是唯一字母的数量,也就是 6)。利用这些输入数据,应可获得 6 * 6 = 36 的行变体。

该过程本身由 run 方法执行。模板类用于返回结果为 SimpleArray 的数组,这已在 方法模板 章节讨论过。在此情况下,其由 result 结构体类型进行参数化。

GenerateStringList 辅助函数负责调用生成器,并根据从生成器收到的排列数组(以所有可能字符串各位置的字母索引形式表示)实际创建字符串。

void GenerateStringList(const string symbolsconst int lenstring &result[])
{
   const int n = StringLen(symbols); // alphabet length, unique characters
   PermutationGenerator g(lenn);
   SimpleArray<PermutationGenerator::Result> *r = g.run();
   ArrayResize(resultr.size());
   // loop through all received character permutations
   for(int i = 0i < r.size(); ++i)
   {
      string element;
      // loop through all characters in the string
      for(int j = 0j < len; ++j)
      {
         // add a letter from the alphabet (by its index) to the string
         element += ShortToString(symbols[r[i].indices[j]]);
      }
      result[i] = element;
   }
}

我们使用几个还不熟悉的函数 (ArrayResize, ShortToString),但我们将很快就会了解。目前,我们只需知道,ShortToString 函数返回由该单个字符基于 ushort 类型字符代码组成的字符串。通过 += 运算符,我们将这些单字符字符串拼接成最终的字符串。别忘了,为字符串定义了运算符 [],因此,symbols[k] 将返回 symbols 字符串的第 k 个字符。当然,k 也可反过来成为整数表达式,并且这里的 r[i].indices[j] 指的是 r 数组的第 i 个元素,从该数组读取了字符串的第 j 位置对应的“字母”字符索引。

接收到的每个字符串存储在 result 数组参数中。

我们在 OnStart 函数中应用这一信息。

void OnStart()
{
   ...
   string messages[];
   GenerateStringList("abcABC"2messages);
   Print("Original data["ArraySize(messages), "]:");
   ArrayPrint(messages);
   
   Print("Default case-sensitive sorting:");
   QuickSortT<stringsorting;
   sorting.QuickSort(messages);
   ArrayPrint(messages);
   
   Print("StringCompare case-insensitive sorting:");
   SortingStringCompare caseOff(false);
   caseOff.QuickSort(messages);
   ArrayPrint(messages);
   
   Print("StringCompare case-sensitive sorting:");
   SortingStringCompare caseOn(true);
   caseOn.QuickSort(messages);
   ArrayPrint(messages);
}

脚本首先将所有字符串选择纳入消息数组,然后以 3 种模式排序:使用内置比较运算符、使用 StringCompare 函数且不区分大小写、使用该函数且区分大小写。

我们将得到以下日志输出:

Original data[36]:
[ 0] "aa" "ab" "ac" "aA" "aB" "aC" "ba" "bb" "bc" "bA" "bB" "bC" "ca" "cb" "cc" "cA" "cB" "cC"
[18] "Aa" "Ab" "Ac" "AA" "AB" "AC" "Ba" "Bb" "Bc" "BA" "BB" "BC" "Ca" "Cb" "Cc" "CA" "CB" "CC"
Default case-sensitive sorting:
[ 0] "AA" "AB" "AC" "Aa" "Ab" "Ac" "BA" "BB" "BC" "Ba" "Bb" "Bc" "CA" "CB" "CC" "Ca" "Cb" "Cc"
[18] "aA" "aB" "aC" "aa" "ab" "ac" "bA" "bB" "bC" "ba" "bb" "bc" "cA" "cB" "cC" "ca" "cb" "cc"
StringCompare case-insensitive sorting:
[ 0] "AA" "Aa" "aA" "aa" "AB" "aB" "Ab" "ab" "aC" "AC" "Ac" "ac" "BA" "Ba" "bA" "ba" "BB" "bB"
[18] "Bb" "bb" "bC" "BC" "Bc" "bc" "CA" "Ca" "cA" "ca" "CB" "cB" "Cb" "cb" "cC" "CC" "Cc" "cc"
StringCompare case-sensitive sorting:
[ 0] "aa" "aA" "Aa" "AA" "ab" "aB" "Ab" "AB" "ac" "aC" "Ac" "AC" "ba" "bA" "Ba" "BA" "bb" "bB"
[18] "Bb" "BB" "bc" "bC" "Bc" "BC" "ca" "cA" "Ca" "CA" "cb" "cB" "Cb" "CB" "cc" "cC" "Cc" "CC"

输出显示这三种模式的差异。