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;
}
}