Codeunit 10 Type Helper

App
Base Application
Namespace
System.Reflection
Versions
17-28

Procedures, 80Events, 2Obsolete, 4

Versions171819202122232425262728

Source29

Source in 29

src/Layers/W1/BaseApp/System/Utilities/TypeHelper.Codeunit.al1054 lines, Copyright (c) Microsoft Corporation. MIT

namespace System.Reflection;

using Microsoft.Finance.GeneralLedger.Setup;
using System;
using System.DateTime;
using System.Environment;
using System.Environment.Configuration;
using System.Globalization;
using System.Utilities;
using System.Xml;

codeunit 10 "Type Helper"
{
    InherentEntitlements = X;
    InherentPermissions = X;

    Permissions = tabledata "Field" = r;

    trigger OnRun()
    begin
    end;

    var
        UnsupportedTypeErr: Label 'The Type is not supported by the Evaluate function.';
        KeyDoesNotExistErr: Label 'The requested key does not exist.';
        InvalidMonthErr: Label 'An invalid month was specified.';
        StringTooLongErr: Label 'This function only allows strings of length up to %1.', Comment = '%1=a number, e.g. 1024';
        UnsupportedNegativesErr: Label 'Negative parameters are not supported by bitwise function %1.', Comment = '%1=function name';
        BitwiseAndTxt: Label 'BitwiseAnd', Locked = true;
        BitwiseOrTxt: Label 'BitwiseOr', Locked = true;
        BitwiseXorTxt: Label 'BitwiseXor', Locked = true;
        ObsoleteFieldErr: Label 'The field %1 of %2 table is obsolete and cannot be used.', Comment = '%1 - field name, %2 - table name';
        ReadingDataSkippedMsg: Label 'Loading field %1 will be skipped because there was an error when reading the data.\To fix the current data, contact your administrator.\Alternatively, you can overwrite the current data by entering data in the field.', Comment = '%1=field caption';

    procedure Evaluate(var Variable: Variant; String: Text; Format: Text; CultureName: Text): Boolean
    begin
        // Variable is return type containing the string value
        // String is input to evaluate
        // Format is in format "MM/dd/yyyy" only supported on date, search MSDN for more details ("CultureInfo.Name Property")
        // CultureName is in format "en-US", search MSDN for more details ("Custom Date and Time Format Strings")
        case true of
            Variable.IsDate:
                exit(TryEvaluateDate(String, Format, CultureName, Variable));
            Variable.IsDateTime:
                exit(TryEvaluateDateTime(String, Format, CultureName, Variable));
            Variable.IsDecimal:
                exit(TryEvaluateDecimal(String, CultureName, Variable));
            Variable.IsInteger:
                exit(TryEvaluateInteger(String, CultureName, Variable));
            else
                Error(UnsupportedTypeErr);
        end;
    end;

    local procedure TryEvaluateDate(DateText: Text; Format: Text; CultureName: Text; var EvaluatedDate: Date): Boolean
    var
        DotNet_CultureInfo: Codeunit DotNet_CultureInfo;
        DotNet_DateTime: Codeunit DotNet_DateTime;
        DotNet_DateTimeStyles: Codeunit DotNet_DateTimeStyles;
        DotNet_XMLConvert: Codeunit DotNet_XMLConvert;
        DotNet_DateTimeOffset: Codeunit DotNet_DateTimeOffset;
    begin
        if (Format = '') and (CultureName = '') then begin
            DotNet_XMLConvert.ToDateTimeOffset(DateText, DotNet_DateTimeOffset);
            DotNet_DateTimeOffset.DateTime(DotNet_DateTime);
        end else begin
            DotNet_CultureInfo.GetCultureInfoByName(CultureName);
            DotNet_DateTimeStyles.None();
            case Format of
                '':
                    if not DotNet_DateTime.TryParse(DateText, DotNet_CultureInfo, DotNet_DateTimeStyles) then
                        exit(false);
                else
                    if not DotNet_DateTime.TryParseExact(DateText, Format, DotNet_CultureInfo, DotNet_DateTimeStyles) then
                        exit(false);
            end;
        end;

        EvaluatedDate := DMY2Date(DotNet_DateTime.Day(), DotNet_DateTime.Month(), DotNet_DateTime.Year());
        exit(true);
    end;

    local procedure TryEvaluateDateTime(DateTimeText: Text; Format: Text; CultureName: Text; var EvaluatedDateTime: DateTime): Boolean
    var
        DotNet_CultureInfo: Codeunit DotNet_CultureInfo;
        DotNet_DateTime: Codeunit DotNet_DateTime;
        DotNet_DateTimeStyles: Codeunit DotNet_DateTimeStyles;
        EvaluatedTime: Time;
    begin
        if CultureName = '' then
            DotNet_CultureInfo.InvariantCulture()
        else
            DotNet_CultureInfo.GetCultureInfoByName(CultureName);
        DotNet_DateTimeStyles.None();
        case Format of
            '':
                if not DotNet_DateTime.TryParse(DateTimeText, DotNet_CultureInfo, DotNet_DateTimeStyles) then
                    exit(false);
            else
                if not DotNet_DateTime.TryParseExact(DateTimeText, Format, DotNet_CultureInfo, DotNet_DateTimeStyles) then
                    exit(false);
        end;

        if not SYSTEM.Evaluate(
             EvaluatedTime,
             StrSubstNo(
               '%1:%2:%3.%4',
               DotNet_DateTime.Hour(),
               DotNet_DateTime.Minute(),
               DotNet_DateTime.Second(),
               DotNet_DateTime.Millisecond()))
        then
            exit(false);
        EvaluatedDateTime :=
          CreateDateTime(
            DMY2Date(DotNet_DateTime.Day(), DotNet_DateTime.Month(), DotNet_DateTime.Year()), EvaluatedTime);
        exit(true);
    end;

    local procedure TryEvaluateDecimal(DecimalText: Text; CultureName: Text; var EvaluatedDecimal: Decimal): Boolean
    var
        CultureInfo: DotNet CultureInfo;
        DotNetDecimal: DotNet Decimal;
        NumberStyles: DotNet NumberStyles;
    begin
        EvaluatedDecimal := 0;
        NumberStyles := NumberStyles.Number + NumberStyles.AllowCurrencySymbol + NumberStyles.AllowParentheses;
        if DotNetDecimal.TryParse(DecimalText, NumberStyles, CultureInfo.GetCultureInfo(CultureName), EvaluatedDecimal) then
            exit(true);
        exit(false)
    end;

    local procedure TryEvaluateInteger(IntegerText: Text; CultureName: Text; var EvaluatedInteger: Integer): Boolean
    var
        CultureInfo: DotNet CultureInfo;
        DotNetInteger: DotNet Int32;
        NumberStyles: DotNet NumberStyles;
    begin
        EvaluatedInteger := 0;
        if DotNetInteger.TryParse(IntegerText, NumberStyles.Number, CultureInfo.GetCultureInfo(CultureName), EvaluatedInteger) then
            exit(true);
        exit(false)
    end;

    procedure GetLocalizedMonthToInt(Month: Text): Integer
    var
        TestMonth: Text;
        Result: Integer;
    begin
        Month := LowerCase(Month);

        for Result := 1 to 12 do begin
            TestMonth := LowerCase(Format(CalcDate(StrSubstNo('<CY+%1M>', Result)), 0, '<Month Text>'));
            if Month = TestMonth then
                exit(Result);
        end;

        Error(InvalidMonthErr);
    end;

    procedure CompareDateTime(DateTimeA: DateTime; DateTimeB: DateTime): Integer
    begin
        // Compares the specified DateTime values for equality within a small threshold.
        // Returns 1 if DateTimeA > DateTimeB, -1 if DateTimeB > DateTimeA, and 0 if they
        // are equal.

        // The threshold must be used to compensate for the varying levels of precision
        // when storing DateTime values. An example of this is the T-SQL datetime type,
        // which has a precision that goes down to the nearest 0, 3, or 7 milliseconds.

        case true of
            DateTimeA = DateTimeB:
                exit(0);
            DateTimeA = 0DT:
                exit(-1);
            DateTimeB = 0DT:
                exit(1);
            Abs(DateTimeA - DateTimeB) < 10:
                exit(0);
            DateTimeA > DateTimeB:
                exit(1);
            else
                exit(-1);
        end;
    end;

    procedure FormatDate(DateToFormat: Date; LanguageId: Integer): Text
    var
        DotNet_CultureInfo: Codeunit DotNet_CultureInfo;
        DotNet_DateTimeFormatInfo: Codeunit DotNet_DateTimeFormatInfo;
        DotNet_DateTime: Codeunit DotNet_DateTime;
    begin
        DotNet_CultureInfo.GetCultureInfoById(LanguageId);
        DotNet_CultureInfo.DateTimeFormat(DotNet_DateTimeFormatInfo);
        DotNet_DateTime.DateTime(Date2DMY(DateToFormat, 3), Date2DMY(DateToFormat, 2), Date2DMY(DateToFormat, 1));
        exit(DotNet_DateTime.ToString(DotNet_DateTimeFormatInfo));
    end;

    procedure FormatDate(DateToFormat: Date; Format: Text; CultureName: Text): Text
    var
        DotNet_CultureInfo: Codeunit DotNet_CultureInfo;
        DotNet_DateTimeFormatInfo: Codeunit DotNet_DateTimeFormatInfo;
        DotNet_DateTime: Codeunit DotNet_DateTime;
    begin
        if CultureName = '' then
            DotNet_CultureInfo.InvariantCulture()
        else
            DotNet_CultureInfo.GetCultureInfoByName(CultureName);

        DotNet_CultureInfo.DateTimeFormat(DotNet_DateTimeFormatInfo);
        DotNet_DateTime.DateTime(Date2DMY(DateToFormat, 3), Date2DMY(DateToFormat, 2), Date2DMY(DateToFormat, 1));
        exit(DotNet_DateTime.ToString(Format, DotNet_DateTimeFormatInfo));
    end;

    procedure FormatDateWithCurrentCulture(DateToFormat: Date): Text
    var
        Language: Codeunit Language;
    begin
        exit(FormatDate(DateToFormat, 'd', Language.GetUserLanguageTag()));
    end;

    procedure GetHMSFromTime(var Hour: Integer; var Minute: Integer; var Second: Integer; TimeSource: Time)
    var
        Milliseconds: Integer;
    begin
        Milliseconds := TimeSource - 000000T;
        Hour := Milliseconds div 1000 div 60 div 60;

        Milliseconds -= (Hour * 1000 * 60 * 60);
        Minute := Milliseconds div 1000 div 60;

        Milliseconds -= (Minute * 1000 * 60);
        Second := Milliseconds div 1000;
    end;

    procedure IsLeapYear(Date: Date): Boolean
    var
        DateTime: DotNet DateTime;
    begin
        exit(DateTime.IsLeapYear(Date2DMY(Date, 3)));
    end;

    procedure GetOptionNo(Value: Text; OptionString: Text): Integer
    var
        OptionNo: Integer;
        OptionsQty: Integer;
    begin
        Value := UpperCase(Value);
        OptionString := UpperCase(OptionString);

        if (Value = '') and (StrPos(OptionString, ' ') = 1) then
            exit(0);
        if (Value <> '') and (StrPos(OptionString, Value) = 0) then
            exit(-1);

        OptionsQty := GetNumberOfOptions(OptionString);
        if OptionsQty > 0 then begin
            for OptionNo := 0 to OptionsQty - 1 do begin
                if OptionsAreEqual(Value, CopyStr(OptionString, 1, StrPos(OptionString, ',') - 1)) then
                    exit(OptionNo);
                OptionString := DelStr(OptionString, 1, StrPos(OptionString, ','));
            end;
            OptionNo += 1;
        end;

        if OptionsAreEqual(Value, OptionString) then
            exit(OptionNo);

        exit(-1);
    end;

    procedure GetOptionNoFromTableField(Value: Text; TableNo: Integer; FieldNo: Integer): Integer
    var
        RecRef: RecordRef;
        FieldRef: FieldRef;
    begin
        RecRef.Open(TableNo);
        FieldRef := RecRef.Field(FieldNo);
        exit(GetOptionNo(Value, FieldRef.OptionCaption));
    end;

    procedure GetNumberOfOptions(OptionString: Text): Integer
    begin
        exit(StrLen(OptionString) - StrLen(DelChr(OptionString, '=', ',')));
    end;

    procedure OptionsAreEqual(Value: Text; CurrentOption: Text): Boolean
    begin
        exit(((Value <> '') and (Value = CurrentOption)) or ((Value = '') and (CurrentOption = ' ')));
    end;

    procedure IsNumeric(Text: Text): Boolean
    var
        Decimal: Decimal;
    begin
        exit(SYSTEM.Evaluate(Decimal, Text));
    end;

    procedure GetField(TableNo: Integer; FieldNo: Integer; var "Field": Record "Field") Result: Boolean
    var
        IsHandled: Boolean;
    begin
        IsHandled := false;
        OnBeforeGetField(TableNo, FieldNo, "Field", Result, IsHandled);
        if IsHandled then
            exit(Result);

        exit(Field.Get(TableNo, FieldNo) and (Field.ObsoleteState <> Field.ObsoleteState::Removed));
    end;

    procedure GetFieldLength(TableNo: Integer; FieldNo: Integer) FieldLength: Integer
    var
        "Field": Record "Field";
        IsHandled: Boolean;
    begin
        IsHandled := false;
        OnBeforeGetFieldLength(Field, FieldLength, IsHandled);
        if IsHandled then
            exit(FieldLength);

        if GetField(TableNo, FieldNo, Field) then
            exit(Field.Len);

        exit(0);
    end;

    procedure TestFieldIsNotObsolete("Field": Record "Field")
    begin
        if Field.ObsoleteState = Field.ObsoleteState::Removed then
            Error(ObsoleteFieldErr, Field."Field Caption", Field.TableName);
    end;

    procedure IsPhoneNumber(Input: Text): Boolean
    var
        Regex: Codeunit Regex;
    begin
        exit(Regex.IsMatch(Input, '^[\(\)\-\+0-9 ]*$'));
    end;

    procedure GetUserTimezoneOffset(var Duration: Duration): Boolean
    var
        UserPersonalization: Record "User Personalization";
        TimeZoneInfo: DotNet TimeZoneInfo;
        TimeZone: Text;
    begin
        if not UserPersonalization.Get(UserSecurityId()) then
            exit(false);

        TimeZone := UserPersonalization."Time Zone";

        if TimeZone = '' then
            exit(false);

        TimeZoneInfo := TimeZoneInfo.FindSystemTimeZoneById(TimeZone);
        Duration := TimeZoneInfo.GetUtcOffset(GetCurrUTCDateTime());
        exit(true);
    end;

    procedure GetUserClientTypeOffset(var Duration: Duration)
    var
        ClientTypeManagement: Codeunit "Client Type Management";
    begin
        Duration := 0;
        if ClientTypeManagement.GetCurrentClientType() = CLIENTTYPE::Web then
            GetUserTimezoneOffset(Duration);
    end;

    procedure GetTimezoneOffset(var Duration: Duration; TimeZoneID: Text)
    var
        DotNet_DateTimeOffset: Codeunit DotNet_DateTimeOffset;
        TimeZoneInfo: DotNet TimeZoneInfo;
    begin
        Duration := DotNet_DateTimeOffset.GetOffset();
        if TimeZoneID <> '' then begin
            TimeZoneInfo := TimeZoneInfo.FindSystemTimeZoneById(TimeZoneID);
            Duration := TimeZoneInfo.BaseUtcOffset;
        end;
    end;

    procedure EvaluateUnixTimestamp(Timestamp: BigInteger): DateTime
    var
        ResultDateTime: DateTime;
        EpochDateTime: DateTime;
        TimezoneOffset: Duration;
        TimestampInMilliseconds: BigInteger;
    begin
        if not GetUserTimezoneOffset(TimezoneOffset) then
            TimezoneOffset := 0;

        EpochDateTime := CreateDateTime(DMY2Date(1, 1, 1970), 0T);

        TimestampInMilliseconds := Timestamp * 1000;

        ResultDateTime := EpochDateTime + TimestampInMilliseconds + TimezoneOffset;

        exit(ResultDateTime);
    end;

    procedure EvaluateUTCDateTime(DateTimeText: Text) EvaluatedDateTime: DateTime
    var
        TypeHelper: Codeunit "Type Helper";
        Value: Variant;
    begin
        Value := EvaluatedDateTime;
        if TypeHelper.Evaluate(Value, DateTimeText, 'R', '') then
            EvaluatedDateTime := Value;
    end;

    procedure FormatDateTime(FormattingDateTime: DateTime; Format: Text; CultureName: Text): Text
    var
        CultureInfo: DotNet CultureInfo;
        DateTimeOffset: DotNet DateTimeOffset;
    begin
        if CultureName = '' then
            CultureInfo := CultureInfo.InvariantCulture
        else
            CultureInfo := CultureInfo.GetCultureInfo(CultureName);

        DateTimeOffset := DateTimeOffset.DateTimeOffset(FormattingDateTime);
        DateTimeOffset := DateTimeOffset.ToLocalTime();

        exit(DateTimeOffset.ToString(Format, CultureInfo));
    end;

    procedure FormatUtcDateTime(DateTime: DateTime; DataFormat: Text; DataFormattingCulture: Text) String: Text
    var
        CultureInfo: DotNet CultureInfo;
        DotNetString: DotNet String;
    begin
        if DataFormattingCulture = '' then
            CultureInfo := CultureInfo.CurrentCulture
        else
            CultureInfo := CultureInfo.CultureInfo(DataFormattingCulture);

        String := DotNetString.Format(CultureInfo, '{0:' + DataFormat + '}', DateTime);
    end;

    procedure GetCurrUTCDateTime(): DateTime
    var
        DotNetDateTime: DotNet DateTime;
    begin
        DotNetDateTime := DotNetDateTime.UtcNow;
        exit(DotNetDateTime)
    end;

    procedure GetCurrUTCDateTimeAsText(): Text
    begin
        exit(FormatDateTime(GetCurrUTCDateTime(), 'R', ''));
    end;

    procedure GetCurrUTCDateTimeISO8601(): Text
    var
        DotNetDateTime: DotNet DateTime;
    begin
        exit(DotNetDateTime.UtcNow.ToString('yyyy-MM-ddTHH:mm:ssZ'));
    end;

    procedure AddHoursToDateTime(SourceDateTime: DateTime; NoOfHours: Integer): DateTime
    var
        MillisecondsToAdd: BigInteger;
    begin
        MillisecondsToAdd := NoOfHours * 3600000; // 60 * 60 * 1000
        exit(SourceDateTime + MillisecondsToAdd);
    end;

    procedure FormatDecimal(Decimal: Decimal; DataFormat: Text; DataFormattingCulture: Text) String: Text
    var
        CultureInfo: DotNet CultureInfo;
        DotNetString: DotNet String;
    begin
        if DataFormattingCulture = '' then
            CultureInfo := CultureInfo.CurrentCulture
        else
            CultureInfo := CultureInfo.CultureInfo(DataFormattingCulture);

        String := DotNetString.Format(CultureInfo, '{0:' + DataFormat + '}', Decimal);
    end;

    procedure UrlEncode(var Value: Text): Text
    var
        HttpUtility: DotNet HttpUtility;
    begin
        Value := HttpUtility.UrlEncode(Value);
        exit(Value);
    end;

    [NonDebuggable]
    procedure UrlEncode(var Value: SecretText): SecretText
    var
        HttpUtility: DotNet HttpUtility;
        EncodedValue: Text;
    begin
        EncodedValue := HttpUtility.UrlEncode(Value.Unwrap());
        Value := EncodedValue;
        exit(EncodedValue);
    end;

#if not CLEAN27
    [NonDebuggable]
    [Obsolete('Use UrlEncode with secret text.', '27.0')]
    procedure UrlEncodeSecret(var Value: Text): Text
    var
        HttpUtility: DotNet HttpUtility;
    begin
        Value := HttpUtility.UrlEncode(Value);
        exit(Value);
    end;
#endif

    procedure UrlDecode(var Value: Text): Text
    var
        HttpUtility: DotNet HttpUtility;
    begin
        Value := HttpUtility.UrlDecode(Value);
        exit(Value);
    end;

    procedure HtmlEncode(var Value: Text): Text
    var
        HttpUtility: DotNet HttpUtility;
    begin
        Value := HttpUtility.HtmlEncode(Value);
        exit(Value);
    end;

    procedure HtmlDecode(var Value: Text): Text
    var
        HttpUtility: DotNet HttpUtility;
    begin
        Value := HttpUtility.HtmlDecode(Value);
        exit(Value);
    end;

    procedure JavaScriptStringEncode(Value: Text): Text
    var
        HttpUtility: DotNet HttpUtility;
    begin
        exit(HttpUtility.JavaScriptStringEncode(Value));
    end;

    procedure JavaScriptStringEncode(Value: Text; AddDoubleQuotes: Boolean): Text
    var
        HttpUtility: DotNet HttpUtility;
    begin
        exit(HttpUtility.JavaScriptStringEncode(Value, AddDoubleQuotes));
    end;

    procedure UriEscapeDataString(Value: Text): Text
    var
        Uri: DotNet Uri;
    begin
        exit(Uri.EscapeDataString(Value));
    end;

    procedure UriGetAuthority(Value: Text): Text
    var
        Uri: DotNet Uri;
        UriPartial: DotNet UriPartial;
    begin
        Uri := Uri.Uri(Value);
        exit(Uri.GetLeftPart(UriPartial.Authority));
    end;

    procedure GetKeyAsString(RecordVariant: Variant; KeyIndex: Integer): Text
    var
        DataTypeManagement: Codeunit "Data Type Management";
        RecRef: RecordRef;
        KeyFieldRef: FieldRef;
        SelectedKeyRef: KeyRef;
        I: Integer;
        KeyString: Text;
        Separator: Text;
    begin
        DataTypeManagement.GetRecordRef(RecordVariant, RecRef);

        if RecRef.KeyCount < KeyIndex then
            Error(KeyDoesNotExistErr);

        SelectedKeyRef := RecRef.KeyIndex(KeyIndex);

        for I := 1 to SelectedKeyRef.FieldCount do begin
            KeyFieldRef := SelectedKeyRef.FieldIndex(I);
            KeyString += Separator + KeyFieldRef.Name;
            Separator := ',';
        end;

        exit(KeyString);
    end;

    procedure ReadAsTextWithSeparator(InStream: InStream; LineSeparator: Text) Content: Text
    var
        Tb: TextBuilder;
        ContentLine: Text;
    begin
        InStream.ReadText(ContentLine);
        Tb.Append(ContentLine);
        while not InStream.EOS do begin
            InStream.ReadText(ContentLine);
            Tb.Append(LineSeparator);
            Tb.Append(ContentLine);
        end;

        exit(Tb.ToText());
    end;

    [TryFunction]
    procedure TryReadAsTextWithSeparator(InStream: InStream; LineSeparator: Text; var Content: Text)
    begin
        Content := ReadAsTextWithSeparator(InStream, LineSeparator);
    end;

    procedure TryReadAsTextWithSepAndFieldErrMsg(InStream: InStream; LineSeparator: Text; FieldCaption: Text) Content: Text
    begin
        if not TryReadAsTextWithSeparator(InStream, LineSeparator, Content) then
            Message(ReadingDataSkippedMsg, FieldCaption);
        exit(Content);
    end;

    procedure CRLFSeparator(): Text[2]
    var
        CRLF: Text[2];
    begin
        CRLF[1] := 13; // Carriage return, '\r'
        CRLF[2] := 10; // Line feed, '\n'
        exit(CRLF);
    end;

    procedure LFSeparator(): Text[1]
    var
        LF: Text[1];
    begin
        LF[1] := 10; // Line feed, '\n'
        exit(LF);
    end;

    procedure SortRecordRef(var RecRef: RecordRef; CommaSeparatedFieldsToSort: Text; "Ascending": Boolean)
    var
        OrderString: Text;
    begin
        if Ascending then
            OrderString := 'order(ascending)'
        else
            OrderString := 'order(descending)';

        RecRef.SetView(StrSubstNo('SORTING(%1) %2', CommaSeparatedFieldsToSort, OrderString));
        if RecRef.FindSet() then;
    end;

    procedure TextDistance(Text1: Text; Text2: Text): Integer
    var
        Array1: array[1026] of Integer;
        Array2: array[1026] of Integer;
        i: Integer;
        j: Integer;
        Cost: Integer;
        MaxLen: Integer;
    begin
        // Returns the number of edits to get from Text1 to Text2
        // Reference: https://en.wikipedia.org/wiki/Levenshtein_distance
        if (StrLen(Text1) + 2 > ArrayLen(Array1)) or (StrLen(Text2) + 2 > ArrayLen(Array1)) then
            Error(StringTooLongErr, ArrayLen(Array1) - 2);
        if Text1 = Text2 then
            exit(0);
        if Text1 = '' then
            exit(StrLen(Text2));
        if Text2 = '' then
            exit(StrLen(Text1));

        if StrLen(Text1) >= StrLen(Text2) then
            MaxLen := StrLen(Text1)
        else
            MaxLen := StrLen(Text2);

        for i := 0 to MaxLen + 1 do
            Array1[i + 1] := i;

        for i := 0 to StrLen(Text1) - 1 do begin
            Array2[1] := i + 1;
            for j := 0 to StrLen(Text2) - 1 do begin
                if Text1[i + 1] = Text2[j + 1] then
                    Cost := 0
                else
                    Cost := 1;
                Array2[j + 2] := MinimumInt3(Array2[j + 1] + 1, Array1[j + 2] + 1, Array1[j + 1] + Cost);
            end;
            for j := 1 to MaxLen + 2 do
                Array1[j] := Array2[j];
        end;
        exit(Array2[StrLen(Text2) + 1]);
    end;

    procedure NewLine(): Text
    var
        SystemEnvironment: DotNet SystemEnvironment;
    begin
        SystemEnvironment := SystemEnvironment.SystemEnvironment();
        exit(SystemEnvironment.NewLine());
    end;

    local procedure MinimumInt3(i1: Integer; i2: Integer; i3: Integer): Integer
    begin
        if (i1 <= i2) and (i1 <= i3) then
            exit(i1);
        if (i2 <= i1) and (i2 <= i3) then
            exit(i2);
        exit(i3);
    end;

    procedure GetMaxNumberOfParametersInSQLQuery(): Integer
    begin
        exit(2100);
    end;

    procedure BitwiseAnd(A: Integer; B: Integer): Integer
    var
        Result: Integer;
        BitMask: Integer;
        BitIndex: Integer;
        MaxBitIndex: Integer;
    begin
        if (A < 0) or (B < 0) then
            Error(UnsupportedNegativesErr, BitwiseAndTxt);
        BitMask := 1;
        Result := 0;
        MaxBitIndex := 31; // 1st bit is ignored as it is always equals to 0 for positive Int32 numbers
        for BitIndex := 1 to MaxBitIndex do begin
            if ((A mod 2) = 1) and ((B mod 2) = 1) then
                Result += BitMask;
            A := A div 2;
            B := B div 2;
            if BitIndex < MaxBitIndex then
                BitMask += BitMask;
        end;
        exit(Result);
    end;

    procedure BitwiseOr(A: Integer; B: Integer): Integer
    var
        Result: Integer;
        BitMask: Integer;
        BitIndex: Integer;
        MaxBitIndex: Integer;
    begin
        if (A < 0) or (B < 0) then
            Error(UnsupportedNegativesErr, BitwiseOrTxt);
        BitMask := 1;
        Result := 0;
        MaxBitIndex := 31; // 1st bit is ignored as it is always equals to 0 for positive Int32 numbers
        for BitIndex := 1 to MaxBitIndex do begin
            if ((A mod 2) = 1) or ((B mod 2) = 1) then
                Result += BitMask;
            A := A div 2;
            B := B div 2;
            if BitIndex < MaxBitIndex then
                BitMask += BitMask;
        end;
        exit(Result);
    end;

    procedure BitwiseXor(A: Integer; B: Integer): Integer
    var
        Result: Integer;
        BitMask: Integer;
        BitIndex: Integer;
        MaxBitIndex: Integer;
    begin
        if (A < 0) or (B < 0) then
            Error(UnsupportedNegativesErr, BitwiseXorTxt);
        BitMask := 1;
        Result := 0;
        MaxBitIndex := 31; // 1st bit is ignored as it is always equals to 0 for positive Int32 numbers
        for BitIndex := 1 to MaxBitIndex do begin
            if (A mod 2) <> (B mod 2) then
                Result += BitMask;
            A := A div 2;
            B := B div 2;
            if BitIndex < MaxBitIndex then
                BitMask += BitMask;
        end;
        exit(Result);
    end;

    procedure GetFormattedCurrentDateTimeInUserTimeZone(StringFormat: Text): Text
    var
        DateTime: DotNet DateTime;
        TimezoneOffset: Duration;
    begin
        if not GetUserTimezoneOffset(TimezoneOffset) then
            TimezoneOffset := 0;
        DateTime := DateTime.Now;
        DateTime := DateTime.ToUniversalTime() + TimezoneOffset;
        exit(DateTime.ToString(StringFormat));
    end;

    procedure GetCurrentDateTimeInUserTimeZone() Result: DateTime
    var
        DateTime: DotNet DateTime;
        TimezoneOffset: Duration;
    begin
        if not GetUserTimezoneOffset(TimezoneOffset) then
            TimezoneOffset := 0;
        DateTime := DateTime.Now;
        DateTime := DateTime.ToUniversalTime() + TimezoneOffset;
        System.Evaluate(Result, DateTime.ToString());
        exit(Result);
    end;

    procedure GetInputDateTimeInUserTimeZone(InputDateTime: DateTime) Result: DateTime
    var
        TypeHelper: Codeunit "Type Helper";
        DateTime: DotNet DateTime;
        TimezoneOffset: Duration;
    begin
        if not TypeHelper.GetUserTimezoneOffset(TimezoneOffset) then
            TimezoneOffset := 0;
        DateTime := InputDateTime;
        DateTime := DateTime.ToUniversalTime() + TimezoneOffset;
        System.Evaluate(Result, DateTime.ToString());
        exit(Result);
    end;

    /// <summary>
    /// NOTE: The procedure's name is incorrect. This procedure converts the time from current client timezone to target timezone, instead of converting from utc to target time zone.
    /// </summary>
    /// <param name="InputDateTime">The datetime based on current Client's time zone.</param>
    /// <param name="TimeZoneTxt">The destination timezone, such as 'GMT standard time','UTC','China standard time'.</param>
    /// <returns>The new datetime based on the detination timezone</returns>
    procedure ConvertDateTimeFromUTCToTimeZone(InputDateTime: DateTime; TimeZoneTxt: Text): DateTime
    var
        TimeZoneInfo: DotNet TimeZoneInfo;
        Offset: Duration;
    begin
        if TimeZoneTxt = '' then
            exit(InputDateTime);

        GetUserClientTypeOffset(Offset);
        InputDateTime := InputDateTime - Offset;

        TimeZoneInfo := TimeZoneInfo.FindSystemTimeZoneById(TimeZoneTxt);
        exit(InputDateTime + TimeZoneInfo.BaseUtcOffset);
    end;

    /// <summary>
    /// Convert the datetime from the specified timezone to current client's timezone.
    /// </summary>
    /// <param name="InputDateTime">The datetime based on the specified timezone.</param>
    /// <param name="TimeZoneTxt">The specified timezone, such as 'GMT standard time','UTC','China standard time'.</param>
    /// <returns>The new datetime based on current client's timezone</returns>
    procedure ConvertDateTimeFromInputTimeZoneToClientTimezone(InputDateTime: DateTime; TimeZoneTxt: Text): DateTime
    var
        TimeZoneInfo: DotNet TimeZoneInfo;
        Offset: Duration;
    begin
        if TimeZoneTxt = '' then
            exit(InputDateTime);

        GetUserClientTypeOffset(Offset);
        InputDateTime := InputDateTime + Offset;

        TimeZoneInfo := TimeZoneInfo.FindSystemTimeZoneById(TimeZoneTxt);
        exit(InputDateTime - TimeZoneInfo.BaseUtcOffset);
    end;

    procedure IntToHex(IntValue: Integer): Text
    var
        DotNetIntPtr: DotNet IntPtr;
    begin
        DotNetIntPtr := DotNetIntPtr.IntPtr(IntValue);
        exit(DotNetIntPtr.ToString('X'));
    end;

    procedure Maximum(Value1: Decimal; Value2: Decimal): Decimal
    begin
        if Value1 > Value2 then
            exit(Value1);
        exit(Value2);
    end;

    procedure Minimum(Value1: Decimal; Value2: Decimal): Decimal
    begin
        if Value1 < Value2 then
            exit(Value1);
        exit(Value2);
    end;

    procedure TransferFieldsWithValidate(var TempFieldBuffer: Record "Field Buffer" temporary; RecordVariant: Variant; var TargetTableRecRef: RecordRef)
    var
        DataTypeManagement: Codeunit "Data Type Management";
        SourceRecRef: RecordRef;
        TargetFieldRef: FieldRef;
        SourceFieldRef: FieldRef;
    begin
        DataTypeManagement.GetRecordRef(RecordVariant, SourceRecRef);

        TempFieldBuffer.Reset();
        if not TempFieldBuffer.FindFirst() then
            exit;

        repeat
            if TargetTableRecRef.FieldExist(TempFieldBuffer."Field ID") then begin
                SourceFieldRef := SourceRecRef.Field(TempFieldBuffer."Field ID");
                TargetFieldRef := TargetTableRecRef.Field(TempFieldBuffer."Field ID");
                if TargetFieldRef.Class = FieldClass::Normal then
                    if TargetFieldRef.Value <> SourceFieldRef.Value then
                        TargetFieldRef.Validate(SourceFieldRef.Value);
            end;
        until TempFieldBuffer.Next() = 0;
    end;

    procedure CalculateLog(Number: Decimal): Decimal
    var
        Math: DotNet Math;
    begin
        exit(Math.Log10(Number));
    end;

    procedure GetAmountFormatLCYWithUserLocale(): Text
    begin
        exit(GetAmountFormatLCYWithUserLocale(0));
    end;

    procedure GetAmountFormatLCYWithUserLocale(DecimalPlaces: Integer): Text
    var
        GeneralLedgerSetup: Record "General Ledger Setup";
        CurrencySymbol: Text[10];
    begin
        GeneralLedgerSetup.Get();
        CurrencySymbol := GeneralLedgerSetup.GetCurrencySymbol();

        exit(GetAmountFormatWithUserLocale(CurrencySymbol, DecimalPlaces));
    end;

    procedure GetAmountFormatWithUserLocale(CurrencySymbol: Text[10]): Text
    begin
        exit(GetAmountFormatWithUserLocale(CurrencySymbol, 0));
    end;

    procedure GetAmountFormatWithUserLocale(CurrencySymbol: Text[10]; DecimalPlaces: Integer): Text
    var
        UserPersonalization: Record "User Personalization";
    begin
        if not UserPersonalization.Get(UserSecurityId()) then
            exit(GetDefaultAmountFormat(DecimalPlaces));

        exit(GetAmountFormat(UserPersonalization."Locale ID", CurrencySymbol, DecimalPlaces));
    end;

    procedure GetAmountFormat(LocaleId: Integer; CurrencySymbol: Text[10]): Text
    begin
        exit(GetAmountFormatDecimalPlaces(LocaleId, CurrencySymbol, 0));
    end;

    procedure GetAmountFormat(LocaleId: Integer; CurrencySymbol: Text[10]; DecimalPlaces: Integer): Text
    begin
        exit(GetAmountFormatDecimalPlaces(LocaleId, CurrencySymbol, DecimalPlaces));
    end;

    procedure GetAmountFormatDecimalPlaces(LocaleId: Integer; CurrencySymbol: Text[10]; DecimalPlaces: Integer): Text
    var
        CurrencyPositivePattern: Integer;
    begin
        // set position of currency symbol based on the locale
        if LocaleId <= 0 then
            exit(GetDefaultAmountFormat(DecimalPlaces));

        if not GetCurrencyStyle(LocaleId, CurrencyPositivePattern) then
            exit(GetDefaultAmountFormat(DecimalPlaces));

        case CurrencyPositivePattern of
            0: // $n
                exit(CurrencySymbol + GetDefaultAmountFormat(DecimalPlaces));
            1: // n$
                exit(GetDefaultAmountFormat(DecimalPlaces) + CurrencySymbol);
            2: // $ n
                exit(CurrencySymbol + ' ' + GetDefaultAmountFormat(DecimalPlaces));
            3: // n $
                exit(GetDefaultAmountFormat(DecimalPlaces) + ' ' + CurrencySymbol);
            else
                exit(GetDefaultAmountFormat(DecimalPlaces));
        end
    end;

    internal procedure GetDefaultAmountFormat(): Text
    begin
        exit('<Precision,0:0><Standard Format,0>');
    end;

    internal procedure GetDefaultAmountFormat(DecimalPlaces: Integer): Text
    begin
#pragma warning disable AA0217
        exit(StrSubstNo('<Precision,0:%1><Standard Format,0>', DecimalPlaces));
#pragma warning restore AA0217
    end;

    procedure GetXMLAmountFormatWithTwoDecimalPlaces(): Text
    begin
        exit('<Precision,2:2><Standard Format,9>');
    end;

    procedure GetXMLDateFormat(): Text
    begin
        exit('<Standard Format,9>');
    end;

    procedure CopyRecVariantToRecRef(RecordVariant: Variant; var RecRef: RecordRef)
    begin
        if RecordVariant.IsRecord() then
            RecRef.GetTable(RecordVariant)
        else
            if RecordVariant.IsRecordRef() then
                RecRef := RecordVariant;
    end;

    procedure IsLatinLetter(ch: Char): Boolean
    begin
        exit(((ch >= 'A') and (ch <= 'Z')) or ((ch >= 'a') and (ch <= 'z')));
    end;

    procedure IsDigit(ch: Char): Boolean
    begin
        exit((ch >= '0') and (ch <= '9'));
    end;

    procedure IsUpper(ch: Char): Boolean
    var
        charTxt: Text[1];
    begin
        charTxt[1] := ch;
        exit(charTxt = UpperCase(charTxt));
    end;

    [TryFunction]
    local procedure GetCurrencyStyle(LocaleId: Integer; var CurrencyPositivePattern: Integer)
    var
        CultureInfo: DotNet CultureInfo;
        NumberFormat: DotNet NumberFormatInfo;
    begin
        CultureInfo := CultureInfo.GetCultureInfo(LocaleId);
        NumberFormat := CultureInfo.NumberFormat;
        CurrencyPositivePattern := NumberFormat.CurrencyPositivePattern;
    end;

    [IntegrationEvent(false, false)]
    local procedure OnBeforeGetFieldLength(var "Field": Record "Field"; var FieldLength: Integer; var IsHandled: Boolean)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnBeforeGetField(TableNo: Integer; FieldNo: Integer; var "Field": Record "Field"; var Result: Boolean; var IsHandled: Boolean)
    begin
    end;
}