diff --git a/src/OneScript.StandardLibrary/StringOperations.cs b/src/OneScript.StandardLibrary/StringOperations.cs index e8ad41f5e..c68d50216 100644 --- a/src/OneScript.StandardLibrary/StringOperations.cs +++ b/src/OneScript.StandardLibrary/StringOperations.cs @@ -5,30 +5,35 @@ This Source Code Form is subject to the terms of the at http://mozilla.org/MPL/2.0/. ----------------------------------------------------------*/ -using System; -using System.Linq; using OneScript.Commons; using OneScript.Contexts; using OneScript.Contexts.Enums; using OneScript.Exceptions; using OneScript.Execution; +using OneScript.Localization; using OneScript.StandardLibrary.Collections; using ScriptEngine.Machine; using ScriptEngine.Machine.Contexts; +using System; +using System.Linq; namespace OneScript.StandardLibrary { [GlobalContext(Category = "Операции со строками")] public class StringOperations : GlobalContextBase { + private static readonly System.Text.RegularExpressions.Regex _templateRe + = new System.Text.RegularExpressions.Regex(@"(%%)|%(\d+)|%\((\d+)\)|%", + System.Text.RegularExpressions.RegexOptions.Compiled); + /// /// Получает строку на языке, заданном во втором параметре (коды языков в соответствии с ISO 639-1) /// или на текущем языке системы. /// /// Строка на нескольких языках /// Код языка (если не указан, возвращает вариант для текущего языка системы, - /// если вариант не найден, то возвращает вариант для английского языка, если не задан вариант для английского языка, - /// то возвращает первый вариант из списка) + /// если вариант не найден, то возвращает вариант для английского языка, + /// если не задан вариант для английского языка, то возвращает первый вариант из списка) [ContextMethod("НСтр", "NStr")] public string NStr(string src, string lang = null) { @@ -39,19 +44,20 @@ public string NStr(string src, string lang = null) /// Определяет, что строка начинается с указанной подстроки. /// /// Строка, начало которой проверяется на совпадение с подстрокой поиска. - /// Строка, содержащая предполагаемое начало строки. В случае если переданное значение является пустой строкой генерируется исключительная ситуация. + /// Строка, содержащая предполагаемое начало строки. + /// В случае, если переданное значение является пустой строкой, генерируется исключительная ситуация. [ContextMethod("СтрНачинаетсяС", "StrStartsWith")] public bool StrStartsWith(string inputString, string searchString) { bool result = false; - if(!string.IsNullOrEmpty(inputString)) + if (!string.IsNullOrEmpty(inputString)) { if (!string.IsNullOrEmpty(searchString)) { result = inputString.StartsWith(searchString); } - else throw new RuntimeException("Ошибка при вызове метода контекста (СтрНачинаетсяС): Недопустимое значение параметра (параметр номер '2')"); + else throw StringOpException.StrStartsWith(); } return result; @@ -61,19 +67,20 @@ public bool StrStartsWith(string inputString, string searchString) /// Определяет, заканчивается ли строка указанной подстрокой. /// /// Строка, окончание которой проверяется на совпадение с подстрокой поиска. - /// Строка, содержащая предполагаемое окончание строки. В случае если переданное значение является пустой строкой генерируется исключительная ситуация. + /// Строка, содержащая предполагаемое окончание строки. + /// В случае, если переданное значение является пустой строкой, генерируется исключительная ситуация. [ContextMethod("СтрЗаканчиваетсяНа", "StrEndsWith")] public bool StrEndsWith(string inputString, string searchString) { bool result = false; - if(!string.IsNullOrEmpty(inputString)) + if (!string.IsNullOrEmpty(inputString)) { if (!string.IsNullOrEmpty(searchString)) { result = inputString.EndsWith(searchString); } - else throw new RuntimeException("Ошибка при вызове метода контекста (СтрЗаканчиваетсяНа): Недопустимое значение параметра (параметр номер '2')"); + else throw StringOpException.StrEndsWith(); } return result; @@ -84,22 +91,25 @@ public bool StrEndsWith(string inputString, string searchString) /// /// Разделяемая строка. /// Строка символов, каждый из которых является индивидуальным разделителем. - /// Указывает необходимость включать в результат пустые строки, которые могут образоваться в результате разделения исходной строки. Значение по умолчанию: Истина. + /// Указывает необходимость включать в результат пустые строки, + /// которые могут образоваться в результате разделения исходной строки. Значение по умолчанию: Истина. [ContextMethod("СтрРазделить", "StrSplit")] public ArrayImpl StrSplit(string inputString, string stringDelimiter, bool? includeEmpty = true) { string[] arrParsed; if (includeEmpty == null) - includeEmpty = true; - - if(!string.IsNullOrEmpty(inputString)) + includeEmpty = true; + + if (!string.IsNullOrEmpty(inputString)) { - arrParsed = inputString.Split(stringDelimiter?.ToCharArray(), (bool) includeEmpty ? StringSplitOptions.None : StringSplitOptions.RemoveEmptyEntries); + arrParsed = inputString.Split(stringDelimiter?.ToCharArray(), + (bool)includeEmpty ? StringSplitOptions.None : StringSplitOptions.RemoveEmptyEntries); } else { - arrParsed = (bool) includeEmpty ? new string[] { string.Empty } : new string[0]; + arrParsed = (bool)includeEmpty ? new string[] { string.Empty } : Array.Empty(); } + return new ArrayImpl(arrParsed.Select(x => ValueFactory.Create(x))); } @@ -111,8 +121,8 @@ public ArrayImpl StrSplit(string inputString, string stringDelimiter, bool? incl [ContextMethod("СтрСоединить", "StrConcat")] public string StrConcat(IBslProcess process, ArrayImpl input, string delimiter = null) { - var strings = input.Select(x => x.AsString(process)); - + var strings = input.Select(x => x.AsString(process)); + return String.Join(delimiter, strings); } @@ -138,24 +148,28 @@ public int StrCompare(string first, string second) /// Указывает номер вхождения искомой подстроки в исходной строке /// Позицию искомой строки в исходной строке. Возвращает 0, если подстрока не найдена. [ContextMethod("СтрНайти", "StrFind")] - public int StrFind(string haystack, string needle, SearchDirection direction = SearchDirection.FromBegin, int startPos = 0, int occurance = 0) + public int StrFind(string haystack, string needle, SearchDirection direction = SearchDirection.FromBegin, int startPos = 0, int occurance = 1) { - int len = haystack.Length; - if (len == 0 || needle.Length == 0) + if (needle == null || needle.Length == 0) + return 1; + + int len = haystack?.Length ?? 0; + if (len == 0) return 0; bool fromBegin = direction == SearchDirection.FromBegin; - if(startPos == 0) + if (startPos == 0) { startPos = fromBegin ? 1 : len; } + else if (startPos < 1 || startPos > len) + throw RuntimeException.InvalidNthArgumentValue(4); - if (startPos < 1 || startPos > len) - throw RuntimeException.InvalidArgumentValue(); - - if (occurance == 0) - occurance = 1; + if (occurance < 0) + return 0; + else if (occurance == 0) + throw RuntimeException.InvalidNthArgumentValue(5); int startIndex = startPos - 1; int foundTimes = 0; @@ -174,7 +188,6 @@ public int StrFind(string haystack, string needle, SearchDirection direction = S if (startIndex >= len) break; } - } else { @@ -189,13 +202,9 @@ public int StrFind(string haystack, string needle, SearchDirection direction = S if (startIndex < 0) break; } - } - if (foundTimes == occurance) - return index + 1; - else - return 0; + return foundTimes == occurance ? index + 1 : 0; } /// @@ -216,9 +225,8 @@ public string StrTemplate(string template, .SkipWhile(x => x == null) .Count(); - var re = new System.Text.RegularExpressions.Regex(@"(%%)|%(\d+)|%\((\d+)\)|%"); int maxNumber = 0; - var result = re.Replace(srcFormat, (m) => + var result = _templateRe.Replace(srcFormat, (m) => { if (m.Groups[1].Success) return "%"; @@ -228,7 +236,7 @@ public string StrTemplate(string template, var number = int.Parse(m.Groups[2].Success ? m.Groups[2].Value : m.Groups[3].Value); if (number < 1 || number > 10) - throw new RuntimeException($"Ошибка синтаксиса шаблона в позиции {m.Index+2}: недопустимый номер подстановки"); + throw StringOpException.TemplateSubst(m.Index + 2, number); //FIXME: отключено, т.к. платформа игнорирует ошибку с недостаточным числом параметров //if (number > passedArgsCount) @@ -240,7 +248,7 @@ public string StrTemplate(string template, return arguments[10-number] ?? ""; } - throw new RuntimeException("Ошибка синтаксиса шаблона в позиции " + (m.Index + 2)); + throw StringOpException.TemplateSyntax(m.Index + 2); }); if (passedArgsCount > maxNumber) @@ -253,9 +261,9 @@ public static IAttachableContext CreateInstance() { return new StringOperations(); } - } + [EnumerationType("НаправлениеПоиска", "SearchDirection")] public enum SearchDirection { @@ -265,5 +273,42 @@ public enum SearchDirection FromEnd } - + + public class StringOpException : RuntimeException + { + public StringOpException(BilingualString message, Exception innerException) : base(message, + innerException) + {} + + public StringOpException(BilingualString message) : base(message) + {} + + public static StringOpException StrStartsWith() + { + return new StringOpException(new BilingualString( + "Ошибка при вызове метода контекста (СтрНачинаетсяС): Недопустимое значение параметра номер '2'", + "Error calling context method (StrStartsWith): Invalid parameter number '2' value")); + } + public static StringOpException StrEndsWith() + { + return new StringOpException(new BilingualString( + "Ошибка при вызове метода контекста (СтрЗаканчиваетсяНа): Недопустимое значение параметра номер '2'", + "Error calling context method (StrEndsWith): Invalid parameter number '2' value")); + } + + public static StringOpException TemplateSyntax(int pos) + { + return new StringOpException(new BilingualString( + $"Ошибка синтаксиса шаблона в позиции {pos}", + $"Template syntax error at position {pos}")); + } + + public static StringOpException TemplateSubst(int pos, int num) + { + return new StringOpException(new BilingualString( + $"Ошибка синтаксиса шаблона в позиции {pos}. Недопустимый номер подстановки: '{num}'", + $"Template syntax error at position {pos}. Invalid substitution number: '{num}'")); + } + } + } diff --git a/tests/stringoperations.os b/tests/stringoperations.os index 8f6c3d25b..ff8880e1d 100644 --- a/tests/stringoperations.os +++ b/tests/stringoperations.os @@ -48,6 +48,13 @@ ВсеТесты.Добавить("ТестДолжен_ВызватьМетод_СтрНайти_ВтороеВхождениеСНачала"); ВсеТесты.Добавить("ТестДолжен_ВызватьМетод_СтрНайти_ВтороеВхождениеСКонца"); ВсеТесты.Добавить("ТестДолжен_ВызватьМетод_СтрНайти_ВтороеВхождениеСКонцаНачинаяСПредпоследнегоСимвола"); + ВсеТесты.Добавить("ТестДолжен_ВызватьМетод_СтрНайти_СПустойСтрокой"); + ВсеТесты.Добавить("ТестДолжен_ВызватьМетод_СтрНайти_СПустойПодстрокойПоиска"); + ВсеТесты.Добавить("ТестДолжен_ВызватьМетод_СтрНайти_СПропущеннымиСтроками"); + ВсеТесты.Добавить("ТестДолжен_ВызватьМетод_СтрНайти_СНевернойНачальнойПозицией"); + ВсеТесты.Добавить("ТестДолжен_ВызватьМетод_СтрНайти_СНулевымЧисломВхождений"); + ВсеТесты.Добавить("ТестДолжен_ВызватьМетод_СтрНайти_СОтрицательнымЧисломВхождений"); + ВсеТесты.Добавить("Тест_ДолженПроверитьНСтрВозвращаетПервуюСтроку"); ВсеТесты.Добавить("ТестДолжен_Проверить_Что_НСТР_С_СуществующимПараметром_ВозвращаетНужнуюСтроку"); ВсеТесты.Добавить("ТестДолжен_Проверить_Что_НСТР_С_НесуществующимПараметром_ВозвращаетПустуюСтроку"); @@ -459,6 +466,69 @@ КонецПроцедуры +Процедура ТестДолжен_ВызватьМетод_СтрНайти_СПустойСтрокой() Экспорт + + ГдеИщем = ""; + ЧтоИщем = ","; + + юТест.ПроверитьРавенство(0, СтрНайти(ГдеИщем, ЧтоИщем)); + +КонецПроцедуры + +Процедура ТестДолжен_ВызватьМетод_СтрНайти_СПустойПодстрокойПоиска() Экспорт + + ГдеИщем = "Один,Два,Три,"; + ЧтоИщем = ""; + + юТест.ПроверитьРавенство(1, СтрНайти(ГдеИщем, ЧтоИщем)); + +КонецПроцедуры + +Процедура ТестДолжен_ВызватьМетод_СтрНайти_СПропущеннымиСтроками() Экспорт + юТест.ПроверитьРавенство(1, СтрНайти(,)); +КонецПроцедуры + +Процедура ТестДолжен_ВызватьМетод_СтрНайти_СНевернойНачальнойПозицией() Экспорт + + ГдеИщем = "Один,Два,Три,"; + ЧтоИщем = ","; + + Попытка + СтрНайти(ГдеИщем, ЧтоИщем, , 999); + Исключение + юТест.ТестПройден(); + Возврат; + КонецПопытки; + + юТест.ТестПровален("Ожидаемое исключение не возникло."); + +КонецПроцедуры + +Процедура ТестДолжен_ВызватьМетод_СтрНайти_СНулевымЧисломВхождений() Экспорт + + ГдеИщем = "Один,Два,Три,"; + ЧтоИщем = ","; + + Попытка + СтрНайти(ГдеИщем, ЧтоИщем,,, 0); + Исключение + юТест.ТестПройден(); + Возврат; + КонецПопытки; + + юТест.ТестПровален("Ожидаемое исключение не возникло."); + +КонецПроцедуры + +Процедура ТестДолжен_ВызватьМетод_СтрНайти_СОтрицательнымЧисломВхождений() Экспорт + + ГдеИщем = "Один,Два,Три,"; + ЧтоИщем = ","; + + юТест.ПроверитьРавенство(0, СтрНайти(ГдеИщем, ЧтоИщем,,, -2)); + +КонецПроцедуры + Процедура Тест_ДолженПроверитьНСтрВозвращаетПервуюСтроку() Экспорт Стр = НСтр("ru = 'Строка1'; en = 'Строка2'", "ru");