# DynamicOps.java

    /*
    ** Module   : DynamicOps.java
    ** Abstract : operators and functions that have dynamic type behavior
    **
    ** Copyright (c) 2013-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --------------------------------Description---------------------------------
    ** 001 GES 20130301 Created the initial version with support for a version of the plus and minus
    **                  operators which dynamically handles the return type based on the operands.
    ** 002 OM  20130321 Added support for LENGTH function when the 1st parameter type cannot be
    **                  detected at conversion time.
    ** 003 VIG 20131010 Added null processing for type argument in length-methods
    ** 004 MAG 20140606 Implements plus(BDT,BDT) and minus(BDT,BDT) binary operators.
    ** 005 OM  20160711 Switch to int64 as return type of length() methods.
    ** 006 CA  20160919 Added APIs for divide, multiply, modulo and negate.
    ** 007 CA  20160924 Fixed a problem in H006 which refactored the validity of the operands for the
    **                  plus and minus operators: the entire expression needs to be negated, not just
    **                  the first operand.
    ** 008 EVL 20161003 Making transformation attempt from character to numeric as possible fix for
    **                  operands compatibility issues.
    ** 009 CA  20181221 Added multiple(NumberType, NumberType).
    ** 010 CA  20190509 MIN and MAX can be POLY, too.
    ** 011 CA  20190826 Fixed string concatenation, where toStringMessage() must be used, and not
    **                  toString().
    ** 012 CA  20200503 DynamicOps.length must return integer and not int64.
    **     AL2 20220319 Added value proxy check.
    ** 013 CA  20230116 A dynamic DIVIDE always returns a decimal.
    ** 014 AS  20241120 Fixed the length method for some BDTs.
    ** 015 ICP 20250123 Used integer.of and int64.of to leverage caches instances.
    */
    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program is distributed in the hope that it will be useful,
    ** but WITHOUT ANY WARRANTY; without even the implied warranty of
    ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    ** GNU Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.util;

    import java.math.*;

    /**
     * A class that provides Progress 4GL compatible operators and functions that have dynamic
     * type behavior. This class provides all functionality as statics and has no instance data.
     */
    public final class DynamicOps
    {
       /** character type in Progress */
       static final String CHAR_TYPE = "character";

       /** Data types to return from dynamic function. */
       static enum ReturnType
       {
          INTEGER, DECIMAL, INT64
       }

       /**
        * Returns the largest instance from the list.  All the values must be assignment-compatible.
        * If they are not, then {@link #IncompatibleDataTypesInExpression()} is executed.
        *
        * @param    args
        *           The list of instances to compare.
        *
        * @return   The largest instance or <code>unknown value</code> if any entries in the list are
        *           unknown or <code>null</code> if an empty list is passed.
        */
       public static BaseDataType maximum(BaseDataType... args)
       {
          if (args == null || args.length == 0)
          {
             return new unknown();
          }

          if (args.length == 1)
          {
             return args[0].val();
          }

          // the argument may be a proxy; make sure to unwrap and work with the real value
          Class<?> type = args[0].val().getClass();
          BaseDataType max = args[0].val();
          BaseDataType val = BaseDataType.generateUnknown(type);
          for (int i = 1; i < args.length; i++)
          {
             if (!compatibleDataTypes(type, args[i].val().getClass()))
             {
                IncompatibleDataTypesInExpression();
                break;
             }

             val.assign(args[i].val());

             if (CompareOps._isGreaterThan(val, max))
             {
                max.assign(val);
             }
          }

          return max;
       }

       /**
        * Returns the smallest instance from the list.  All the values must be assignment-compatible.
        * If they are not, then {@link #IncompatibleDataTypesInExpression()} is executed.
        *
        * @param    args
        *           The list of instances to compare.
        *
        * @return   The smallest instance or <code>unknown value</code> if any entries in the list are
        *           unknown or <code>null</code> if an empty list is passed.
        */
       public static BaseDataType minimum(BaseDataType... args)
       {
          if (args == null || args.length == 0)
          {
             return new unknown();
          }

          if (args.length == 1)
          {
             return args[0].val();
          }

          // the argument may be a proxy; make sure to unwrap and work with the real value
          Class<?> type = args[0].val().getClass();
          BaseDataType min = args[0].val();
          BaseDataType val = BaseDataType.generateUnknown(type);
          for (int i = 1; i < args.length; i++)
          {
             if (!compatibleDataTypes(type, args[i].val().getClass()))
             {
                IncompatibleDataTypesInExpression();
                break;
             }

             val.assign(args[i].val());

             if (CompareOps._isLessThan(val, min))
             {
                min.assign(val);
             }
          }

          return min;
       }

       /**
        * Implements the binary plus operator whose result will differ based on the types of the
        * operands, which will not be known until runtime.
        * <p>
        * If either operand is the <code>unknown value</code>, the result will
        * be the <code>unknown value</code>.
        *
        * @param    op1
        *           The left operand.
        * @param    op2
        *           The right operand.
        *
        * @return   The dynamically determined result.
        */
       public static BaseDataType plus(BaseDataType op1, BaseDataType op2)
       {
          // the operands may be proxy; make sure to unwrap and work with the real values
          op1 = op1 == null ? null : op1.val();
          op2 = op2 == null ? null : op2.val();

          // allowed types
          if (!(((op1 instanceof Text)       ||
                 (op1 instanceof NumberType) ||
                 (op1 instanceof date)       ||
                 (op1 instanceof handle)     ||
                 (op1 instanceof logical)) &&
                ((op2 instanceof Text)       ||
                 (op2 instanceof NumberType) ||
                 (op2 instanceof date)       ||
                 (op2 instanceof handle)     ||
                 (op2 instanceof logical))))
          {
             IncompatibleDataTypesInExpression();
             return new unknown();
          }

          // check for unknown results
          if (op1.isUnknown() || op2.isUnknown())
          {
             return op1.instantiateUnknown();
          }

          // P4G restrictions.
          // handle or logical are valid only with Text values.
          if (!(op1 instanceof Text) && (op2 instanceof handle || op2 instanceof logical))
          {
             IncompatibleDataTypesInExpression();
             return new unknown();
          }
          else if (!(op2 instanceof Text) && (op1 instanceof handle || op1 instanceof logical))
          {
             IncompatibleDataTypesInExpression();
             return new unknown();
          }
          // datetime and datetimetz cannot be used.
          else if (op1 instanceof datetime || op2 instanceof datetime)
          {
             IncompatibleDataTypesInExpression();
             return new unknown();
          }
          // any date and date not allowed.
          else if (op1 instanceof date && op2 instanceof date)
          {
             IncompatibleDataTypesInExpression();
             return new unknown();
          }

          // at least one operand is longchar type.
          if (op1 instanceof longchar || op2 instanceof longchar)
          {
             String val1 = new longchar(op1).toStringMessage();
             String val2 = new longchar(op2).toStringMessage();

             // concatenate values
             StringBuilder sb = new StringBuilder(val1);
             sb.append(val2);
             return new longchar(sb.toString());
          }
          // at least one operand is character type.
          else if (op1 instanceof character || op2 instanceof character)
          {
             String val1 = new character(op1).toStringMessage();
             String val2 = new character(op2).toStringMessage();

             // concatenate values
             StringBuilder sb = new StringBuilder(val1);
             sb.append(val2);
             return new character(sb.toString());
          }
          // at least one operand is date type.
          else if (op1 instanceof date || op2 instanceof date)
          {
             long val1 = new date(op1).longValue();
             long val2 = new date(op2).longValue();
             return new date(val1 + val2);
          }
          // at least one operand is decimal type.
          else if (op1 instanceof decimal || op2 instanceof decimal)
          {
             BigDecimal val1 = new decimal(op1).toBigDecimal();
             BigDecimal val2 = new decimal(op2).toBigDecimal();
             return new decimal(val1.add(val2));
          }
          // at least one operand is int64 type.
          else if (op1 instanceof int64 || op2 instanceof int64)
          {
             long val1 = new int64(op1).longValue();
             long val2 = new int64(op2).longValue();
             return int64.of(val1 + val2);
          }
          // at least one operand is integer type.
          else if (op1 instanceof integer || op2 instanceof integer)
          {
             long val1 = new integer(op1).longValue();
             long val2 = new integer(op2).longValue();
             return integer.of(val1 + val2);
          }
          // at least one operand is recid type.
          else if (op1 instanceof recid || op2 instanceof recid)
          {
             long val1 = new recid(op1).longValue();
             long val2 = new recid(op2).longValue();
             return new recid(val1 + val2);
          }
          else
          {
             IncompatibleDataTypesInExpression();
          }

          return new unknown();
       }

       /**
        * Implements the minus binary operator whose result will differ based on the types of the
        * operands, which will not be known until runtime.
        * <p>
        * If either operand is the <code>unknown value</code>, the result will
        * be the <code>unknown value</code>.
        *
        * @param    op1
        *           The left operand.
        * @param    op2
        *           The right operand.
        *
        * @return   The dynamically determined result.
        */
       public static BaseDataType minus(BaseDataType op1, BaseDataType op2)
       {
          // the operands may be proxy; make sure to unwrap and work with the real values
          op1 = op1 == null ? null : op1.val();
          op2 = op2 == null ? null : op2.val();

          // allowed BDT types
          if (!(((op1 instanceof NumberType) ||
                 (op1 instanceof date)       ||
                 (op1 instanceof handle)     ||
                 (op1 instanceof logical)) &&
                ((op2 instanceof NumberType) ||
                 (op2 instanceof date)       ||
                 (op2 instanceof handle)     ||
                 (op2 instanceof logical))))
          {
             IncompatibleDataTypesInExpression();
             return new unknown();
          }

          // check for unknown results
          if (op1.isUnknown() || op2.isUnknown())
          {
             return op1.instantiateUnknown();
          }

          // P4G restrictions.
          // datetime and datetimetz cannot be used.
          if (op1 instanceof datetime || op2 instanceof datetime)
          {
             IncompatibleDataTypesInExpression();
             return new unknown();
          }
          // handle or logical works only with decimals.
          else if (!(op1 instanceof decimal) && (op2 instanceof handle || op2 instanceof logical))
          {
             IncompatibleDataTypesInExpression();
             return new unknown();
          }
          else if (!(op2 instanceof decimal) && (op1 instanceof handle || op1 instanceof logical))
          {
             IncompatibleDataTypesInExpression();
             return new unknown();
          }
          // date as op2 works only with date or decimal as op1
          else if (op2 instanceof date && !(op1 instanceof date || op1 instanceof decimal))
          {
             IncompatibleDataTypesInExpression();
             return new unknown();
          }

          // at least one operand is decimal type.
          if (op1 instanceof decimal || op2 instanceof decimal)
          {
             BigDecimal val1 = new decimal(op1).toBigDecimal();
             BigDecimal val2 = new decimal(op2).toBigDecimal();
             return new decimal(val1.subtract(val2));
          }
          // both operands are date but the return type is decimal!!!
          else if (op1 instanceof date && op2 instanceof date)
          {
             long val1 = new date(op1).longValue();
             long val2 = new date(op2).longValue();
             return new decimal(val1 - val2);
          }
          // only op1 could be of type date
          else if (op1 instanceof date)
          {
             long val1 = new date(op1).longValue();
             long val2 = new date(op2).longValue();
             return new date(val1 - val2);
          }
          // at least one operand is int64 type.
          else if (op1 instanceof int64 || op2 instanceof int64)
          {
             long val1 = new int64(op1).longValue();
             long val2 = new int64(op2).longValue();
             return int64.of(val1 - val2);
          }
          // at least one operand is integer type.
          else if (op1 instanceof integer || op2 instanceof integer)
          {
             long val1 = new integer(op1).longValue();
             long val2 = new integer(op2).longValue();
             return integer.of(val1 - val2);
          }
          // at least one operand is recid type.
          else if (op1 instanceof recid || op2 instanceof recid)
          {
             long val1 = new recid(op1).longValue();
             long val2 = new recid(op2).longValue();
             return new recid(val1 - val2);
          }
          else
          {
             IncompatibleDataTypesInExpression();
          }

          return new unknown();
       }

       /**
        * Error message to display for incompatible types.
        */
       private static void IncompatibleDataTypesInExpression()
       {
          String err = "Incompatible data types in expression or assignment.";
          ErrorManager.recordOrThrowError(223, err);
       }

       /**
        * Returns the length of the given <code>BaseDataType</code> instance. This method is called
        * when P2J is unable to resolve the type of the first parameter at conversion time and this
        * is postponed to runtime when the actual type of <code>bdt</code> will be determined
        * dynamically.
        *
        * @param   bdt
        *          The instance to test.
        * @param   type
        *          The measuring unit (CHARACTER or RAW, COLUMN not supported yet).
        *
        * @return  The number of characters or bytes in the given instance or <code>unknown</code>
        *          value  if the instance is <code>unknown</code>.
        */
       public static integer length(BaseDataType bdt, character type)
       {
          // the value may be a proxy; make sure to unwrap and work with the real value
          bdt = bdt == null ? null : bdt.val();

          if (type == null || type.isUnknown())
          {
             return integer.UNKNOWN;
          }

          return DynamicOps.length(bdt, type.toStringMessage());
       }

       /**
        * Returns the length of the given <code>BaseDataType</code> instance. This method is called
        * when P2J is unable to resolve the type of the first parameter at conversion time and this
        * is postponed to runtime when the actual type of <code>bdt</code> will be determined
        * dynamically.
        *
        * @param   bdt
        *          The instance to test.
        * @param   type
        *          The measuring unit (CHARACTER or RAW, COLUMN not supported yet).
        *          "character" is supposed to be the default measurement unit
        *           for the LENGTH builtin function (when type is null)
        *
        * @return  The number of characters or bytes in the given instance or <code>unknown</code>
        *          value  if the instance is <code>unknown</code>.
        */
       public static integer length(BaseDataType bdt, String type)
       {
          // the value may be a proxy; make sure to unwrap and work with the real value
          bdt = bdt == null ? null : bdt.val();

          if (bdt == null || bdt.isUnknown())
          {
             return integer.UNKNOWN;
          }

          if (type == null)
          {
             type = CHAR_TYPE;
          }

          if (bdt instanceof Text)
          {
             Text txt = (Text) bdt;
             return TextOps.length(txt, type);
          }
          else if (bdt instanceof BinaryData)
          {
             return new integer(BinaryData.length((BinaryData) bdt));
          }
          else if (bdt instanceof logical)
          {
             if (((logical) bdt).booleanValue())
             {
                return integer.of(3);
             }
             else
             {
                return integer.of(2);
             }
          }
          else if (bdt instanceof datetimetz)
          {
             ErrorManager.recordOrThrowError(5730,
                     "Unable to complete automatic conversion to character -- use STRING function", false);
          }
          else if (bdt instanceof NumberType || bdt instanceof date || bdt instanceof handle)
          {
             return integer.of(bdt.toStringExport().length());
          }
          else if (bdt instanceof rowid)
          {
             ErrorManager.recordOrThrowError(5729,
                     "Incompatible datatypes found during runtime conversion", false);
          }

          // none of the above cases ?
          return integer.UNKNOWN;
       }

       /**
        * Returns the length of the given <code>BaseDataType</code> instance. This method is called
        * when P2J is unable to resolve the type of the first parameter at conversion time and this
        * is postponed to runtime when the actual type of <code>bdt</code> will be determined
        * dynamically.
        *
        * @param   bdt
        *          The instance to test.
        *
        * @return  The number of characters or bytes in the given instance or <code>unknown</code>
        *          value  if the instance is <code>unknown</code>.
        */
       public static integer length(BaseDataType bdt)
       {
          // the value may be a proxy; make sure to unwrap and work with the real value
          bdt = bdt == null ? null : bdt.val();
          // if [cpinternal] is set to [UTF-8] and the [type] is invalid, the following error condition is thrown:
          //       ** The data type argument value must be "raw", "character" or "column". (1186)
          return DynamicOps.length(bdt, CHAR_TYPE);
       }

       /**
        * Implements the binary divide operator whose result will differ based on the types of the
        * operands, which will not be known until runtime.
        * <p>
        * If either operand is the <code>unknown value</code>, the result will
        * be the <code>unknown value</code>.
        *
        * @param    op1
        *           The left operand.
        * @param    op2
        *           The right operand.
        *
        * @return   The dynamically determined result.
        */
       public static decimal divide(BaseDataType op1, BaseDataType op2)
       {
          // the operands may be proxy; make sure to unwrap and work with the real values
          op1 = op1 == null ? null : op1.val();
          op2 = op2 == null ? null : op2.val();

          // try to transform character values to number one
          if (op1 instanceof character)
          {
             op1 = characterToNumber(op1, ReturnType.DECIMAL);
          }
          if (op2 instanceof character)
          {
             op2 = characterToNumber(op2, ReturnType.DECIMAL);
          }

          // allowed types
          if (!(op1 instanceof NumberType && op2 instanceof NumberType))
          {
             IncompatibleDataTypesInExpression();
             return new decimal();
          }

          // check for unknown results
          if (op1.isUnknown() || op2.isUnknown())
          {
             return new decimal();
          }

          return MathOps.divide(((NumberType) op1), ((NumberType) op2));
       }

       /**
        * Implements the binary multiply operator whose result will differ based on the types of the
        * operands, which will not be known until runtime.
        * <p>
        * If either operand is the <code>unknown value</code>, the result will
        * be the <code>unknown value</code>.
        *
        * @param    op1
        *           The left operand.  If it is character attempt to convert to numeric happens.
        * @param    op2
        *           The right operand.
        *
        * @return   The dynamically determined result.
        */
       public static NumberType multiply(NumberType op1, NumberType op2)
       {
          // the values may be proxy; make sure to unwrap and work with the real values
          return multiply(op1 == null ? null : op1.val(), op2 == null ? null : op2.val());
       }

       /**
        * Implements the binary multiply operator whose result will differ based on the types of the
        * operands, which will not be known until runtime.
        * <p>
        * If either operand is the <code>unknown value</code>, the result will
        * be the <code>unknown value</code>.
        *
        * @param    op1
        *           The left operand.  If it is character attempt to convert to numeric happens.
        * @param    op2
        *           The right operand.
        *
        * @return   The dynamically determined result.
        */
       public static NumberType multiply(BaseDataType op1, NumberType op2)
       {
          // the value may be a proxy; make sure to unwrap and work with the real value
          op1 = op1 == null ? null : op1.val();
          BaseDataType val2 = op2 == null ? null : op2.val();

          // try to transform character values to number one
          if (op1 instanceof character)
          {
             if (val2 instanceof decimal)
             {
                op1 = characterToNumber(op1, ReturnType.DECIMAL);
             }
             else if (val2 instanceof integer)
             {
                op1 = characterToNumber(op1, ReturnType.INTEGER);
             }
          }

          // and call the base version
          return multiply(op1, val2);
       }

       /**
        * Implements the binary multiply operator whose result will differ based on the types of the
        * operands, which will not be known until runtime.
        * <p>
        * If either operand is the <code>unknown value</code>, the result will
        * be the <code>unknown value</code>.
        *
        * @param    op1
        *           The left operand.
        * @param    op2
        *           The right operand.  If it is character attempt to convert to numeric happens.
        *
        * @return   The dynamically determined result.
        */
       public static NumberType multiply(NumberType op1, BaseDataType op2)
       {
          // the value may be a proxy; make sure to unwrap and work with the real value
          BaseDataType val1 = op1 == null ? null : op1.val();
          op2 = op2 == null ? null : op2.val();

          // try to transform character values to number one
          if (op2 instanceof character)
          {
             if (val1 instanceof decimal)
             {
                op2 = characterToNumber(op2, ReturnType.DECIMAL);
             }
             else if (val1 instanceof integer)
             {
                op2 = characterToNumber(op2, ReturnType.INTEGER);
             }
          }

          // and call the base version
          return multiply(val1, op2);
       }

       /**
        * Implements the binary multiply operator whose result will differ based on the types of the
        * operands, which will not be known until runtime.
        * <p>
        * If either operand is the <code>unknown value</code>, the result will
        * be the <code>unknown value</code>.
        *
        * @param    op1
        *           The left operand.
        * @param    op2
        *           The right operand.
        *
        * @return   The dynamically determined result.
        */
       public static NumberType multiply(BaseDataType op1, BaseDataType op2)
       {
          // the values may be proxy; make sure to unwrap and work with the real values
          op1 = op1 == null ? null : op1.val();
          op2 = op2 == null ? null : op2.val();

          // allowed types
          if (!(op1 instanceof NumberType && op2 instanceof NumberType))
          {
             IncompatibleDataTypesInExpression();
             return new decimal();
          }

          // check for unknown results
          if (op1.isUnknown() || op2.isUnknown())
          {
             return new decimal();
          }

          return MathOps.multiply(((NumberType) op1), ((NumberType) op2));
       }

       /**
        * Implements the binary modulo operator whose result will differ based on the types of the
        * operands, which will not be known until runtime.
        * <p>
        * If either operand is the <code>unknown value</code>, the result will
        * be the <code>unknown value</code>.
        *
        * @param    op1
        *           The left operand.
        * @param    op2
        *           The right operand.
        *
        * @return   The dynamically determined result.
        */
       public static NumberType modulo(BaseDataType op1, BaseDataType op2)
       {
          // the values may be proxy; make sure to unwrap and work with the real values
          op1 = op1 == null ? null : op1.val();
          op2 = op2 == null ? null : op2.val();

          // try to transform character values to number one
          if (op1 instanceof character)
          {
             op1 = characterToNumber(op1, ReturnType.INTEGER);
          }
          if (op2 instanceof character)
          {
             op2 = characterToNumber(op2, ReturnType.INTEGER);
          }

          // allowed types
          if (!(op1 instanceof NumberType && op2 instanceof NumberType))
          {
             IncompatibleDataTypesInExpression();
             return new decimal();
          }

          // check for unknown results
          if (op1.isUnknown() || op2.isUnknown())
          {
             return new decimal();
          }

          return MathOps.modulo(((NumberType) op1), ((NumberType) op2));
       }

       /**
        * Implements the unary negate operator whose result will differ based on the types of the
        * operands, which will not be known until runtime.
        * <p>
        * If either operand is the <code>unknown value</code>, the result will
        * be the <code>unknown value</code>.
        *
        * @param    op
        *           The operand.
        *
        * @return   The dynamically determined result.
        */
       public static NumberType negate(BaseDataType op)
       {
          // the operand may be a proxy; make sure to unwrap and work with the real value
          op = op == null ? null : op.val();
          // allowed types
          if (!(op instanceof NumberType) || op.isUnknown())
          {
             IncompatibleDataTypesInExpression();
             return new decimal();
          }

          if (op instanceof decimal)
          {
             return MathOps.negate((decimal) op);
          }
          else
          {
             return MathOps.negate((int64) op);
          }
       }

       /**
        * Helper method to make transformation attempt from character data type into numeric one.
        * <p>
        * Can be used as last resort if for some reasons operant types are not compatible.
        *
        * @param    bdt
        *           The value to convert.
        * @param    type
        *           The converted variable type.
        *
        * @return   The new value of number type if conversion is possible or original value if not.
        */
       private static BaseDataType characterToNumber(BaseDataType bdt, ReturnType type)
       {
          if (bdt.isUnknown())
          {
             return bdt;
          }

          BaseDataType bdtRet = null;

          // trying to convert non-emtpty string representation to decimal
          // depending on data type requested
          String bdtString = bdt.toStringMessage();
          if (!bdtString.isEmpty())
          {
             switch (type)
             {
                case DECIMAL:
                {
                   bdtRet = new decimal(bdtString);
                   break;
                }
                case INTEGER:
                {
                   bdtRet = new integer(bdtString);
                   break;
                }
             }
          }

          return bdtRet != null ? bdtRet : bdt;
       }

       /**
        * Check if the given 4GL data types are assignment-compatible.
        *
        * @param    bdt1
        *           The first data type to check.
        * @param    bdt2
        *           The first data type to check.
        *
        * @return   <code>true</code> if the types are compatible.
        */
       private static boolean compatibleDataTypes(Class<?> bdt1, Class<?> bdt2)
       {
          if (!(BaseDataType.class.isAssignableFrom(bdt1) &&
                BaseDataType.class.isAssignableFrom(bdt2)))
          {
             return false;
          }
          if (bdt1 == bdt2 || bdt2.isAssignableFrom(bdt1) || bdt1.isAssignableFrom(bdt2))
          {
             return true;
          }

          if (BinaryData.class.isAssignableFrom(bdt1) && BinaryData.class.isAssignableFrom(bdt2))
          {
             return true;
          }

          if (date.class.isAssignableFrom(bdt1) && date.class.isAssignableFrom(bdt2))
          {
             return true;
          }

          if (NumberType.class.isAssignableFrom(bdt1) && NumberType.class.isAssignableFrom(bdt2))
          {
             return true;
          }

          if (Text.class.isAssignableFrom(bdt1) && Text.class.isAssignableFrom(bdt2))
          {
             return true;
          }

          if ((NumberType.class.isAssignableFrom(bdt1) && date.class.isAssignableFrom(bdt2)) ||
              (date.class.isAssignableFrom(bdt1) && NumberType.class.isAssignableFrom(bdt2)))
          {
             return true;
          }

          return false;
       }
    }