Skip to content

Commit f6623d6

Browse files
committed
Implement PostgreSQL money type.
fixes #1077
1 parent f614899 commit f6623d6

File tree

8 files changed

+301
-2
lines changed

8 files changed

+301
-2
lines changed

vertx-pg-client/README.adoc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,12 @@ The *Reactive Postgres Client* currently supports the following data types
215215
|`io.vertx.pgclient.data.Inet[]`
216216
|✔
217217

218+
|`MONEY`
219+
|`io.vertx.pgclient.data.Money`
220+
|✔
221+
|`io.vertx.pgclient.data.Money[]`
222+
|✔
223+
218224
|`PATH`
219225
|`i.r.p.data.Path`
220226
|✔

vertx-pg-client/src/main/asciidoc/index.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ Currently the client supports the following PostgreSQL types
308308
* TSVECTOR (`java.lang.String`)
309309
* TSQUERY (`java.lang.String`)
310310
* INET (`io.vertx.pgclient.data.Inet`)
311+
* MONEY (`io.vertx.pgclient.data.Money`)
311312

312313
Tuple decoding uses the above types when storing values, it also performs on the flu conversion the actual value when possible:
313314

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright (c) 2011-2021 Contributors to the Eclipse Foundation
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
7+
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
8+
*
9+
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
10+
*/
11+
package io.vertx.pgclient.data;
12+
13+
import java.math.BigDecimal;
14+
import java.math.BigInteger;
15+
16+
/**
17+
* The PostgreSQL <a href="https://www.postgresql.org/docs/9.1/datatype-money.html">MONEY</> type.
18+
*
19+
* This has the {@link #getIntegerPart() integer part} and {@link #getDecimalPart() decimal part} of the value without loss of information.
20+
*
21+
* {@link #bigDecimalValue()} returns the value without loss of information
22+
* {@link #doubleValue()} ()} returns the value possible loss of information
23+
*/
24+
public class Money {
25+
26+
private long integerPart;
27+
private int decimalPart;
28+
29+
public Money(long integerPart, int decimalPart) {
30+
setIntegerPart(integerPart);
31+
setDecimalPart(decimalPart);
32+
}
33+
34+
public Money(Number value) {
35+
if (value instanceof Double || value instanceof Float) {
36+
value = BigDecimal.valueOf((double) value);
37+
}
38+
if (value instanceof BigDecimal) {
39+
BigInteger bd = ((BigDecimal) value).multiply(new BigDecimal(100)).toBigInteger();
40+
setIntegerPart(bd.divide(BigInteger.valueOf(100)).longValueExact());
41+
setDecimalPart(bd.remainder(BigInteger.valueOf(100)).abs().intValueExact());
42+
} else {
43+
setIntegerPart(value.longValue());
44+
}
45+
}
46+
47+
public Money() {
48+
}
49+
50+
public long getIntegerPart() {
51+
return integerPart;
52+
}
53+
54+
public int getDecimalPart() {
55+
return decimalPart;
56+
}
57+
58+
/**
59+
* Set the integer part of the monetary value.
60+
*
61+
* <p> This value must belong to the range {@code ]Long.MAX_VALUE / 100, Long.MIN_VALUE / 100[}
62+
*
63+
* @param part the integer part of the value
64+
* @return this object
65+
*/
66+
public Money setIntegerPart(long part) {
67+
if (part > Long.MAX_VALUE / 100 || part < Long.MIN_VALUE / 100) {
68+
throw new IllegalArgumentException();
69+
}
70+
integerPart = part;
71+
return this;
72+
}
73+
74+
/**
75+
* Set the decimal part of the monetary value.
76+
*
77+
* <p> This value must belong to the range {@code [0, 100]}
78+
*
79+
* @param part decimal part
80+
* @return this object
81+
*/
82+
public Money setDecimalPart(int part) {
83+
if (part > 99 || part < 0) {
84+
throw new IllegalArgumentException();
85+
}
86+
decimalPart = part;
87+
return this;
88+
}
89+
90+
/**
91+
* @return the monetary amount as a big decimal without loss of information
92+
*/
93+
public BigDecimal bigDecimalValue() {
94+
BigDecimal value = new BigDecimal(integerPart).multiply(BigDecimal.valueOf(100));
95+
if (integerPart >= 0) {
96+
value = value.add(BigDecimal.valueOf(decimalPart));
97+
} else {
98+
value = value.subtract(BigDecimal.valueOf(decimalPart));
99+
}
100+
return value;
101+
}
102+
103+
/**
104+
* @return the monetary amount as a double with possible loss of information
105+
*/
106+
public double doubleValue() {
107+
return bigDecimalValue().doubleValue();
108+
}
109+
110+
@Override
111+
public boolean equals(Object o) {
112+
if (this == o) return true;
113+
if (o == null || getClass() != o.getClass()) return false;
114+
Money that = (Money) o;
115+
return decimalPart == that.decimalPart && integerPart == that.integerPart;
116+
}
117+
118+
@Override
119+
public int hashCode() {
120+
return ((Long)integerPart).hashCode() ^ ((Integer)decimalPart).hashCode();
121+
}
122+
123+
@Override
124+
public String toString() {
125+
return "Money(" + integerPart + "." + decimalPart + ")";
126+
}
127+
}

vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/DataType.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import io.vertx.pgclient.data.Inet;
2828
import io.vertx.pgclient.data.Line;
2929
import io.vertx.pgclient.data.LineSegment;
30+
import io.vertx.pgclient.data.Money;
3031
import io.vertx.sqlclient.Tuple;
3132
import io.vertx.sqlclient.data.Numeric;
3233
import io.vertx.pgclient.data.Interval;
@@ -63,8 +64,8 @@ public enum DataType {
6364
FLOAT8_ARRAY(1022, true, Double[].class, Number[].class, JDBCType.DOUBLE, Tuple::getArrayOfDoubles),
6465
NUMERIC(1700, false, Numeric.class, Number.class, JDBCType.NUMERIC, Tuple::getNumeric),
6566
NUMERIC_ARRAY(1231, false, Numeric[].class, Number[].class, JDBCType.NUMERIC, Tuple::getArrayOfNumerics),
66-
MONEY(790, true, Object.class, null),
67-
MONEY_ARRAY(791, true, Object[].class, null),
67+
MONEY(790, true, Money.class, null),
68+
MONEY_ARRAY(791, true, Money[].class, null),
6869
BIT(1560, true, Object.class, JDBCType.BIT),
6970
BIT_ARRAY(1561, true, Object[].class, JDBCType.BIT),
7071
VARBIT(1562, true, Object.class, JDBCType.OTHER),

vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/DataTypeCodec.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ public class DataTypeCodec {
9090
private static final LocalDateTime LOCAL_DATE_TIME_EPOCH = LocalDateTime.of(2000, 1, 1, 0, 0, 0);
9191
private static final OffsetDateTime OFFSET_DATE_TIME_EPOCH = LocalDateTime.of(2000, 1, 1, 0, 0, 0).atOffset(ZoneOffset.UTC);
9292
private static final Inet[] empty_inet_array = new Inet[0];
93+
private static final Money[] empty_money_array = new Money[0];
9394

9495
// Sentinel used when an object is refused by the data type
9596
public static final Object REFUSED_SENTINEL = new Object();
@@ -119,6 +120,7 @@ public class DataTypeCodec {
119120
private static final IntFunction<Circle[]> CIRCLE_ARRAY_FACTORY = size -> size == 0 ? empty_circle_array : new Circle[size];
120121
private static final IntFunction<Interval[]> INTERVAL_ARRAY_FACTORY = size -> size == 0 ? empty_interval_array : new Interval[size];
121122
private static final IntFunction<Inet[]> INET_ARRAY_FACTORY = size -> size == 0 ? empty_inet_array : new Inet[size];
123+
private static final IntFunction<Money[]> MONEY_ARRAY_FACTORY = size -> size == 0 ? empty_money_array : new Money[size];
122124

123125
private static final java.time.format.DateTimeFormatter TIMETZ_FORMAT = new DateTimeFormatterBuilder()
124126
.parseCaseInsensitive()
@@ -352,6 +354,12 @@ public static void encodeBinary(DataType id, Object value, ByteBuf buff) {
352354
case INET_ARRAY:
353355
binaryEncodeArray((Inet[]) value, DataType.INET, buff);
354356
break;
357+
case MONEY:
358+
binaryEncodeMoney((Money) value, buff);
359+
break;
360+
case MONEY_ARRAY:
361+
binaryEncodeArray((Money[]) value, DataType.MONEY, buff);
362+
break;
355363
default:
356364
logger.debug("Data type " + id + " does not support binary encoding");
357365
defaultEncodeBinary(value, buff);
@@ -485,6 +493,10 @@ public static Object decodeBinary(DataType id, int index, int len, ByteBuf buff)
485493
return binaryDecodeInet(index, len, buff);
486494
case INET_ARRAY:
487495
return binaryDecodeArray(INET_ARRAY_FACTORY, DataType.INET, index, len, buff);
496+
case MONEY:
497+
return binaryDecodeMoney(index, len, buff);
498+
case MONEY_ARRAY:
499+
return binaryDecodeArray(MONEY_ARRAY_FACTORY, DataType.MONEY, index, len, buff);
488500
default:
489501
logger.debug("Data type " + id + " does not support binary decoding");
490502
return defaultDecodeBinary(index, len, buff);
@@ -621,6 +633,10 @@ public static Object decodeText(DataType id, int index, int len, ByteBuf buff) {
621633
return textDecodeInet(index, len, buff);
622634
case INET_ARRAY:
623635
return textDecodeArray(INET_ARRAY_FACTORY, DataType.INET, index, len, buff);
636+
case MONEY:
637+
return textDecodeMoney(index, len, buff);
638+
case MONEY_ARRAY:
639+
return textDecodeArray(MONEY_ARRAY_FACTORY, DataType.MONEY, index, len, buff);
624640
default:
625641
return defaultDecodeText(index, len, buff);
626642
}
@@ -1437,6 +1453,22 @@ private static void binaryEncodeInet(Inet value, ByteBuf buff) {
14371453
buff.writeBytes(data);
14381454
}
14391455

1456+
private static void binaryEncodeMoney(Money money, ByteBuf buff) {
1457+
long integerPart = money.getIntegerPart();
1458+
long value;
1459+
if (integerPart >= 0) {
1460+
value = money.getIntegerPart() * 100 + money.getDecimalPart();
1461+
} else {
1462+
value = money.getIntegerPart() * 100 - money.getDecimalPart();
1463+
}
1464+
binaryEncodeINT8(value, buff);
1465+
}
1466+
1467+
private static Money binaryDecodeMoney(int index, int len, ByteBuf buff) {
1468+
long value = binaryDecodeINT8(index, len, buff);
1469+
return new Money(value / 100, Math.abs(((int)value % 100)));
1470+
}
1471+
14401472
private static String binaryDecodeTsQuery(int index, int len, ByteBuf buff) {
14411473
return buff.getCharSequence(index, len, StandardCharsets.UTF_8).toString();
14421474
}
@@ -1478,6 +1510,27 @@ private static Inet textDecodeInet(int index, int len, ByteBuf buff) {
14781510
return inet;
14791511
}
14801512

1513+
private static Money textDecodeMoney(int index, int len, ByteBuf buff) {
1514+
String s = textDecodeVARCHAR(index, len, buff);
1515+
s = s.substring(1);
1516+
long integerPart = 0;
1517+
int decimalPart = 0;
1518+
int idx = 0;
1519+
char c;
1520+
while (idx < s.length() && (c = s.charAt(idx++)) != '.') {
1521+
if (c >= '0' && c <= '9') {
1522+
integerPart = integerPart * 10 + (c - '0');
1523+
}
1524+
}
1525+
while (idx < s.length()) {
1526+
c = s.charAt(idx++);
1527+
if (c >= '0' && c <= '9') {
1528+
decimalPart = decimalPart * 10 + (c - '0');
1529+
}
1530+
}
1531+
return new Money(integerPart, decimalPart);
1532+
}
1533+
14811534
/**
14821535
* Decode the specified {@code buff} formatted as an hex string starting at the buffer readable index
14831536
* with the specified {@code length} to a {@link Buffer}.

vertx-pg-client/src/test/java/io/vertx/pgclient/data/ExtendedQueryDataTypeCodecTestBase.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,39 @@ protected <T> void testGeneric(TestContext ctx, String sql, T[] expected, BiFunc
6363
}));
6464
}));
6565
}
66+
67+
protected <T> void testDecode(TestContext ctx, String sql, BiFunction<Row, Integer, T> getter, T... expected) {
68+
Async async = ctx.async();
69+
PgConnection.connect(vertx, options, ctx.asyncAssertSuccess(conn -> {
70+
conn.preparedQuery(sql).execute(
71+
ctx.asyncAssertSuccess(result -> {
72+
ctx.assertEquals(result.size(), 1);
73+
Iterator<Row> it = result.iterator();
74+
Row row = it.next();
75+
ctx.assertEquals(row.size(), expected.length);
76+
for (int idx = 0;idx < expected.length;idx++) {
77+
compare(ctx, expected[idx], getter.apply(row, idx));
78+
compare(ctx, expected[idx], row.getValue(idx));
79+
}
80+
async.complete();
81+
}));
82+
}));
83+
}
84+
85+
protected <T> void testEncode(TestContext ctx, String sql, Tuple tuple, String... expected) {
86+
Async async = ctx.async();
87+
PgConnection.connect(vertx, options, ctx.asyncAssertSuccess(conn -> {
88+
conn.preparedQuery(sql).execute(tuple,
89+
ctx.asyncAssertSuccess(result -> {
90+
ctx.assertEquals(result.size(), 1);
91+
Iterator<Row> it = result.iterator();
92+
Row row = it.next();
93+
ctx.assertEquals(row.size(), expected.length);
94+
for (int idx = 0;idx < expected.length;idx++) {
95+
compare(ctx, expected[idx], row.getString(idx));
96+
}
97+
async.complete();
98+
}));
99+
}));
100+
}
66101
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright (c) 2011-2021 Contributors to the Eclipse Foundation
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
7+
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
8+
*
9+
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
10+
*/
11+
package io.vertx.pgclient.data;
12+
13+
import io.vertx.ext.unit.TestContext;
14+
import io.vertx.sqlclient.Tuple;
15+
import org.junit.Test;
16+
17+
public class MonetaryTypeExtendedCodecTest extends ExtendedQueryDataTypeCodecTestBase {
18+
19+
@Test
20+
public void testDecodeMoney(TestContext ctx) {
21+
testDecode(ctx, "SELECT 1234.45::MONEY, (-1234.45)::MONEY", Tuple::getValue, new Money(1234, 45), new Money(-1234, 45));
22+
}
23+
24+
@Test
25+
public void testEncodeMoney(TestContext ctx) {
26+
testEncode(ctx, "SELECT ($1::MONEY)::VARCHAR, ($2::MONEY)::VARCHAR", Tuple.of(new Money(1234, 45), new Money(-1234, 45)), "$1,234.45", "-$1,234.45");
27+
}
28+
29+
@Test
30+
public void testDecodeMoneyArray(TestContext ctx) {
31+
testDecode(ctx, "SELECT '{ 1234.45, -1234.45 }'::MONEY[]", Tuple::getValue, (Object)(new Money[] { new Money(1234, 45), new Money(-1234, 45) }));
32+
}
33+
34+
@Test
35+
public void testEncodeMoneyArray(TestContext ctx) {
36+
testEncode(ctx, "SELECT (($1::MONEY[])[1])::VARCHAR", Tuple.of(new Money[] { new Money(1234, 45) }), "$1,234.45");
37+
}
38+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright (c) 2011-2021 Contributors to the Eclipse Foundation
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
7+
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
8+
*
9+
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
10+
*/
11+
package io.vertx.pgclient.data;
12+
13+
import io.vertx.ext.unit.TestContext;
14+
import io.vertx.sqlclient.Row;
15+
import io.vertx.sqlclient.Tuple;
16+
import org.junit.Test;
17+
18+
public class MoneyTypeSimpleCodecTest extends SimpleQueryDataTypeCodecTestBase {
19+
20+
@Test
21+
public void testMoney(TestContext ctx) {
22+
Money expected = new Money(1234, 56);
23+
testDecodeGeneric(ctx, "1234.56", "MONEY", "money", Tuple::getValue, Row::getValue, expected);
24+
}
25+
26+
@Test
27+
public void testNegativeMoney(TestContext ctx) {
28+
// Does not look possible with text format
29+
Money expected = new Money(1234, 56);
30+
testDecodeGeneric(ctx, "-1234.56", "MONEY", "money", Tuple::getValue, Row::getValue, expected);
31+
}
32+
33+
@Test
34+
public void testMoneyArray(TestContext ctx) {
35+
Money expected = new Money(1234, 56);
36+
testDecodeGenericArray(ctx, "ARRAY ['1234.56' :: MONEY]", "money", Tuple::getValue, Row::getValue, expected);
37+
}
38+
}

0 commit comments

Comments
 (0)