提交 397d3126 authored 作者: noelgrandin's avatar noelgrandin

Issue 528: Add Oracle-compatible TO_CHAR function, patch by Daniel Gredler.

上级 46188bab
...@@ -3346,6 +3346,14 @@ This method returns a string. ...@@ -3346,6 +3346,14 @@ This method returns a string.
CALL XMLTEXT('test') CALL XMLTEXT('test')
" "
"Functions (String)","TO_CHAR","
TO_CHAR(value [, format[, nlsParam]])
","
Oracle-compatible TO_CHAR function that can format a timestamp, a number, or text.
","
CALL TO_CHAR(TIMESTAMP '2010-01-01 00:00:00', 'DD MON, YYYY')
"
"Functions (Time and Date)","CURRENT_DATE"," "Functions (Time and Date)","CURRENT_DATE","
{ CURRENT_DATE [ () ] | CURDATE() | SYSDATE | TODAY } { CURRENT_DATE [ () ] | CURDATE() | SYSDATE | TODAY }
"," ","
......
...@@ -44,6 +44,7 @@ Change Log ...@@ -44,6 +44,7 @@ Change Log
</li><li>Slightly reduce the memory cost of View metadata. </li><li>Slightly reduce the memory cost of View metadata.
</li><li>Extend support of "GRANT ALTER ANY SCHEMA TO &lt;user&gt;" to allow grantee ability to manipulate tables </li><li>Extend support of "GRANT ALTER ANY SCHEMA TO &lt;user&gt;" to allow grantee ability to manipulate tables
</li><li>Issue 532: Javadoc for ErrorCode.ROLES_AND_RIGHT_CANNOT_BE_MIXED looks wrong </li><li>Issue 532: Javadoc for ErrorCode.ROLES_AND_RIGHT_CANNOT_BE_MIXED looks wrong
</li><li>Issue 528: Add Oracle-compatible TO_CHAR function, patch by Daniel Gredler.
</li></ul> </li></ul>
<h2>Version 1.3.174 (2013-10-19)</h2> <h2>Version 1.3.174 (2013-10-19)</h2>
......
...@@ -72,7 +72,6 @@ See also <a href="build.html#providing_patches">Providing Patches</a>. ...@@ -72,7 +72,6 @@ See also <a href="build.html#providing_patches">Providing Patches</a>.
</li><li>Store all temp files in the temp directory. </li><li>Store all temp files in the temp directory.
</li><li>Don't use temp files, specially not deleteOnExit (bug 4513817: File.deleteOnExit consumes memory). </li><li>Don't use temp files, specially not deleteOnExit (bug 4513817: File.deleteOnExit consumes memory).
Also to allow opening client / server (remote) connections when using LOBs. Also to allow opening client / server (remote) connections when using LOBs.
</li><li>Sequence: add features [NO] MINVALUE, MAXVALUE, CYCLE.
</li><li>Make DDL (Data Definition) operations transactional. </li><li>Make DDL (Data Definition) operations transactional.
</li><li>Deferred integrity checking (DEFERRABLE INITIALLY DEFERRED). </li><li>Deferred integrity checking (DEFERRABLE INITIALLY DEFERRED).
</li><li>Groovy Stored Procedures: http://groovy.codehaus.org/GSQL </li><li>Groovy Stored Procedures: http://groovy.codehaus.org/GSQL
...@@ -122,7 +121,7 @@ See also <a href="build.html#providing_patches">Providing Patches</a>. ...@@ -122,7 +121,7 @@ See also <a href="build.html#providing_patches">Providing Patches</a>.
</li><li>Custom class loader to reload functions on demand. </li><li>Custom class loader to reload functions on demand.
</li><li>Test http://mysql-je.sourceforge.net/ </li><li>Test http://mysql-je.sourceforge.net/
</li><li>H2 Console: the webclient could support more features like phpMyAdmin. </li><li>H2 Console: the webclient could support more features like phpMyAdmin.
</li><li>Support Oracle functions: TRUNC, NVL2, TO_CHAR, TO_DATE, TO_NUMBER. </li><li>Support Oracle functions: TO_DATE, TO_NUMBER.
</li><li>Work on the Java to C converter. </li><li>Work on the Java to C converter.
</li><li>The HELP information schema can be directly exposed in the Console. </li><li>The HELP information schema can be directly exposed in the Console.
</li><li>Maybe use the 0x1234 notation for binary fields, see MS SQL Server. </li><li>Maybe use the 0x1234 notation for binary fields, see MS SQL Server.
......
...@@ -488,6 +488,13 @@ public class ErrorCode { ...@@ -488,6 +488,13 @@ public class ErrorCode {
*/ */
public static final int SEQUENCE_ATTRIBUTES_INVALID = 90009; public static final int SEQUENCE_ATTRIBUTES_INVALID = 90009;
/**
* The error with code <code>90010</code> is thrown when
* trying to format a timestamp or number using TO_CHAR
* with an invalid format.
*/
public static final int INVALID_TO_CHAR_FORMAT = 90010;
/** /**
* The error with code <code>22007</code> is thrown when * The error with code <code>22007</code> is thrown when
* a text can not be converted to a date, time, or timestamp constant. * a text can not be converted to a date, time, or timestamp constant.
...@@ -1884,7 +1891,7 @@ public class ErrorCode { ...@@ -1884,7 +1891,7 @@ public class ErrorCode {
public static final int JAVA_OBJECT_SERIALIZER_CHANGE_WITH_DATA_TABLE = 90141; public static final int JAVA_OBJECT_SERIALIZER_CHANGE_WITH_DATA_TABLE = 90141;
// next are 90010, 90011, 90021, 90039, // next are 90011, 90021, 90039,
// 90051, 90056, 90110, 90122, 90142 // 90051, 90056, 90110, 90122, 90142
private ErrorCode() { private ErrorCode() {
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
*/ */
package org.h2.expression; package org.h2.expression;
import static org.h2.util.ToChar.toChar;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
...@@ -85,7 +86,7 @@ public class Function extends Expression implements FunctionCall { ...@@ -85,7 +86,7 @@ public class Function extends Expression implements FunctionCall {
SPACE = 71, SUBSTR = 72, SUBSTRING = 73, UCASE = 74, LOWER = 75, UPPER = 76, POSITION = 77, TRIM = 78, SPACE = 71, SUBSTR = 72, SUBSTRING = 73, UCASE = 74, LOWER = 75, UPPER = 76, POSITION = 77, TRIM = 78,
STRINGENCODE = 79, STRINGDECODE = 80, STRINGTOUTF8 = 81, UTF8TOSTRING = 82, XMLATTR = 83, XMLNODE = 84, STRINGENCODE = 79, STRINGDECODE = 80, STRINGTOUTF8 = 81, UTF8TOSTRING = 82, XMLATTR = 83, XMLNODE = 84,
XMLCOMMENT = 85, XMLCDATA = 86, XMLSTARTDOC = 87, XMLTEXT = 88, REGEXP_REPLACE = 89, RPAD = 90, LPAD = 91, XMLCOMMENT = 85, XMLCDATA = 86, XMLSTARTDOC = 87, XMLTEXT = 88, REGEXP_REPLACE = 89, RPAD = 90, LPAD = 91,
CONCAT_WS = 92; CONCAT_WS = 92, TO_CHAR = 93;
public static final int CURDATE = 100, CURTIME = 101, DATE_ADD = 102, DATE_DIFF = 103, DAY_NAME = 104, public static final int CURDATE = 100, CURTIME = 101, DATE_ADD = 102, DATE_DIFF = 103, DAY_NAME = 104,
DAY_OF_MONTH = 105, DAY_OF_WEEK = 106, DAY_OF_YEAR = 107, HOUR = 108, MINUTE = 109, MONTH = 110, MONTH_NAME = 111, DAY_OF_MONTH = 105, DAY_OF_WEEK = 106, DAY_OF_YEAR = 107, HOUR = 108, MINUTE = 109, MONTH = 110, MONTH_NAME = 111,
...@@ -280,6 +281,7 @@ public class Function extends Expression implements FunctionCall { ...@@ -280,6 +281,7 @@ public class Function extends Expression implements FunctionCall {
addFunction("REGEXP_REPLACE", REGEXP_REPLACE, 3, Value.STRING); addFunction("REGEXP_REPLACE", REGEXP_REPLACE, 3, Value.STRING);
addFunction("RPAD", RPAD, VAR_ARGS, Value.STRING); addFunction("RPAD", RPAD, VAR_ARGS, Value.STRING);
addFunction("LPAD", LPAD, VAR_ARGS, Value.STRING); addFunction("LPAD", LPAD, VAR_ARGS, Value.STRING);
addFunction("TO_CHAR", TO_CHAR, VAR_ARGS, Value.STRING);
// date // date
addFunctionNotDeterministic("CURRENT_DATE", CURRENT_DATE, 0, Value.DATE); addFunctionNotDeterministic("CURRENT_DATE", CURRENT_DATE, 0, Value.DATE);
...@@ -1191,6 +1193,25 @@ public class Function extends Expression implements FunctionCall { ...@@ -1191,6 +1193,25 @@ public class Function extends Expression implements FunctionCall {
case LPAD: case LPAD:
result = ValueString.get(StringUtils.pad(v0.getString(), v1.getInt(), v2 == null ? null : v2.getString(), false), database.getMode().treatEmptyStringsAsNull); result = ValueString.get(StringUtils.pad(v0.getString(), v1.getInt(), v2 == null ? null : v2.getString(), false), database.getMode().treatEmptyStringsAsNull);
break; break;
case TO_CHAR:
switch(v0.getType()){
case Value.TIME:
case Value.DATE:
case Value.TIMESTAMP:
result = ValueString.get(toChar(v0.getTimestamp(), v1 == null ? null : v1.getString(), v2 == null ? null : v2.getString()), database.getMode().treatEmptyStringsAsNull);
break;
case Value.SHORT:
case Value.INT:
case Value.LONG:
case Value.DECIMAL:
case Value.DOUBLE:
case Value.FLOAT:
result = ValueString.get(toChar(v0.getBigDecimal(), v1 == null ? null : v1.getString(), v2 == null ? null : v2.getString()), database.getMode().treatEmptyStringsAsNull);
break;
default:
result = ValueString.get(v0.getString(), database.getMode().treatEmptyStringsAsNull);
}
break;
case H2VERSION: case H2VERSION:
result = ValueString.get(Constants.getVersion(), database.getMode().treatEmptyStringsAsNull); result = ValueString.get(Constants.getVersion(), database.getMode().treatEmptyStringsAsNull);
break; break;
...@@ -1777,6 +1798,10 @@ public class Function extends Expression implements FunctionCall { ...@@ -1777,6 +1798,10 @@ public class Function extends Expression implements FunctionCall {
min = 1; min = 1;
max = 2; max = 2;
break; break;
case TO_CHAR:
min = 1;
max = 3;
break;
case REPLACE: case REPLACE:
case LOCATE: case LOCATE:
case INSTR: case INSTR:
......
...@@ -37,6 +37,7 @@ ...@@ -37,6 +37,7 @@
90007=The object is already closed 90007=The object is already closed
90008=Invalid value {0} for parameter {1} 90008=Invalid value {0} for parameter {1}
90009=Unable to create or alter sequence {0} because of invalid attributes (start value {1}, min value {2}, max value {3}, increment {4}) 90009=Unable to create or alter sequence {0} because of invalid attributes (start value {1}, min value {2}, max value {3}, increment {4})
90010=Invalid TO_CHAR format {0}
90012=Parameter {0} is not set 90012=Parameter {0} is not set
90013=Database {0} not found 90013=Database {0} not found
90014=Error parsing {0} 90014=Error parsing {0}
......
...@@ -1183,6 +1183,10 @@ Returns the XML declaration." ...@@ -1183,6 +1183,10 @@ Returns the XML declaration."
XMLTEXT(valueString [, escapeNewlineBoolean]) XMLTEXT(valueString [, escapeNewlineBoolean])
"," ","
Creates an XML text element." Creates an XML text element."
"Functions (String)","TO_CHAR","
TO_CHAR(value [, format[, nlsParam]])
","
Oracle-compatible TO_CHAR function that can format a timestamp, a number, or text."
"Functions (Time and Date)","CURRENT_DATE"," "Functions (Time and Date)","CURRENT_DATE","
{ CURRENT_DATE [ () ] | CURDATE() | SYSDATE | TODAY } { CURRENT_DATE [ () ] | CURDATE() | SYSDATE | TODAY }
"," ","
......
/*
* Copyright 2004-2013 H2 Group. Multiple-Licensed under the H2 License,
* Version 1.0, and under the Eclipse Public License, Version 1.0
* (http://h2database.com/html/license.html).
* Initial Developer: Daniel Gredler
*/
package org.h2.util;
import static java.lang.Math.abs;
import java.math.BigDecimal;
import java.sql.Date;
import java.sql.Timestamp;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Currency;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;
import org.h2.constant.ErrorCode;
import org.h2.message.DbException;
/**
* Emulates Oracle's TO_CHAR function.
*/
public class ToChar {
/** The beginning of the Julian calendar. */
private static final long JULIAN_EPOCH;
static {
GregorianCalendar epoch = new GregorianCalendar(Locale.ENGLISH);
epoch.setGregorianChange(new Date(Long.MAX_VALUE));
epoch.clear();
epoch.set(4713, Calendar.JANUARY, 1, 0, 0, 0);
epoch.set(Calendar.ERA, GregorianCalendar.BC);
JULIAN_EPOCH = epoch.getTimeInMillis();
}
private ToChar() {
// utility class
}
/**
* Emulates Oracle's TO_CHAR(number) function.
*
* <p><table border="1">
* <tr><td><b>Input</b></td><td><b>Output</b></td><td><b>Closest {@link DecimalFormat} Equivalent</b></td></tr>
* <tr><td>,</td><td>Grouping separator.</td><td>,</td></tr>
* <tr><td>.</td><td>Decimal separator.</td><td>.</td></tr>
* <tr><td>$</td><td>Leading dollar sign.</td><td>$</td></tr>
* <tr><td>0</td><td>Leading or trailing zeroes.</td><td>0</td></tr>
* <tr><td>9</td><td>Digit.</td><td>#</td></tr>
* <tr><td>B</td><td>Blanks integer part of a fixed point number less than 1.</td><td>#</td></tr>
* <tr><td>C</td><td>ISO currency symbol.</td><td>\u00A4</td></tr>
* <tr><td>D</td><td>Local decimal separator.</td><td>.</td></tr>
* <tr><td>EEEE</td><td>Returns a value in scientific notation.</td><td>E</td></tr>
* <tr><td>FM</td><td>Returns values with no leading or trailing spaces.</td><td>None.</td></tr>
* <tr><td>G</td><td>Local grouping separator.</td><td>,</td></tr>
* <tr><td>L</td><td>Local currency symbol.</td><td>\u00A4</td></tr>
* <tr><td>MI</td><td>Negative values get trailing minus sign, positive get trailing space.</td><td>-</td></tr>
* <tr><td>PR</td><td>Negative values get enclosing angle brackets, positive get spaces.</td><td>None.</td></tr>
* <tr><td>RN</td><td>Returns values in Roman numerals.</td><td>None.</td></tr>
* <tr><td>S</td><td>Returns values with leading/trailing +/- signs.</td><td>None.</td></tr>
* <tr><td>TM</td><td>Returns smallest number of characters possible.</td><td>None.</td></tr>
* <tr><td>U</td><td>Returns the dual currency symbol.</td><td>None.</td></tr>
* <tr><td>V</td><td>Returns a value multiplied by 10^n.</td><td>None.</td></tr>
* <tr><td>X</td><td>Hex value.</td><td>None.</td></tr>
* </table>
*
* @param number the number to format
* @param format the format pattern to use (if any)
* @param nlsParam the NLS parameter (if any)
* @return the formatted number
* @see <a href="http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions181.htm">TO_CHAR(number)</a>
* @see <a href="http://docs.oracle.com/cd/B19306_01/server.102/b14200/sql_elements004.htm#i34570">Number format models</a>
*/
public static String toChar(BigDecimal number, String format, String nlsParam) {
// short-circuit logic for formats that don't follow common logic below
String formatUp = format != null ? format.toUpperCase() : null;
if (formatUp == null || formatUp.equals("TM") || formatUp.equals("TM9")) {
String s = number.toPlainString();
return s.startsWith("0.") ? s.substring(1) : s;
} else if (formatUp.equals("TME")) {
int pow = number.precision() - number.scale() - 1;
number = number.movePointLeft(pow);
return number.toPlainString() + "E" + (pow < 0 ? '-' : '+') + (abs(pow) < 10 ? "0" : "") + abs(pow);
} else if (formatUp.equals("RN")) {
boolean lowercase = format.startsWith("r");
String rn = StringUtils.pad(toRomanNumeral(number.intValue()), 15, " ", false);
return lowercase ? rn.toLowerCase() : rn;
} else if (formatUp.equals("FMRN")) {
boolean lowercase = format.charAt(2) == 'r';
String rn = toRomanNumeral(number.intValue());
return lowercase ? rn.toLowerCase() : rn;
} else if (formatUp.endsWith("X")) {
return toHex(number, format);
}
String originalFormat = format;
DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance();
char localGrouping = symbols.getGroupingSeparator();
char localDecimal = symbols.getDecimalSeparator();
boolean leadingSign = formatUp.startsWith("S");
if (leadingSign) {
format = format.substring(1);
}
boolean trailingSign = formatUp.endsWith("S");
if (trailingSign) {
format = format.substring(0, format.length() - 1);
}
boolean trailingMinus = formatUp.endsWith("MI");
if (trailingMinus) {
format = format.substring(0, format.length() - 2);
}
boolean angleBrackets = formatUp.endsWith("PR");
if (angleBrackets) {
format = format.substring(0, format.length() - 2);
}
int v = formatUp.indexOf("V");
if (v >= 0) {
int digits = 0;
for (int i = v + 1; i < format.length(); i++) {
char c = format.charAt(i);
if (c == '0' || c == '9') {
digits++;
}
}
number = number.movePointRight(digits);
format = format.substring(0, v) + format.substring(v + 1);
}
Integer power;
if (format.endsWith("EEEE")) {
power = number.precision() - number.scale() - 1;
number = number.movePointLeft(power);
format = format.substring(0, format.length() - 4);
} else {
power = null;
}
int maxLength = 1;
boolean fillMode = !formatUp.startsWith("FM");
if (!fillMode) {
format = format.substring(2);
}
// blanks flag doesn't seem to actually do anything
format = format.replaceAll("[Bb]", "");
// if we need to round the number to fit into the format specified,
// go ahead and do that first
int separator = findDecimalSeparator(format);
int formatScale = calculateScale(format, separator);
if (formatScale < number.scale()) {
number = number.setScale(formatScale, BigDecimal.ROUND_HALF_UP);
}
// any 9s to the left of the decimal separator but to the right of a
// 0 behave the same as a 0, e.g. "09999.99" -> "00000.99"
for (int i = format.indexOf('0'); i >= 0 && i < separator; i++) {
if (format.charAt(i) == '9') {
format = format.substring(0, i) + "0" + format.substring(i + 1);
}
}
StringBuilder output = new StringBuilder();
String unscaled = number.unscaledValue().abs().toString();
// start at the decimal point and fill in the numbers to the left,
// working our way from right to left
int i = separator - 1;
int j = unscaled.length() - number.scale() - 1;
for (; i >= 0; i--) {
char c = format.charAt(i);
maxLength++;
if (c == '9' || c == '0') {
if (j >= 0) {
char digit = unscaled.charAt(j);
output.insert(0, digit);
j--;
} else if (c == '0' && power == null) {
output.insert(0, '0');
}
} else if (c == ',') {
// only add the grouping separator if we have more numbers
if (j >= 0 || (i > 0 && format.charAt(i - 1) == '0')) {
output.insert(0, c);
}
} else if (c == 'G' || c == 'g') {
// only add the grouping separator if we have more numbers
if (j >= 0 || (i > 0 && format.charAt(i - 1) == '0')) {
output.insert(0, localGrouping);
}
} else if (c == 'C' || c == 'c') {
Currency currency = Currency.getInstance(Locale.getDefault());
output.insert(0, currency.getCurrencyCode());
maxLength += 6;
} else if (c == 'L' || c == 'l' || c == 'U' || c == 'u') {
Currency currency = Currency.getInstance(Locale.getDefault());
output.insert(0, currency.getSymbol());
maxLength += 9;
} else if (c == '$') {
output.insert(0, c);
} else {
throw DbException.get(ErrorCode.INVALID_TO_CHAR_FORMAT, originalFormat);
}
}
// if the format (to the left of the decimal point) was too small
// to hold the number, return a big "######" string
if (j >= 0) {
return StringUtils.pad("", format.length() + 1, "#", true);
}
if (separator < format.length()) {
// add the decimal point
maxLength++;
char pt = format.charAt(separator);
if (pt == 'd' || pt == 'D') {
output.append(localDecimal);
} else {
output.append(pt);
}
// start at the decimal point and fill in the numbers to the right,
// working our way from left to right
i = separator + 1;
j = unscaled.length() - number.scale();
for (; i < format.length(); i++) {
char c = format.charAt(i);
maxLength++;
if (c == '9' || c == '0') {
if (j < unscaled.length()) {
char digit = unscaled.charAt(j);
output.append(digit);
j++;
} else {
if (c == '0' || fillMode) {
output.append('0');
}
}
} else {
throw DbException.get(ErrorCode.INVALID_TO_CHAR_FORMAT, originalFormat);
}
}
}
addSign(output, number.signum(), leadingSign, trailingSign, trailingMinus, angleBrackets, fillMode);
if (power != null) {
output.append('E');
output.append(power < 0 ? '-' : '+');
output.append(Math.abs(power) < 10 ? "0" : "");
output.append(Math.abs(power));
}
if (fillMode) {
if (power != null) {
output.insert(0, ' ');
} else {
while (output.length() < maxLength) {
output.insert(0, ' ');
}
}
}
return output.toString();
}
private static void addSign(StringBuilder output, int signum, boolean leadingSign, boolean trailingSign,
boolean trailingMinus, boolean angleBrackets, boolean fillMode) {
if (angleBrackets) {
if (signum < 0) {
output.insert(0, '<');
output.append('>');
} else if (fillMode) {
output.insert(0, ' ');
output.append(' ');
}
} else {
String sign;
if (signum == 0) {
sign = "";
} else if (signum < 0) {
sign = "-";
} else {
if (leadingSign || trailingSign) {
sign = "+";
} else if (fillMode) {
sign = " ";
} else {
sign = "";
}
}
if (trailingMinus || trailingSign) {
output.append(sign);
} else {
output.insert(0, sign);
}
}
}
private static int findDecimalSeparator(String format) {
int index = format.indexOf('.');
if (index == -1) {
index = format.indexOf('D');
if (index == -1) {
index = format.indexOf('d');
if (index == -1) {
index = format.length();
}
}
}
return index;
}
private static int calculateScale(String format, int separator) {
int scale = 0;
for (int i = separator; i < format.length(); i++) {
char c = format.charAt(i);
if (c == '0' || c == '9') {
scale++;
}
}
return scale;
}
private static String toRomanNumeral(int number) {
int[] values = new int[] { 1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1 };
String[] numerals = new String[] { "M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I" };
StringBuilder result = new StringBuilder();
for (int i = 0; i < values.length; i++) {
int value = values[i];
String numeral = numerals[i];
while (number >= value) {
result.append(numeral);
number -= value;
}
}
return result.toString();
}
private static String toHex(BigDecimal number, String format) {
boolean fillMode = !format.toUpperCase().startsWith("FM");
boolean uppercase = !format.contains("x");
boolean zeroPadded = format.startsWith("0");
int digits = 0;
for (int i = 0; i < format.length(); i++) {
char c = format.charAt(i);
if (c == '0' || c == 'X' || c == 'x') {
digits++;
}
}
int i = number.setScale(0, BigDecimal.ROUND_HALF_UP).intValue();
String hex = Integer.toHexString(i);
if (digits < hex.length()) {
hex = StringUtils.pad("", digits + 1, "#", true);
} else {
if (uppercase) {
hex = hex.toUpperCase();
}
if (zeroPadded) {
hex = StringUtils.pad(hex, digits, "0", false);
}
if (fillMode) {
hex = StringUtils.pad(hex, format.length() + 1, " ", false);
}
}
return hex;
}
/**
* Emulates Oracle's TO_CHAR(datetime) function.
*
* <p><table border="1">
* <tr><td><b>Input</b></td><td><b>Output</b></td><td><b>Closest {@link SimpleDateFormat} Equivalent</b></td></tr>
* <tr><td>- / , . ; : "text"</td><td>Reproduced verbatim.</td><td>'text'</td></tr>
* <tr><td>A.D. AD B.C. BC</td><td>Era designator, with or without periods.</td><td>G</td></tr>
* <tr><td>A.M. AM P.M. PM</td><td>AM/PM marker.</td><td>a</td></tr>
* <tr><td>CC SCC</td><td>Century.</td><td>None.</td></tr>
* <tr><td>D</td><td>Day of week.</td><td>u</td></tr>
* <tr><td>DAY</td><td>Name of day.</td><td>EEEE</td></tr>
* <tr><td>DY</td><td>Abbreviated day name.</td><td>EEE</td></tr>
* <tr><td>DD</td><td>Day of month.</td><td>d</td></tr>
* <tr><td>DDD</td><td>Day of year.</td><td>D</td></tr>
* <tr><td>DL</td><td>Long date format.</td><td>EEEE, MMMM d, yyyy</td></tr>
* <tr><td>DS</td><td>Short date format.</td><td>MM/dd/yyyy</td></tr>
* <tr><td>E</td><td>Abbreviated era name (Japanese, Chinese, Thai)</td><td>None.</td></tr>
* <tr><td>EE</td><td>Full era name (Japanese, Chinese, Thai)</td><td>None.</td></tr>
* <tr><td>FF[1-9]</td><td>Fractional seconds.</td><td>S</td></tr>
* <tr><td>FM</td><td>Returns values with no leading or trailing spaces.</td><td>None.</td></tr>
* <tr><td>FX</td><td>Requires exact matches between character data and format model.</td><td>None.</td></tr>
* <tr><td>HH HH12</td><td>Hour in AM/PM (1-12).</td><td>hh</td></tr>
* <tr><td>HH24</td><td>Hour in day (0-23).</td><td>HH</td></tr>
* <tr><td>IW</td><td>Week in year.</td><td>w</td></tr>
* <tr><td>WW</td><td>Week in year.</td><td>w</td></tr>
* <tr><td>W</td><td>Week in month.</td><td>W</td></tr>
* <tr><td>IYYY IYY IY I</td><td>Last 4/3/2/1 digit(s) of ISO year.</td><td>yyyy yyy yy y</td></tr>
* <tr><td>RRRR RR</td><td>Last 4/2 digits of year.</td><td>yyyy yy</td></tr>
* <tr><td>Y,YYY</td><td>Year with comma.</td><td>None.</td></tr>
* <tr><td>YEAR SYEAR</td><td>Year spelled out (S prefixes BC years with minus sign).</td><td>None.</td></tr>
* <tr><td>YYYY SYYYY</td><td>4-digit year (S prefixes BC years with minus sign).</td><td>yyyy</td></tr>
* <tr><td>YYY YY Y</td><td>Last 3/2/1 digit(s) of year.</td><td>yyy yy y</td></tr>
* <tr><td>J</td><td>Julian day (number of days since January 1, 4712 BC).</td><td>None.</td></tr>
* <tr><td>MI</td><td>Minute in hour.</td><td>mm</td></tr>
* <tr><td>MM</td><td>Month in year.</td><td>MM</td></tr>
* <tr><td>MON</td><td>Abbreviated name of month.</td><td>MMM</td></tr>
* <tr><td>MONTH</td><td>Name of month, padded with spaces.</td><td>MMMM</td></tr>
* <tr><td>RM</td><td>Roman numeral month (I-XII).</td><td>None.</td></tr>
* <tr><td>Q</td><td>Quarter of year.</td><td>None.</td></tr>
* <tr><td>SS</td><td>Seconds in minute.</td><td>ss</td></tr>
* <tr><td>SSSSS</td><td>Seconds in day.</td><td>None.</td></tr>
* <tr><td>TS</td><td>Short time format.</td><td>h:mm:ss aa</td></tr>
* <tr><td>TZD</td><td>Daylight savings time zone abbreviation.</td><td>z</td></tr>
* <tr><td>TZR</td><td>Time zone region information.</td><td>zzzz</td></tr>
* <tr><td>X</td><td>Local radix character.</td><td>None.</td></tr>
* </table>
*
* @param ts the timestamp to format
* @param format the format pattern to use (if any)
* @param nlsParam the NLS parameter (if any)
* @return the formatted timestamp
* @see <a href="http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions180.htm">TO_CHAR(datetime)</a>
* @see <a href="http://docs.oracle.com/cd/B19306_01/server.102/b14200/sql_elements004.htm#i34924">Datetime format models</a>
*/
public static String toChar(Timestamp ts, String format, String nlsParam) {
if (format == null) {
format = "DD-MON-YY HH.MI.SS.FF PM";
}
GregorianCalendar cal = new GregorianCalendar(Locale.ENGLISH);
cal.setTimeInMillis(ts.getTime());
StringBuilder output = new StringBuilder();
boolean fillMode = true;
for (int i = 0; i < format.length();) {
Capitalization cap;
// AD / BC
if ((cap = containsAt(format, i, "A.D.", "B.C.")) != null) {
String era = cal.get(Calendar.ERA) == GregorianCalendar.AD ? "A.D." : "B.C.";
output.append(cap.apply(era));
i += 4;
} else if ((cap = containsAt(format, i, "AD", "BC")) != null) {
String era = cal.get(Calendar.ERA) == GregorianCalendar.AD ? "AD" : "BC";
output.append(cap.apply(era));
i += 2;
// AM / PM
} else if ((cap = containsAt(format, i, "A.M.", "P.M.")) != null) {
String am = cal.get(Calendar.AM_PM) == Calendar.AM ? "A.M." : "P.M.";
output.append(cap.apply(am));
i += 4;
} else if ((cap = containsAt(format, i, "AM", "PM")) != null) {
String am = cal.get(Calendar.AM_PM) == Calendar.AM ? "AM" : "PM";
output.append(cap.apply(am));
i += 2;
// Long/short date/time format
} else if ((cap = containsAt(format, i, "DL")) != null) {
output.append(new SimpleDateFormat("EEEE, MMMM d, yyyy").format(ts));
i += 2;
} else if ((cap = containsAt(format, i, "DS")) != null) {
output.append(new SimpleDateFormat("MM/dd/yyyy").format(ts));
i += 2;
} else if ((cap = containsAt(format, i, "TS")) != null) {
output.append(new SimpleDateFormat("h:mm:ss aa").format(ts));
i += 2;
// Day
} else if ((cap = containsAt(format, i, "DDD")) != null) {
output.append(cal.get(Calendar.DAY_OF_YEAR));
i += 3;
} else if ((cap = containsAt(format, i, "DD")) != null) {
output.append(cal.get(Calendar.DAY_OF_MONTH));
i += 2;
} else if ((cap = containsAt(format, i, "DY")) != null) {
String day = new SimpleDateFormat("EEE").format(ts).toUpperCase();
output.append(cap.apply(day));
i += 2;
} else if ((cap = containsAt(format, i, "DAY")) != null) {
String day = new SimpleDateFormat("EEEE").format(ts);
if (fillMode) {
day = StringUtils.pad(day, "Wednesday".length(), " ", true);
}
output.append(cap.apply(day));
i += 3;
} else if ((cap = containsAt(format, i, "D")) != null) {
output.append(cal.get(Calendar.DAY_OF_WEEK));
i += 1;
} else if ((cap = containsAt(format, i, "J")) != null) {
long millis = ts.getTime() - JULIAN_EPOCH;
long days = (long) Math.floor(millis / (1000 * 60 * 60 * 24));
output.append(days);
i += 1;
// Hours
} else if ((cap = containsAt(format, i, "HH24")) != null) {
output.append(new DecimalFormat("00").format(cal.get(Calendar.HOUR_OF_DAY)));
i += 4;
} else if ((cap = containsAt(format, i, "HH12")) != null) {
output.append(new DecimalFormat("00").format(cal.get(Calendar.HOUR)));
i += 4;
} else if ((cap = containsAt(format, i, "HH")) != null) {
output.append(new DecimalFormat("00").format(cal.get(Calendar.HOUR)));
i += 2;
// Minutes
} else if ((cap = containsAt(format, i, "MI")) != null) {
output.append(new DecimalFormat("00").format(cal.get(Calendar.MINUTE)));
i += 2;
// Seconds
} else if ((cap = containsAt(format, i, "SSSSS")) != null) {
int seconds = cal.get(Calendar.HOUR_OF_DAY) * 60 * 60;
seconds += cal.get(Calendar.MINUTE) * 60;
seconds += cal.get(Calendar.SECOND);
output.append(seconds);
i += 5;
} else if ((cap = containsAt(format, i, "SS")) != null) {
output.append(new DecimalFormat("00").format(cal.get(Calendar.SECOND)));
i += 2;
// Fractional seconds
} else if ((cap = containsAt(format, i, "FF1", "FF2", "FF3", "FF4", "FF5", "FF6", "FF7", "FF8", "FF9")) != null) {
int x = Integer.parseInt(format.substring(i + 2, i + 3));
int ff = (int) (cal.get(Calendar.MILLISECOND) * Math.pow(10, x - 3));
output.append(ff);
i += 3;
} else if ((cap = containsAt(format, i, "FF")) != null) {
output.append(cal.get(Calendar.MILLISECOND) * 1000);
i += 2;
// Time zone
} else if ((cap = containsAt(format, i, "TZR")) != null) {
TimeZone tz = TimeZone.getDefault();
output.append(tz.getID());
i += 3;
} else if ((cap = containsAt(format, i, "TZD")) != null) {
TimeZone tz = TimeZone.getDefault();
boolean daylight = tz.inDaylightTime(new java.util.Date());
output.append(tz.getDisplayName(daylight, TimeZone.SHORT));
i += 3;
// Week
} else if ((cap = containsAt(format, i, "IW", "WW")) != null) {
output.append(cal.get(Calendar.WEEK_OF_YEAR));
i += 2;
} else if ((cap = containsAt(format, i, "W")) != null) {
int w = (int) (1 + Math.floor(cal.get(Calendar.DAY_OF_MONTH) / 7));
output.append(w);
i += 1;
// Year
} else if ((cap = containsAt(format, i, "Y,YYY")) != null) {
output.append(new DecimalFormat("#,###").format(getYear(cal)));
i += 5;
} else if ((cap = containsAt(format, i, "SYYYY")) != null) {
if (cal.get(Calendar.ERA) == GregorianCalendar.BC) {
output.append('-');
}
output.append(new DecimalFormat("0000").format(getYear(cal)));
i += 5;
} else if ((cap = containsAt(format, i, "YYYY", "IYYY", "RRRR")) != null) {
output.append(new DecimalFormat("0000").format(getYear(cal)));
i += 4;
} else if ((cap = containsAt(format, i, "YYY", "IYY")) != null) {
output.append(new DecimalFormat("000").format(getYear(cal) % 1000));
i += 3;
} else if ((cap = containsAt(format, i, "YY", "IY", "RR")) != null) {
output.append(new DecimalFormat("00").format(getYear(cal) % 100));
i += 2;
} else if ((cap = containsAt(format, i, "I", "Y")) != null) {
output.append(getYear(cal) % 10);
i += 1;
// Month / quarter
} else if ((cap = containsAt(format, i, "MONTH")) != null) {
String month = new SimpleDateFormat("MMMM").format(ts);
if (fillMode) {
month = StringUtils.pad(month, "September".length(), " ", true);
}
output.append(cap.apply(month));
i += 5;
} else if ((cap = containsAt(format, i, "MON")) != null) {
String month = new SimpleDateFormat("MMM").format(ts);
output.append(cap.apply(month));
i += 3;
} else if ((cap = containsAt(format, i, "MM")) != null) {
output.append(cal.get(Calendar.MONTH) + 1);
i += 2;
} else if ((cap = containsAt(format, i, "RM")) != null) {
int month = cal.get(Calendar.MONTH) + 1;
output.append(cap.apply(toRomanNumeral(month)));
i += 2;
} else if ((cap = containsAt(format, i, "Q")) != null) {
int q = (int) (1 + Math.floor(cal.get(Calendar.MONTH) / 3));
output.append(q);
i += 1;
// Local radix character
} else if ((cap = containsAt(format, i, "X")) != null) {
char c = DecimalFormatSymbols.getInstance().getDecimalSeparator();
output.append(c);
i += 1;
// Format modifiers
} else if ((cap = containsAt(format, i, "FM")) != null) {
fillMode = !fillMode;
i += 2;
} else if ((cap = containsAt(format, i, "FX")) != null) {
i += 2;
// Literal text
} else if ((cap = containsAt(format, i, "\"")) != null) {
for (i = i + 1; i < format.length(); i++) {
char c = format.charAt(i);
if (c != '"') {
output.append(c);
} else {
i++;
break;
}
}
} else if (format.charAt(i) == '-' || format.charAt(i) == '/' || format.charAt(i) == ','
|| format.charAt(i) == '.' || format.charAt(i) == ';' || format.charAt(i) == ':'
|| format.charAt(i) == ' ') {
output.append(format.charAt(i));
i += 1;
// Anything else
} else {
throw DbException.get(ErrorCode.INVALID_TO_CHAR_FORMAT, format);
}
}
return output.toString();
}
private static int getYear(Calendar cal) {
int year = cal.get(Calendar.YEAR);
if (cal.get(Calendar.ERA) == GregorianCalendar.BC) {
year--;
}
return year;
}
/**
* Returns a capitalization strategy if the specified string contains any of the specified substrings at the
* specified index. The capitalization strategy indicates the casing of the substring that was found. If none
* of the specified substrings are found, this method returns <code>null</code>.
*
* @param s the string to check
* @param index the index to check at
* @param substrings the substrings to check for within the string
* @return a capitalization strategy if the specified string contains any of the specified substrings at the
* specified index, <code>null</code> otherwise
*/
private static Capitalization containsAt(String s, int index, String... substrings) {
for (String substring : substrings) {
if (index + substring.length() <= s.length()) {
boolean found = true;
Boolean up1 = null;
Boolean up2 = null;
for (int i = 0; i < substring.length(); i++) {
char c1 = s.charAt(index + i);
char c2 = substring.charAt(i);
if (c1 != c2 && Character.toUpperCase(c1) != Character.toUpperCase(c2)) {
found = false;
break;
} else if (Character.isLetter(c1)) {
if (up1 == null) {
up1 = Character.isUpperCase(c1);
} else if (up2 == null) {
up2 = Character.isUpperCase(c1);
}
}
}
if (found) {
return Capitalization.toCapitalization(up1, up2);
}
}
}
return null;
}
/** Represents a capitalization / casing strategy. */
private enum Capitalization {
/** All letters are uppercased. */
UPPERCASE,
/** All letters are lowercased. */
LOWERCASE,
/** The string is capitalized (first letter uppercased, subsequent letters lowercased). */
CAPITALIZE;
/**
* Returns the capitalization / casing strategy which should be used when the first and second letters have
* the specified casing.
*
* @param up1 whether or not the first letter is uppercased
* @param up2 whether or not the second letter is uppercased
* @return the capitalization / casing strategy which should be used when the first and second letters have
* the specified casing
*/
public static Capitalization toCapitalization(Boolean up1, Boolean up2) {
if (up1 == null) {
return Capitalization.CAPITALIZE;
} else if (up2 == null) {
return up1 ? Capitalization.UPPERCASE : Capitalization.LOWERCASE;
} else if (up1) {
return up2 ? Capitalization.UPPERCASE : Capitalization.CAPITALIZE;
} else {
return Capitalization.LOWERCASE;
}
}
/**
* Applies this capitalization strategy to the specified string.
*
* @param s the string to apply this strategy to
* @return the resultant string
*/
public String apply(String s) {
if (s == null || s.isEmpty()) {
return s;
}
switch (this) {
case UPPERCASE:
return s.toUpperCase();
case LOWERCASE:
return s.toLowerCase();
case CAPITALIZE:
return Character.toUpperCase(s.charAt(0)) + (s.length() > 1 ? s.toLowerCase().substring(1) : "");
default:
throw new IllegalArgumentException("Unknown capitalization strategy: " + this);
}
}
}
}
...@@ -712,6 +712,8 @@ public abstract class TestBase { ...@@ -712,6 +712,8 @@ public abstract class TestBase {
} else if (expected == null || actual == null) { } else if (expected == null || actual == null) {
fail("Expected: " + expected + " Actual: " + actual + " " + message); fail("Expected: " + expected + " Actual: " + actual + " " + message);
} else if (!expected.equals(actual)) { } else if (!expected.equals(actual)) {
int al = expected.length();
int bl = actual.length();
for (int i = 0; i < expected.length(); i++) { for (int i = 0; i < expected.length(); i++) {
String s = expected.substring(0, i); String s = expected.substring(0, i);
if (!actual.startsWith(s)) { if (!actual.startsWith(s)) {
...@@ -719,8 +721,6 @@ public abstract class TestBase { ...@@ -719,8 +721,6 @@ public abstract class TestBase {
break; break;
} }
} }
int al = expected.length();
int bl = actual.length();
if (al > 4000) { if (al > 4000) {
expected = expected.substring(0, 4000); expected = expected.substring(0, 4000);
} }
...@@ -970,6 +970,22 @@ public abstract class TestBase { ...@@ -970,6 +970,22 @@ public abstract class TestBase {
} }
} }
/**
* Check that executing the specified query results in the specified error.
*
* @param expectedErrorMessage the expected error message
* @param stat the statement
* @param sql the SQL statement to execute
*/
protected void assertThrows(String expectedErrorMessage, Statement stat, String sql) {
try {
stat.executeQuery(sql);
fail("Expected error: " + expectedErrorMessage);
} catch (SQLException e) {
assertTrue(e.getMessage().startsWith(expectedErrorMessage));
}
}
/** /**
* Check if the result set meta data is correct. * Check if the result set meta data is correct.
* *
......
...@@ -26,7 +26,11 @@ import java.sql.Statement; ...@@ -26,7 +26,11 @@ import java.sql.Statement;
import java.sql.Types; import java.sql.Types;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
import java.util.Currency;
import java.util.Date;
import java.util.Locale;
import java.util.Properties; import java.util.Properties;
import java.util.TimeZone;
import java.util.UUID; import java.util.UUID;
import org.h2.api.AggregateFunction; import org.h2.api.AggregateFunction;
import org.h2.constant.ErrorCode; import org.h2.constant.ErrorCode;
...@@ -82,6 +86,9 @@ public class TestFunctions extends TestBase implements AggregateFunction { ...@@ -82,6 +86,9 @@ public class TestFunctions extends TestBase implements AggregateFunction {
testNvl2(); testNvl2();
testConcatWs(); testConcatWs();
testTruncate(); testTruncate();
testToCharFromDateTime();
testToCharFromNumber();
testToCharFromText();
// TODO // TODO
// testCachingOfDeterministicFunctionAlias(); // testCachingOfDeterministicFunctionAlias();
...@@ -217,11 +224,11 @@ public class TestFunctions extends TestBase implements AggregateFunction { ...@@ -217,11 +224,11 @@ public class TestFunctions extends TestBase implements AggregateFunction {
Connection conn = getConnection("functions"); Connection conn = getConnection("functions");
Statement stat = conn.createStatement(); Statement stat = conn.createStatement();
ResultSet rs; ResultSet rs;
stat.execute("create alias TO_CHAR for \"" + getClass().getName() + ".toChar\""); stat.execute("create alias TO_CHAR_2 for \"" + getClass().getName() + ".toChar\"");
rs = stat.executeQuery("call TO_CHAR(TIMESTAMP '2001-02-03 04:05:06', 'format')"); rs = stat.executeQuery("call TO_CHAR_2(TIMESTAMP '2001-02-03 04:05:06', 'format')");
rs.next(); rs.next();
assertEquals("2001-02-03 04:05:06.0", rs.getString(1)); assertEquals("2001-02-03 04:05:06.0", rs.getString(1));
stat.execute("drop alias TO_CHAR"); stat.execute("drop alias TO_CHAR_2");
conn.close(); conn.close();
} }
...@@ -935,6 +942,294 @@ public class TestFunctions extends TestBase implements AggregateFunction { ...@@ -935,6 +942,294 @@ public class TestFunctions extends TestBase implements AggregateFunction {
conn.close(); conn.close();
} }
private void testToCharFromDateTime() throws SQLException {
deleteDb("functions");
Connection conn = getConnection("functions");
Statement stat = conn.createStatement();
TimeZone tz = TimeZone.getDefault();
boolean daylight = tz.inDaylightTime(new Date());
String tzShortName = tz.getDisplayName(daylight, TimeZone.SHORT);
String tzLongName = tz.getID();
stat.executeUpdate("CREATE TABLE T (X TIMESTAMP(6))");
stat.executeUpdate("INSERT INTO T VALUES (TIMESTAMP '1979-11-12 08:12:34.560')");
stat.executeUpdate("CREATE TABLE U (X TIMESTAMP(6))");
stat.executeUpdate("INSERT INTO U VALUES (TIMESTAMP '-100-01-15 14:04:02.120')");
assertResult("1979-11-12 08:12:34.56", stat, "SELECT X FROM T");
assertResult("-100-01-15 14:04:02.12", stat, "SELECT X FROM U");
assertResult("12-NOV-79 08.12.34.560000 AM", stat, "SELECT TO_CHAR(X) FROM T");
assertResult("- / , . ; : text - /", stat, "SELECT TO_CHAR(X, '- / , . ; : \"text\" - /') FROM T");
assertResult("1979-11-12", stat, "SELECT TO_CHAR(X, 'YYYY-MM-DD') FROM T");
assertResult("1979/11/12", stat, "SELECT TO_CHAR(X, 'YYYY/MM/DD') FROM T");
assertResult("1979,11,12", stat, "SELECT TO_CHAR(X, 'YYYY,MM,DD') FROM T");
assertResult("1979.11.12", stat, "SELECT TO_CHAR(X, 'YYYY.MM.DD') FROM T");
assertResult("1979;11;12", stat, "SELECT TO_CHAR(X, 'YYYY;MM;DD') FROM T");
assertResult("1979:11:12", stat, "SELECT TO_CHAR(X, 'YYYY:MM:DD') FROM T");
assertResult("year 1979!", stat, "SELECT TO_CHAR(X, '\"year \"YYYY\"!\"') FROM T");
assertResult("1979 AD", stat, "SELECT TO_CHAR(X, 'YYYY AD') FROM T");
assertResult("1979 A.D.", stat, "SELECT TO_CHAR(X, 'YYYY A.D.') FROM T");
assertResult("0100 B.C.", stat, "SELECT TO_CHAR(X, 'YYYY A.D.') FROM U");
assertResult("1979 AD", stat, "SELECT TO_CHAR(X, 'YYYY BC') FROM T");
assertResult("100 BC", stat, "SELECT TO_CHAR(X, 'YYY BC') FROM U");
assertResult("00 BC", stat, "SELECT TO_CHAR(X, 'YY BC') FROM U");
assertResult("0 BC", stat, "SELECT TO_CHAR(X, 'Y BC') FROM U");
assertResult("1979 A.D.", stat, "SELECT TO_CHAR(X, 'YYYY B.C.') FROM T");
assertResult("08:12 AM", stat, "SELECT TO_CHAR(X, 'HH:MI AM') FROM T");
assertResult("08:12 A.M.", stat, "SELECT TO_CHAR(X, 'HH:MI A.M.') FROM T");
assertResult("02:04 P.M.", stat, "SELECT TO_CHAR(X, 'HH:MI A.M.') FROM U");
assertResult("08:12 AM", stat, "SELECT TO_CHAR(X, 'HH:MI PM') FROM T");
assertResult("02:04 PM", stat, "SELECT TO_CHAR(X, 'HH:MI PM') FROM U");
assertResult("08:12 A.M.", stat, "SELECT TO_CHAR(X, 'HH:MI P.M.') FROM T");
assertResult("A.M.", stat, "SELECT TO_CHAR(X, 'P.M.') FROM T");
assertResult("a.m.", stat, "SELECT TO_CHAR(X, 'p.M.') FROM T");
assertResult("a.m.", stat, "SELECT TO_CHAR(X, 'p.m.') FROM T");
assertResult("AM", stat, "SELECT TO_CHAR(X, 'PM') FROM T");
assertResult("Am", stat, "SELECT TO_CHAR(X, 'Pm') FROM T");
assertResult("am", stat, "SELECT TO_CHAR(X, 'pM') FROM T");
assertResult("am", stat, "SELECT TO_CHAR(X, 'pm') FROM T");
assertResult("2", stat, "SELECT TO_CHAR(X, 'D') FROM T");
assertResult("2", stat, "SELECT TO_CHAR(X, 'd') FROM T");
assertResult("MONDAY ", stat, "SELECT TO_CHAR(X, 'DAY') FROM T");
assertResult("Monday ", stat, "SELECT TO_CHAR(X, 'Day') FROM T");
assertResult("monday ", stat, "SELECT TO_CHAR(X, 'day') FROM T");
assertResult("monday ", stat, "SELECT TO_CHAR(X, 'dAY') FROM T");
assertResult("Monday", stat, "SELECT TO_CHAR(X, 'fmDay') FROM T");
assertResult("monday -monday-monday-monday -monday", stat, "SELECT TO_CHAR(X, 'day-fmday-day-fmday-fmday') FROM T");
assertResult("12", stat, "SELECT TO_CHAR(X, 'DD') FROM T");
assertResult("316", stat, "SELECT TO_CHAR(X, 'DDD') FROM T");
assertResult("316", stat, "SELECT TO_CHAR(X, 'DdD') FROM T");
assertResult("316", stat, "SELECT TO_CHAR(X, 'dDD') FROM T");
assertResult("316", stat, "SELECT TO_CHAR(X, 'ddd') FROM T");
assertResult("Monday, November 12, 1979", stat, "SELECT TO_CHAR(X, 'DL') FROM T");
assertResult("Monday, November 12, 1979", stat, "SELECT TO_CHAR(X, 'DL', 'NLS_DATE_LANGUAGE = English') FROM T");
assertResult("11/12/1979", stat, "SELECT TO_CHAR(X, 'DS') FROM T");
assertResult("11/12/1979", stat, "SELECT TO_CHAR(X, 'Ds') FROM T");
assertResult("11/12/1979", stat, "SELECT TO_CHAR(X, 'dS') FROM T");
assertResult("11/12/1979", stat, "SELECT TO_CHAR(X, 'ds') FROM T");
assertResult("MON", stat, "SELECT TO_CHAR(X, 'DY') FROM T");
assertResult("Mon", stat, "SELECT TO_CHAR(X, 'Dy') FROM T");
assertResult("mon", stat, "SELECT TO_CHAR(X, 'dy') FROM T");
assertResult("mon", stat, "SELECT TO_CHAR(X, 'dY') FROM T");
assertResult("08:12:34.560000", stat, "SELECT TO_CHAR(X, 'HH:MI:SS.FF') FROM T");
assertResult("08:12:34.5", stat, "SELECT TO_CHAR(X, 'HH:MI:SS.FF1') FROM T");
assertResult("08:12:34.56", stat, "SELECT TO_CHAR(X, 'HH:MI:SS.FF2') FROM T");
assertResult("08:12:34.560", stat, "SELECT TO_CHAR(X, 'HH:MI:SS.FF3') FROM T");
assertResult("08:12:34.5600", stat, "SELECT TO_CHAR(X, 'HH:MI:SS.FF4') FROM T");
assertResult("08:12:34.56000", stat, "SELECT TO_CHAR(X, 'HH:MI:SS.FF5') FROM T");
assertResult("08:12:34.560000", stat, "SELECT TO_CHAR(X, 'HH:MI:SS.FF6') FROM T");
assertResult("08:12:34.5600000", stat, "SELECT TO_CHAR(X, 'HH:MI:SS.FF7') FROM T");
assertResult("08:12:34.56000000", stat, "SELECT TO_CHAR(X, 'HH:MI:SS.FF8') FROM T");
assertResult("08:12:34.560000000", stat, "SELECT TO_CHAR(X, 'HH:MI:SS.FF9') FROM T");
assertResult("08:12:34.560000000", stat, "SELECT TO_CHAR(X, 'HH:MI:SS.ff9') FROM T");
assertResult("08:12:34.560000000", stat, "SELECT TO_CHAR(X, 'HH:MI:SS.fF9') FROM T");
assertResult("08:12", stat, "SELECT TO_CHAR(X, 'HH:MI') FROM T");
assertResult("08:12", stat, "SELECT TO_CHAR(X, 'HH12:MI') FROM T");
assertResult("08:12", stat, "SELECT TO_CHAR(X, 'HH24:MI') FROM T");
assertResult("46", stat, "SELECT TO_CHAR(X, 'IW') FROM T");
assertResult("46", stat, "SELECT TO_CHAR(X, 'WW') FROM T");
assertResult("2", stat, "SELECT TO_CHAR(X, 'W') FROM T");
assertResult("9", stat, "SELECT TO_CHAR(X, 'I') FROM T");
assertResult("79", stat, "SELECT TO_CHAR(X, 'IY') FROM T");
assertResult("979", stat, "SELECT TO_CHAR(X, 'IYY') FROM T");
assertResult("1979", stat, "SELECT TO_CHAR(X, 'IYYY') FROM T");
assertResult("2444190", stat, "SELECT TO_CHAR(X, 'J') FROM T");
assertResult("12", stat, "SELECT TO_CHAR(X, 'MI') FROM T");
assertResult("11", stat, "SELECT TO_CHAR(X, 'MM') FROM T");
assertResult("11", stat, "SELECT TO_CHAR(X, 'Mm') FROM T");
assertResult("11", stat, "SELECT TO_CHAR(X, 'mM') FROM T");
assertResult("11", stat, "SELECT TO_CHAR(X, 'mm') FROM T");
assertResult("NOV", stat, "SELECT TO_CHAR(X, 'MON') FROM T");
assertResult("Nov", stat, "SELECT TO_CHAR(X, 'Mon') FROM T");
assertResult("nov", stat, "SELECT TO_CHAR(X, 'mon') FROM T");
assertResult("NOVEMBER ", stat, "SELECT TO_CHAR(X, 'MONTH') FROM T");
assertResult("November ", stat, "SELECT TO_CHAR(X, 'Month') FROM T");
assertResult("november ", stat, "SELECT TO_CHAR(X, 'month') FROM T");
assertResult("November", stat, "SELECT TO_CHAR(X, 'fmMonth') FROM T");
assertResult("4", stat, "SELECT TO_CHAR(X, 'Q') FROM T");
assertResult("XI", stat, "SELECT TO_CHAR(X, 'RM') FROM T");
assertResult("xi", stat, "SELECT TO_CHAR(X, 'rm') FROM T");
assertResult("Xi", stat, "SELECT TO_CHAR(X, 'Rm') FROM T");
assertResult("79", stat, "SELECT TO_CHAR(X, 'RR') FROM T");
assertResult("1979", stat, "SELECT TO_CHAR(X, 'RRRR') FROM T");
assertResult("34", stat, "SELECT TO_CHAR(X, 'SS') FROM T");
assertResult("29554", stat, "SELECT TO_CHAR(X, 'SSSSS') FROM T");
assertResult("8:12:34 AM", stat, "SELECT TO_CHAR(X, 'TS') FROM T");
assertResult(tzLongName, stat, "SELECT TO_CHAR(X, 'TZR') FROM T");
assertResult(tzShortName, stat, "SELECT TO_CHAR(X, 'TZD') FROM T");
assertResult(".", stat, "SELECT TO_CHAR(X, 'X') FROM T");
assertResult("1,979", stat, "SELECT TO_CHAR(X, 'Y,YYY') FROM T");
assertResult("1979", stat, "SELECT TO_CHAR(X, 'YYYY') FROM T");
assertResult("1979", stat, "SELECT TO_CHAR(X, 'SYYYY') FROM T");
assertResult("-0100", stat, "SELECT TO_CHAR(X, 'SYYYY') FROM U");
assertResult("979", stat, "SELECT TO_CHAR(X, 'YYY') FROM T");
assertResult("79", stat, "SELECT TO_CHAR(X, 'YY') FROM T");
assertResult("9", stat, "SELECT TO_CHAR(X, 'Y') FROM T");
assertResult("7979", stat, "SELECT TO_CHAR(X, 'yyfxyy') FROM T");
assertThrows("", stat, "SELECT TO_CHAR(X, 'A') FROM T");
conn.close();
}
private void testToCharFromNumber() throws SQLException {
deleteDb("functions");
Connection conn = getConnection("functions");
Statement stat = conn.createStatement();
Currency currency = Currency.getInstance(Locale.getDefault());
String cc = currency.getCurrencyCode();
String cs = currency.getSymbol();
assertResult(".45", stat, "SELECT TO_CHAR(0.45) FROM DUAL");
assertResult("12923", stat, "SELECT TO_CHAR(12923) FROM DUAL");
assertResult(" 12923.00", stat, "SELECT TO_CHAR(12923, '99999.99', 'NLS_CURRENCY = BTC') FROM DUAL");
assertResult("12923.", stat, "SELECT TO_CHAR(12923, 'FM99999.99', 'NLS_CURRENCY = BTC') FROM DUAL");
assertResult("######", stat, "SELECT TO_CHAR(12345, '9,999') FROM DUAL");
assertResult("######", stat, "SELECT TO_CHAR(1234567, '9,999') FROM DUAL");
assertResult(" 12,345", stat, "SELECT TO_CHAR(12345, '99,999') FROM DUAL");
assertResult(" 123,45", stat, "SELECT TO_CHAR(12345, '999,99') FROM DUAL");
assertResult("######", stat, "SELECT TO_CHAR(12345, '9.999') FROM DUAL");
assertResult("#######", stat, "SELECT TO_CHAR(12345, '99.999') FROM DUAL");
assertResult("########", stat, "SELECT TO_CHAR(12345, '999.999') FROM DUAL");
assertResult("#########", stat, "SELECT TO_CHAR(12345, '9999.999') FROM DUAL");
assertResult(" 12345.000", stat, "SELECT TO_CHAR(12345, '99999.999') FROM DUAL");
assertResult("###", stat, "SELECT TO_CHAR(12345, '$9') FROM DUAL");
assertResult("#####", stat, "SELECT TO_CHAR(12345, '$999') FROM DUAL");
assertResult("######", stat, "SELECT TO_CHAR(12345, '$9999') FROM DUAL");
assertResult(" " + cs + "12345", stat, "SELECT TO_CHAR(12345, '$99999999') FROM DUAL");
assertResult(" " + cs + "12,345.35", stat, "SELECT TO_CHAR(12345.345, '$99,999,999.99') FROM DUAL");
assertResult(" " + cs + "12,345", stat, "SELECT TO_CHAR(12345.345, '$99g999g999') FROM DUAL");
assertResult(" 12,345.35", stat, "SELECT TO_CHAR(12345.345, '99,999,999.99') FROM DUAL");
assertResult("12,345.35", stat, "SELECT TO_CHAR(12345.345, 'FM99,999,999.99') FROM DUAL");
assertResult(" 00,012,345.35", stat, "SELECT TO_CHAR(12345.345, '00,000,000.00') FROM DUAL");
assertResult("00,012,345.35", stat, "SELECT TO_CHAR(12345.345, 'FM00,000,000.00') FROM DUAL");
assertResult("###", stat, "SELECT TO_CHAR(12345, '09') FROM DUAL");
assertResult("#####", stat, "SELECT TO_CHAR(12345, '0999') FROM DUAL");
assertResult(" 00012345", stat, "SELECT TO_CHAR(12345, '09999999') FROM DUAL");
assertResult(" 0000012345", stat, "SELECT TO_CHAR(12345, '0009999999') FROM DUAL");
assertResult("###", stat, "SELECT TO_CHAR(12345, '90') FROM DUAL");
assertResult("#####", stat, "SELECT TO_CHAR(12345, '9990') FROM DUAL");
assertResult(" 12345", stat, "SELECT TO_CHAR(12345, '99999990') FROM DUAL");
assertResult(" 12345", stat, "SELECT TO_CHAR(12345, '9999999000') FROM DUAL");
assertResult(" 12345", stat, "SELECT TO_CHAR(12345, '9999999990') FROM DUAL");
assertResult("12345", stat, "SELECT TO_CHAR(12345, 'FM9999999990') FROM DUAL");
assertResult(" 12345.2300", stat, "SELECT TO_CHAR(12345.23, '9999999.9000') FROM DUAL");
assertResult(" 12345", stat, "SELECT TO_CHAR(12345, '9999999') FROM DUAL");
assertResult(" 12345", stat, "SELECT TO_CHAR(12345, '999999') FROM DUAL");
assertResult(" 12345", stat, "SELECT TO_CHAR(12345, '99999') FROM DUAL");
assertResult(" 12345", stat, "SELECT TO_CHAR(12345, '00000') FROM DUAL");
assertResult("#####", stat, "SELECT TO_CHAR(12345, '9999') FROM DUAL");
assertResult("#####", stat, "SELECT TO_CHAR(12345, '0000') FROM DUAL");
assertResult(" -12345", stat, "SELECT TO_CHAR(-12345, '99999999') FROM DUAL");
assertResult(" -12345", stat, "SELECT TO_CHAR(-12345, '9999999') FROM DUAL");
assertResult(" -12345", stat, "SELECT TO_CHAR(-12345, '999999') FROM DUAL");
assertResult("-12345", stat, "SELECT TO_CHAR(-12345, '99999') FROM DUAL");
assertResult("#####", stat, "SELECT TO_CHAR(-12345, '9999') FROM DUAL");
assertResult("####", stat, "SELECT TO_CHAR(-12345, '999') FROM DUAL");
assertResult(" 0", stat, "SELECT TO_CHAR(0, '9999999') FROM DUAL");
assertResult(" 00.30", stat, "SELECT TO_CHAR(0.3, '00.99') FROM DUAL");
assertResult("00.3", stat, "SELECT TO_CHAR(0.3, 'FM00.99') FROM DUAL");
assertResult(" 00.30", stat, "SELECT TO_CHAR(0.3, '00.00') FROM DUAL");
assertResult(" .30000", stat, "SELECT TO_CHAR(0.3, '99.00000') FROM DUAL");
assertResult(".30000", stat, "SELECT TO_CHAR(0.3, 'FM99.00000') FROM DUAL");
assertResult(" 00.30", stat, "SELECT TO_CHAR(0.3, 'B00.99') FROM DUAL");
assertResult(" .30", stat, "SELECT TO_CHAR(0.3, 'B99.99') FROM DUAL");
assertResult(" .30", stat, "SELECT TO_CHAR(0.3, '99.99') FROM DUAL");
assertResult(".3", stat, "SELECT TO_CHAR(0.3, 'FMB99.99') FROM DUAL");
assertResult(" 00.30", stat, "SELECT TO_CHAR(0.3, 'B09.99') FROM DUAL");
assertResult(" 00.30", stat, "SELECT TO_CHAR(0.3, 'B00.00') FROM DUAL");
assertResult(" " + cc + "123.45", stat, "SELECT TO_CHAR(123.45, 'C999.99') FROM DUAL");
assertResult(" -" + cc + "123.45", stat, "SELECT TO_CHAR(-123.45, 'C999.99') FROM DUAL");
assertResult(" " + cc + "123.45", stat, "SELECT TO_CHAR(123.45, 'C999,999.99') FROM DUAL");
assertResult(" " + cc + "123", stat, "SELECT TO_CHAR(123.45, 'C999g999') FROM DUAL");
assertResult(cc + "123.45", stat, "SELECT TO_CHAR(123.45, 'FMC999,999.99') FROM DUAL");
assertResult(" " + cs + "123.45", stat, "SELECT TO_CHAR(123.45, 'L999.99') FROM DUAL");
assertResult(" -" + cs + "123.45", stat, "SELECT TO_CHAR(-123.45, 'L999.99') FROM DUAL");
assertResult(cs + "123.45", stat, "SELECT TO_CHAR(123.45, 'FML999.99') FROM DUAL");
assertResult(" " + cs + "123.45", stat, "SELECT TO_CHAR(123.45, 'U999.99') FROM DUAL");
assertResult(" " + cs + "123.45", stat, "SELECT TO_CHAR(123.45, 'u999.99') FROM DUAL");
assertResult(" .33", stat, "SELECT TO_CHAR(0.326, '99D99') FROM DUAL");
assertResult(" 1.2E+02", stat, "SELECT TO_CHAR(123.456, '9.9EEEE') FROM DUAL");
assertResult(" 1.2E+14", stat, "SELECT TO_CHAR(123456789012345, '9.9EEEE') FROM DUAL");
assertResult(" 1E+02", stat, "SELECT TO_CHAR(123.456, '9EEEE') FROM DUAL");
assertResult(" 1E+02", stat, "SELECT TO_CHAR(123.456, '999EEEE') FROM DUAL");
assertResult(" 1E-03", stat, "SELECT TO_CHAR(.00123456, '999EEEE') FROM DUAL");
assertResult(" 1E+00", stat, "SELECT TO_CHAR(1, '999EEEE') FROM DUAL");
assertResult(" -1E+00", stat, "SELECT TO_CHAR(-1, '999EEEE') FROM DUAL");
assertResult(" 1.23456000E+02", stat, "SELECT TO_CHAR(123.456, '00.00000000EEEE') FROM DUAL");
assertResult("1.23456000E+02", stat, "SELECT TO_CHAR(123.456, 'fm00.00000000EEEE') FROM DUAL");
assertResult(" 1,234,567", stat, "SELECT TO_CHAR(1234567, '9G999G999') FROM DUAL");
assertResult("-1,234,567", stat, "SELECT TO_CHAR(-1234567, '9G999G999') FROM DUAL");
assertResult("123.45-", stat, "SELECT TO_CHAR(-123.45, '999.99MI') FROM DUAL");
assertResult("123.45-", stat, "SELECT TO_CHAR(-123.45, '999.99mi') FROM DUAL");
assertResult("123.45-", stat, "SELECT TO_CHAR(-123.45, '999.99mI') FROM DUAL");
assertResult("230.00-", stat, "SELECT TO_CHAR(-230, '999.99MI') FROM DUAL");
assertResult("230-", stat, "SELECT TO_CHAR(-230, '999MI') FROM DUAL");
assertResult("123.45 ", stat, "SELECT TO_CHAR(123.45, '999.99MI') FROM DUAL");
assertResult("230.00 ", stat, "SELECT TO_CHAR(230, '999.99MI') FROM DUAL");
assertResult("230 ", stat, "SELECT TO_CHAR(230, '999MI') FROM DUAL");
assertResult("230", stat, "SELECT TO_CHAR(230, 'FM999MI') FROM DUAL");
assertResult("<230>", stat, "SELECT TO_CHAR(-230, '999PR') FROM DUAL");
assertResult("<230>", stat, "SELECT TO_CHAR(-230, '999pr') FROM DUAL");
assertResult("<230>", stat, "SELECT TO_CHAR(-230, 'fm999pr') FROM DUAL");
assertResult(" 230 ", stat, "SELECT TO_CHAR(230, '999PR') FROM DUAL");
assertResult("230", stat, "SELECT TO_CHAR(230, 'FM999PR') FROM DUAL");
assertResult("0", stat, "SELECT TO_CHAR(0, 'fm999pr') FROM DUAL");
assertResult(" CCXXXVIII", stat, "SELECT TO_CHAR(238, 'RN') FROM DUAL");
assertResult("CCXXXVIII", stat, "SELECT TO_CHAR(238, 'FMRN') FROM DUAL");
assertResult("cxlix", stat, "SELECT TO_CHAR(149, 'FMrN') FROM DUAL");
assertResult(" MCMLXXIX", stat, "SELECT TO_CHAR(1979, 'RN') FROM DUAL;");
assertResult(" xliv", stat, "SELECT TO_CHAR(44, 'rN') FROM DUAL");
assertResult(" mdcclxxvi", stat, "SELECT TO_CHAR(1776, 'rn') FROM DUAL");
assertResult(" +42", stat, "SELECT TO_CHAR(42, 'S999') FROM DUAL");
assertResult(" +42", stat, "SELECT TO_CHAR(42, 's999') FROM DUAL");
assertResult(" 42+", stat, "SELECT TO_CHAR(42, '999S') FROM DUAL");
assertResult(" -42", stat, "SELECT TO_CHAR(-42, 'S999') FROM DUAL");
assertResult(" 42-", stat, "SELECT TO_CHAR(-42, '999S') FROM DUAL");
assertResult("42", stat, "SELECT TO_CHAR(42, 'TM') FROM DUAL");
assertResult("-42", stat, "SELECT TO_CHAR(-42, 'TM') FROM DUAL");
assertResult("4212341241234.23412342", stat, "SELECT TO_CHAR(4212341241234.23412342, 'tm') FROM DUAL");
assertResult(".23412342", stat, "SELECT TO_CHAR(0.23412342, 'tm') FROM DUAL");
assertResult(" 12300", stat, "SELECT TO_CHAR(123, '999V99') FROM DUAL");
assertResult("######", stat, "SELECT TO_CHAR(1234, '999V99') FROM DUAL");
assertResult("123400", stat, "SELECT TO_CHAR(1234, 'FM9999v99') FROM DUAL");
assertResult("1234", stat, "SELECT TO_CHAR(123.4, 'FM9999V9') FROM DUAL");
assertResult("123", stat, "SELECT TO_CHAR(123.4, 'FM9999V') FROM DUAL");
assertResult("123400000", stat, "SELECT TO_CHAR(123.4, 'FM9999V090909') FROM DUAL");
assertResult("##", stat, "SELECT TO_CHAR(123, 'X') FROM DUAL");
assertResult(" 7B", stat, "SELECT TO_CHAR(123, 'XX') FROM DUAL");
assertResult(" 7b", stat, "SELECT TO_CHAR(123, 'Xx') FROM DUAL");
assertResult(" 7b", stat, "SELECT TO_CHAR(123, 'xX') FROM DUAL");
assertResult(" 7B", stat, "SELECT TO_CHAR(123, 'XXXX') FROM DUAL");
assertResult(" 007B", stat, "SELECT TO_CHAR(123, '000X') FROM DUAL");
assertResult(" 007B", stat, "SELECT TO_CHAR(123, '0XXX') FROM DUAL");
assertResult("####", stat, "SELECT TO_CHAR(123456789, 'FMXXX') FROM DUAL");
assertResult("7B", stat, "SELECT TO_CHAR(123, 'FMXX') FROM DUAL");
assertResult("C6", stat, "SELECT TO_CHAR(197.6, 'FMXX') FROM DUAL");
assertResult(" 7", stat, "SELECT TO_CHAR(7, 'XX') FROM DUAL");
assertResult("123", stat, "SELECT TO_CHAR(123, 'TM') FROM DUAL");
assertResult("123", stat, "SELECT TO_CHAR(123, 'tm') FROM DUAL");
assertResult("123", stat, "SELECT TO_CHAR(123, 'tM9') FROM DUAL");
assertResult("1.23E+02", stat, "SELECT TO_CHAR(123, 'TME') FROM DUAL");
assertResult("1.23456789012345E+14", stat, "SELECT TO_CHAR(123456789012345, 'TME') FROM DUAL");
assertResult("4.5E-01", stat, "SELECT TO_CHAR(0.45, 'TME') FROM DUAL");
assertResult("4.5E-01", stat, "SELECT TO_CHAR(0.45, 'tMe') FROM DUAL");
assertThrows("Invalid format \"999.99q\"", stat, "SELECT TO_CHAR(123.45, '999.99q') FROM DUAL");
assertThrows("Invalid format \"fm999.99q\"", stat, "SELECT TO_CHAR(123.45, 'fm999.99q') FROM DUAL");
assertThrows("Invalid format \"q999.99\"", stat, "SELECT TO_CHAR(123.45, 'q999.99') FROM DUAL");
conn.close();
}
private void testToCharFromText() throws SQLException {
deleteDb("functions");
Connection conn = getConnection("functions");
Statement stat = conn.createStatement();
assertResult("abc", stat, "SELECT TO_CHAR('abc') FROM DUAL");
conn.close();
}
private void testCachingOfDeterministicFunctionAlias() throws SQLException { private void testCachingOfDeterministicFunctionAlias() throws SQLException {
deleteDb("functions"); deleteDb("functions");
Connection conn = getConnection("functions"); Connection conn = getConnection("functions");
......
...@@ -743,4 +743,8 @@ lives pauses allocates kicks introduction straightforward getenv ...@@ -743,4 +743,8 @@ lives pauses allocates kicks introduction straightforward getenv
ordinate tweaking fetching rfe yates cookie btrfs cookies ordinate tweaking fetching rfe yates cookie btrfs cookies
nocycle nomaxvalue nominvalue cycling proceed prospective exhausted contingent nocycle nomaxvalue nominvalue cycling proceed prospective exhausted contingent
validities hang degenerates freezes emulation gredler cemo koc blanked validities hang degenerates freezes emulation gredler cemo koc blanked
reverting blanked jump reverting blanked jump capitalization capitalize symbol symbols verbatim
\ No newline at end of file closest resultant savings designator numeral numerals lowercased uppercased
casing epoch century abbreviation scientific circuit emulates blanks substrings
thai tme fmrn fmxxx fmday fml syyyy nov iyy iyyy syear scc fmc fmb fmxx tzr tzd
btc mcmlxxix xliv mdcclxxvi ccxxxviii xii cxlix yyfxyy
\ No newline at end of file
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论