Skip to content
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ object FunctionRegistry {
expression[Substring]("substr"),
expression[Substring]("substring"),
expression[Upper]("ucase"),
expression[UnHex]("unhex"),
expression[Upper]("upper")
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,67 @@ case class Pow(left: Expression, right: Expression)
}
}

/**
* Performs the inverse operation of HEX.
* Resulting characters are returned as a byte array.
*/
case class UnHex(child: Expression) extends UnaryExpression with Serializable {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should just implement ExpectsInputTypes, and use BinaryType as the only type.

There will be a rule in #7175 to do implicit type casting from null type to expected type(s).


override def dataType: DataType = BinaryType

override def checkInputDataTypes(): TypeCheckResult = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and remove this once the expression extends ExpectsInputTypes

if (child.dataType.isInstanceOf[StringType] || child.dataType == NullType) {
TypeCheckResult.TypeCheckSuccess
} else {
TypeCheckResult.TypeCheckFailure(s"unHex accepts String type, not ${child.dataType}")
}
}

override def eval(input: InternalRow): Any = {
val num = child.eval(input)
if (num == null) {
null
} else {
unhex(num.asInstanceOf[UTF8String].getBytes)
}
}

private val hexDigits = {
val array = Array.fill[Byte](128)(-1)
(0 to 9).foreach(i => array('0' + i) = i.toByte)
(0 to 5).foreach(i => array('A' + i) = (i + 10).toByte)
(0 to 5).foreach(i => array('a' + i) = (i + 10).toByte)
array
}

private def toDigit(b: Byte): Byte = {
val digit = hexDigits(b)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a val of UnHex.

if (digit == -1) {
throw new NumberFormatException(s"invalid hex number $b")
}
digit
}

private def unhex(inputBytes: Array[Byte]): Array[Byte] = {
var bytes = inputBytes
if ((bytes.length & 0x01) != 0) {
bytes = 48.toByte +: bytes // padding with '0'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can padding with 0 without allocation. you can use '0'.toByte

}
val out = new Array[Byte](bytes.length >> 1)
// two characters form the hex value.
var i = 0
while (i < bytes.length) {
try {
out(i / 2) = ((toDigit(bytes(i)) << 4) | toDigit(bytes(i + 1)) & 0xFF).toByte
i += 2
} catch {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

case _: NumberFormatException => return null
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could check the value from toDigit here without throwing exceptions (Using exception as control flow is slow).

}
}
out
}
}

case class Hypot(left: Expression, right: Expression)
extends BinaryMathExpression(math.hypot, "HYPOT")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,12 @@ class MathFunctionsSuite extends SparkFunSuite with ExpressionEvalHelper {
// scalastyle:on
}

test("unhex") {
checkEvaluation(UnHex(Literal("737472696E67")), "string".getBytes)
checkEvaluation(UnHex(Literal("")), new Array[Byte](0))
checkEvaluation(UnHex(Literal("0")), Array[Byte](0))
}

test("hypot") {
testBinary(Hypot, math.hypot)
}
Expand Down
18 changes: 18 additions & 0 deletions sql/core/src/main/scala/org/apache/spark/sql/functions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1062,6 +1062,24 @@ object functions {
*/
def hex(colName: String): Column = hex(Column(colName))

/**
* Inverse of hex. Interprets each pair of characters as a hexadecimal number
* and converts to the byte representation of number.
*
* @group math_funcs
* @since 1.5.0
*/
def unhex(column: Column): Column = UnHex(column.expr)

/**
* Inverse of hex. Interprets each pair of characters as a hexadecimal number
* and converts to the byte representation of number.
*
* @group math_funcs
* @since 1.5.0
*/
def unhex(colName: String): Column = unhex(Column(colName))

/**
* Computes `sqrt(a^2^ + b^2^)` without intermediate overflow or underflow.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,15 @@ class MathExpressionsSuite extends QueryTest {
checkAnswer(data.selectExpr("hex(cast(d as binary))"), Seq(Row("68656C6C6F")))
}

test("unhex") {
val data = Seq(("1C", "737472696E67")).toDF("a", "b")
checkAnswer(data.select(unhex('a)), Row(Array[Byte](28.toByte)))
checkAnswer(data.select(unhex('b)), Row("string".getBytes))
checkAnswer(data.selectExpr("unhex(a)"), Row(Array[Byte](28.toByte)))
checkAnswer(data.selectExpr("unhex(b)"), Row("string".getBytes))
checkAnswer(data.selectExpr("""unhex("##")"""), Row(null))
}

test("hypot") {
testTwoToOneMathFunction(hypot, hypot, math.hypot)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,7 @@ class HiveCompatibilitySuite extends HiveQueryFileTest with BeforeAndAfter {
"udf_trim",
"udf_ucase",
"udf_unix_timestamp",
"udf_unhex",
"udf_upper",
"udf_var_pop",
"udf_var_samp",
Expand Down