From 0bdf8ff24d33a920675013d410fe26a7d8334dfd Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Sat, 18 Jul 2026 19:50:45 +0900 Subject: [PATCH 1/3] Add CDC text formatting APIs --- java_runtime/src/classes/java.rs | 1 + java_runtime/src/classes/java/text.rs | 13 + .../src/classes/java/text/date_format.rs | 580 ++++++++++++++++++ .../src/classes/java/text/decimal_format.rs | 525 ++++++++++++++++ .../src/classes/java/text/field_position.rs | 101 +++ java_runtime/src/classes/java/text/format.rs | 113 ++++ .../src/classes/java/text/number_format.rs | 531 ++++++++++++++++ .../src/classes/java/text/parse_exception.rs | 43 ++ .../src/classes/java/text/parse_position.rs | 89 +++ .../classes/java/text/simple_date_format.rs | 564 +++++++++++++++++ .../src/classes/java/util/calendar.rs | 33 +- java_runtime/src/loader.rs | 8 + java_runtime/tests/classes/java/mod.rs | 1 + java_runtime/tests/classes/java/text/mod.rs | 3 + .../classes/java/text/test_date_format.rs | 186 ++++++ .../tests/classes/java/text/test_format.rs | 110 ++++ .../classes/java/text/test_number_format.rs | 171 ++++++ 17 files changed, 3071 insertions(+), 1 deletion(-) create mode 100644 java_runtime/src/classes/java/text.rs create mode 100644 java_runtime/src/classes/java/text/date_format.rs create mode 100644 java_runtime/src/classes/java/text/decimal_format.rs create mode 100644 java_runtime/src/classes/java/text/field_position.rs create mode 100644 java_runtime/src/classes/java/text/format.rs create mode 100644 java_runtime/src/classes/java/text/number_format.rs create mode 100644 java_runtime/src/classes/java/text/parse_exception.rs create mode 100644 java_runtime/src/classes/java/text/parse_position.rs create mode 100644 java_runtime/src/classes/java/text/simple_date_format.rs create mode 100644 java_runtime/tests/classes/java/text/mod.rs create mode 100644 java_runtime/tests/classes/java/text/test_date_format.rs create mode 100644 java_runtime/tests/classes/java/text/test_format.rs create mode 100644 java_runtime/tests/classes/java/text/test_number_format.rs diff --git a/java_runtime/src/classes/java.rs b/java_runtime/src/classes/java.rs index bb72b385..45ac1f64 100644 --- a/java_runtime/src/classes/java.rs +++ b/java_runtime/src/classes/java.rs @@ -1,4 +1,5 @@ pub mod io; pub mod lang; pub mod net; +pub mod text; pub mod util; diff --git a/java_runtime/src/classes/java/text.rs b/java_runtime/src/classes/java/text.rs new file mode 100644 index 00000000..ec779d1d --- /dev/null +++ b/java_runtime/src/classes/java/text.rs @@ -0,0 +1,13 @@ +mod date_format; +mod decimal_format; +mod field_position; +mod format; +mod number_format; +mod parse_exception; +mod parse_position; +mod simple_date_format; + +pub use self::{ + date_format::DateFormat, decimal_format::DecimalFormat, field_position::FieldPosition, format::Format, number_format::NumberFormat, + parse_exception::ParseException, parse_position::ParsePosition, simple_date_format::SimpleDateFormat, +}; diff --git a/java_runtime/src/classes/java/text/date_format.rs b/java_runtime/src/classes/java/text/date_format.rs new file mode 100644 index 00000000..eea38e37 --- /dev/null +++ b/java_runtime/src/classes/java/text/date_format.rs @@ -0,0 +1,580 @@ +use alloc::{format, string::ToString, vec}; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{Array, ClassInstanceRef, JavaError, Jvm, Result, runtime::JavaLangString}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::{ + lang::{Object, String, StringBuffer}, + text::{FieldPosition, NumberFormat, ParseException, ParsePosition}, + util::{Calendar, Date, Locale, TimeZone}, + }, +}; + +// public abstract class java.text.DateFormat +pub struct DateFormat; + +impl DateFormat { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/text/DateFormat", + parent_class: Some("java/text/Format"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::clinit, MethodAccessFlags::STATIC), + JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PROTECTED), + JavaMethodProto::new( + "format", + "(Ljava/lang/Object;Ljava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + Self::format_object, + MethodAccessFlags::PUBLIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "format", + "(Ljava/util/Date;)Ljava/lang/String;", + Self::format_date, + MethodAccessFlags::PUBLIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new_abstract( + "format", + "(Ljava/util/Date;Ljava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT, + ), + JavaMethodProto::new("parse", "(Ljava/lang/String;)Ljava/util/Date;", Self::parse, MethodAccessFlags::PUBLIC), + JavaMethodProto::new_abstract( + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/util/Date;", + MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT, + ), + JavaMethodProto::new( + "parseObject", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/lang/Object;", + Self::parse_object, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "getInstance", + "()Ljava/text/DateFormat;", + Self::get_instance, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "getTimeInstance", + "()Ljava/text/DateFormat;", + Self::get_time_instance, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "getTimeInstance", + "(I)Ljava/text/DateFormat;", + Self::get_time_instance_with_style, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "getTimeInstance", + "(ILjava/util/Locale;)Ljava/text/DateFormat;", + Self::get_time_instance_with_locale, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "getDateInstance", + "()Ljava/text/DateFormat;", + Self::get_date_instance, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "getDateInstance", + "(I)Ljava/text/DateFormat;", + Self::get_date_instance_with_style, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "getDateInstance", + "(ILjava/util/Locale;)Ljava/text/DateFormat;", + Self::get_date_instance_with_locale, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "getDateTimeInstance", + "()Ljava/text/DateFormat;", + Self::get_date_time_instance, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "getDateTimeInstance", + "(II)Ljava/text/DateFormat;", + Self::get_date_time_instance_with_styles, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "getDateTimeInstance", + "(IILjava/util/Locale;)Ljava/text/DateFormat;", + Self::get_date_time_instance_with_locale, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "getAvailableLocales", + "()[Ljava/util/Locale;", + Self::get_available_locales, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new("getCalendar", "()Ljava/util/Calendar;", Self::get_calendar, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("setCalendar", "(Ljava/util/Calendar;)V", Self::set_calendar, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "getNumberFormat", + "()Ljava/text/NumberFormat;", + Self::get_number_format, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "setNumberFormat", + "(Ljava/text/NumberFormat;)V", + Self::set_number_format, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("getTimeZone", "()Ljava/util/TimeZone;", Self::get_time_zone, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("setTimeZone", "(Ljava/util/TimeZone;)V", Self::set_time_zone, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("isLenient", "()Z", Self::is_lenient, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("setLenient", "(Z)V", Self::set_lenient, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("clone", "()Ljava/lang/Object;", Self::clone, MethodAccessFlags::PUBLIC), + ], + fields: vec![ + JavaFieldProto::new("calendar", "Ljava/util/Calendar;", FieldAccessFlags::PROTECTED), + JavaFieldProto::new("numberFormat", "Ljava/text/NumberFormat;", FieldAccessFlags::PROTECTED), + JavaFieldProto::new( + "ERA_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "YEAR_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "MONTH_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "DATE_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "HOUR_OF_DAY1_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "HOUR_OF_DAY0_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "MINUTE_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "SECOND_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "MILLISECOND_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "DAY_OF_WEEK_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "DAY_OF_YEAR_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "DAY_OF_WEEK_IN_MONTH_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "WEEK_OF_YEAR_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "WEEK_OF_MONTH_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "AM_PM_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "HOUR1_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "HOUR0_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "TIMEZONE_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new("FULL", "I", FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL), + JavaFieldProto::new("LONG", "I", FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL), + JavaFieldProto::new( + "MEDIUM", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "SHORT", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "DEFAULT", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + ], + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::ABSTRACT, + } + } + + async fn clinit(jvm: &Jvm, _: &mut RuntimeContext) -> Result<()> { + for (name, value) in [ + ("ERA_FIELD", 0), + ("YEAR_FIELD", 1), + ("MONTH_FIELD", 2), + ("DATE_FIELD", 3), + ("HOUR_OF_DAY1_FIELD", 4), + ("HOUR_OF_DAY0_FIELD", 5), + ("MINUTE_FIELD", 6), + ("SECOND_FIELD", 7), + ("MILLISECOND_FIELD", 8), + ("DAY_OF_WEEK_FIELD", 9), + ("DAY_OF_YEAR_FIELD", 10), + ("DAY_OF_WEEK_IN_MONTH_FIELD", 11), + ("WEEK_OF_YEAR_FIELD", 12), + ("WEEK_OF_MONTH_FIELD", 13), + ("AM_PM_FIELD", 14), + ("HOUR1_FIELD", 15), + ("HOUR0_FIELD", 16), + ("TIMEZONE_FIELD", 17), + ("FULL", 0), + ("LONG", 1), + ("MEDIUM", 2), + ("SHORT", 3), + ("DEFAULT", 2), + ] { + jvm.put_static_field("java/text/DateFormat", name, "I", value).await?; + } + Ok(()) + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { + let _: () = jvm.invoke_special(&this, "java/text/Format", "", "()V", ()).await?; + let calendar: ClassInstanceRef = jvm + .invoke_static("java/util/Calendar", "getInstance", "()Ljava/util/Calendar;", ()) + .await?; + let number_format: ClassInstanceRef = jvm + .invoke_static("java/text/NumberFormat", "getInstance", "()Ljava/text/NumberFormat;", ()) + .await?; + jvm.put_field(&mut this, "calendar", "Ljava/util/Calendar;", calendar).await?; + jvm.put_field(&mut this, "numberFormat", "Ljava/text/NumberFormat;", number_format).await + } + + async fn format_object( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + object: ClassInstanceRef, + buffer: ClassInstanceRef, + position: ClassInstanceRef, + ) -> Result> { + if object.is_null() || !jvm.is_instance(&**object, "java/util/Date") { + return Err(jvm + .exception("java/lang/IllegalArgumentException", "Cannot format given Object as a Date") + .await); + } + let date: ClassInstanceRef = ClassInstanceRef::new(object.instance); + jvm.invoke_virtual( + &this, + "format", + "(Ljava/util/Date;Ljava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + (date, buffer, position), + ) + .await + } + + async fn format_date( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + date: ClassInstanceRef, + ) -> Result> { + if date.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "date").await); + } + let buffer: ClassInstanceRef = jvm.new_class("java/lang/StringBuffer", "()V", ()).await?.into(); + let position: ClassInstanceRef = jvm.new_class("java/text/FieldPosition", "(I)V", (0,)).await?.into(); + let buffer: ClassInstanceRef = jvm + .invoke_virtual( + &this, + "format", + "(Ljava/util/Date;Ljava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + (date, buffer, position), + ) + .await?; + jvm.invoke_virtual(&buffer, "toString", "()Ljava/lang/String;", ()).await + } + + async fn parse( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + source: ClassInstanceRef, + ) -> Result> { + if source.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "source").await); + } + let position: ClassInstanceRef = jvm.new_class("java/text/ParsePosition", "(I)V", (0,)).await?.into(); + let date: ClassInstanceRef = jvm + .invoke_virtual( + &this, + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/util/Date;", + (source, position.clone()), + ) + .await?; + let index: i32 = jvm.invoke_virtual(&position, "getIndex", "()I", ()).await?; + if index == 0 { + let error_index: i32 = jvm.invoke_virtual(&position, "getErrorIndex", "()I", ()).await?; + let message = JavaLangString::from_rust_string(jvm, "Unparseable date").await?; + let exception: ClassInstanceRef = jvm + .new_class("java/text/ParseException", "(Ljava/lang/String;I)V", (message, error_index)) + .await? + .into(); + return Err(JavaError::JavaException(exception.into())); + } + Ok(date) + } + + async fn parse_object( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + source: ClassInstanceRef, + position: ClassInstanceRef, + ) -> Result> { + let date: ClassInstanceRef = jvm + .invoke_virtual( + &this, + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/util/Date;", + (source, position), + ) + .await?; + Ok(ClassInstanceRef::new(date.instance)) + } + + fn date_pattern(style: i32) -> Option<&'static str> { + match style { + 0 => Some("EEEE, MMMM d, yyyy"), + 1 => Some("MMMM d, yyyy"), + 2 => Some("MMM d, yyyy"), + 3 => Some("M/d/yy"), + _ => None, + } + } + + fn time_pattern(style: i32) -> Option<&'static str> { + match style { + 0 | 1 => Some("h:mm:ss a z"), + 2 => Some("h:mm:ss a"), + 3 => Some("h:mm a"), + _ => None, + } + } + + async fn new_formatter( + jvm: &Jvm, + date_style: Option, + time_style: Option, + locale: ClassInstanceRef, + ) -> Result> { + if locale.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "locale").await); + } + let date_pattern = date_style.and_then(Self::date_pattern); + let time_pattern = time_style.and_then(Self::time_pattern); + if (date_style.is_some() && date_pattern.is_none()) || (time_style.is_some() && time_pattern.is_none()) { + return Err(jvm.exception("java/lang/IllegalArgumentException", "Illegal date style").await); + } + let pattern = match (date_pattern, time_pattern) { + (Some(date), Some(time)) => format!("{date} {time}"), + (Some(date), None) => date.to_string(), + (None, Some(time)) => time.to_string(), + (None, None) => return Err(jvm.exception("java/lang/IllegalArgumentException", "No date or time style").await), + }; + let pattern = JavaLangString::from_rust_string(jvm, &pattern).await?; + Ok(jvm + .new_class("java/text/SimpleDateFormat", "(Ljava/lang/String;Ljava/util/Locale;)V", (pattern, locale)) + .await? + .into()) + } + + async fn get_instance(jvm: &Jvm, _: &mut RuntimeContext) -> Result> { + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + Self::new_formatter(jvm, Some(3), Some(3), locale).await + } + + async fn get_time_instance(jvm: &Jvm, _: &mut RuntimeContext) -> Result> { + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + Self::new_formatter(jvm, None, Some(2), locale).await + } + + async fn get_time_instance_with_style(jvm: &Jvm, _: &mut RuntimeContext, style: i32) -> Result> { + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + Self::new_formatter(jvm, None, Some(style), locale).await + } + + async fn get_time_instance_with_locale( + jvm: &Jvm, + _: &mut RuntimeContext, + style: i32, + locale: ClassInstanceRef, + ) -> Result> { + Self::new_formatter(jvm, None, Some(style), locale).await + } + + async fn get_date_instance(jvm: &Jvm, _: &mut RuntimeContext) -> Result> { + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + Self::new_formatter(jvm, Some(2), None, locale).await + } + + async fn get_date_instance_with_style(jvm: &Jvm, _: &mut RuntimeContext, style: i32) -> Result> { + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + Self::new_formatter(jvm, Some(style), None, locale).await + } + + async fn get_date_instance_with_locale( + jvm: &Jvm, + _: &mut RuntimeContext, + style: i32, + locale: ClassInstanceRef, + ) -> Result> { + Self::new_formatter(jvm, Some(style), None, locale).await + } + + async fn get_date_time_instance(jvm: &Jvm, _: &mut RuntimeContext) -> Result> { + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + Self::new_formatter(jvm, Some(2), Some(2), locale).await + } + + async fn get_date_time_instance_with_styles( + jvm: &Jvm, + _: &mut RuntimeContext, + date_style: i32, + time_style: i32, + ) -> Result> { + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + Self::new_formatter(jvm, Some(date_style), Some(time_style), locale).await + } + + async fn get_date_time_instance_with_locale( + jvm: &Jvm, + _: &mut RuntimeContext, + date_style: i32, + time_style: i32, + locale: ClassInstanceRef, + ) -> Result> { + Self::new_formatter(jvm, Some(date_style), Some(time_style), locale).await + } + + async fn get_available_locales(jvm: &Jvm, _: &mut RuntimeContext) -> Result>> { + jvm.invoke_static("java/util/Locale", "getAvailableLocales", "()[Ljava/util/Locale;", ()) + .await + } + + async fn get_calendar(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + jvm.get_field(&this, "calendar", "Ljava/util/Calendar;").await + } + + async fn set_calendar(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, calendar: ClassInstanceRef) -> Result<()> { + if calendar.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "calendar").await); + } + jvm.put_field(&mut this, "calendar", "Ljava/util/Calendar;", calendar).await + } + + async fn get_number_format(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + jvm.get_field(&this, "numberFormat", "Ljava/text/NumberFormat;").await + } + + async fn set_number_format( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + number_format: ClassInstanceRef, + ) -> Result<()> { + if number_format.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "numberFormat").await); + } + jvm.put_field(&mut this, "numberFormat", "Ljava/text/NumberFormat;", number_format).await + } + + async fn get_time_zone(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let calendar: ClassInstanceRef = jvm.get_field(&this, "calendar", "Ljava/util/Calendar;").await?; + jvm.invoke_virtual(&calendar, "getTimeZone", "()Ljava/util/TimeZone;", ()).await + } + + async fn set_time_zone(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, time_zone: ClassInstanceRef) -> Result<()> { + let calendar: ClassInstanceRef = jvm.get_field(&this, "calendar", "Ljava/util/Calendar;").await?; + jvm.invoke_virtual(&calendar, "setTimeZone", "(Ljava/util/TimeZone;)V", (time_zone,)) + .await + } + + async fn is_lenient(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let calendar: ClassInstanceRef = jvm.get_field(&this, "calendar", "Ljava/util/Calendar;").await?; + jvm.invoke_virtual(&calendar, "isLenient", "()Z", ()).await + } + + async fn set_lenient(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, lenient: bool) -> Result<()> { + let calendar: ClassInstanceRef = jvm.get_field(&this, "calendar", "Ljava/util/Calendar;").await?; + jvm.invoke_virtual(&calendar, "setLenient", "(Z)V", (lenient,)).await + } + + async fn clone(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let calendar: ClassInstanceRef = jvm.get_field(&this, "calendar", "Ljava/util/Calendar;").await?; + let number_format: ClassInstanceRef = jvm.get_field(&this, "numberFormat", "Ljava/text/NumberFormat;").await?; + let mut cloned: ClassInstanceRef = jvm.shallow_clone(&this)?.into(); + let mut cloned_calendar: ClassInstanceRef = jvm.shallow_clone(&calendar)?.into(); + let fields: ClassInstanceRef> = jvm.get_field(&calendar, "fields", "[I").await?; + let cloned_fields: ClassInstanceRef> = jvm.shallow_clone(&fields)?.into(); + let time_zone: ClassInstanceRef = jvm.get_field(&calendar, "timeZone", "Ljava/util/TimeZone;").await?; + let cloned_time_zone: ClassInstanceRef = jvm.shallow_clone(&time_zone)?.into(); + jvm.put_field(&mut cloned_calendar, "fields", "[I", cloned_fields).await?; + jvm.put_field(&mut cloned_calendar, "timeZone", "Ljava/util/TimeZone;", cloned_time_zone) + .await?; + let cloned_number_format: ClassInstanceRef = jvm.shallow_clone(&number_format)?.into(); + jvm.put_field(&mut cloned, "calendar", "Ljava/util/Calendar;", cloned_calendar).await?; + jvm.put_field(&mut cloned, "numberFormat", "Ljava/text/NumberFormat;", cloned_number_format) + .await?; + Ok(ClassInstanceRef::new(cloned.instance)) + } +} diff --git a/java_runtime/src/classes/java/text/decimal_format.rs b/java_runtime/src/classes/java/text/decimal_format.rs new file mode 100644 index 00000000..71e90808 --- /dev/null +++ b/java_runtime/src/classes/java/text/decimal_format.rs @@ -0,0 +1,525 @@ +use alloc::{ + format, + string::{String as RustString, ToString}, + vec, + vec::Vec, +}; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::{ + lang::{Double, Long, Number, String, StringBuffer}, + text::{FieldPosition, ParsePosition}, + }, +}; + +struct DecimalPattern { + positive_prefix: RustString, + positive_suffix: RustString, + negative_prefix: RustString, + negative_suffix: RustString, + minimum_integer_digits: i32, + maximum_fraction_digits: i32, + minimum_fraction_digits: i32, + grouping_used: bool, + grouping_size: i32, + multiplier: i32, +} + +// public class java.text.DecimalFormat +pub struct DecimalFormat; + +impl DecimalFormat { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/text/DecimalFormat", + parent_class: Some("java/text/NumberFormat"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_pattern, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "format", + "(DLjava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + Self::format_double, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "format", + "(JLjava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + Self::format_long, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/lang/Number;", + Self::parse, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("applyPattern", "(Ljava/lang/String;)V", Self::apply_pattern, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("toPattern", "()Ljava/lang/String;", Self::to_pattern, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getMultiplier", "()I", Self::get_multiplier, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("setMultiplier", "(I)V", Self::set_multiplier, MethodAccessFlags::PUBLIC), + ], + fields: vec![ + JavaFieldProto::new("pattern", "Ljava/lang/String;", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("positivePrefix", "Ljava/lang/String;", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("positiveSuffix", "Ljava/lang/String;", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("negativePrefix", "Ljava/lang/String;", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("negativeSuffix", "Ljava/lang/String;", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("multiplier", "I", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("groupingSize", "I", FieldAccessFlags::PRIVATE), + ], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + fn parse_affix(value: &[char]) -> Option<(RustString, bool)> { + let mut result = RustString::new(); + let mut percent = false; + let mut quoted = false; + let mut index = 0; + while index < value.len() { + if value[index] == '\'' { + if index + 1 < value.len() && value[index + 1] == '\'' { + result.push('\''); + index += 2; + continue; + } + quoted = !quoted; + index += 1; + continue; + } + if !quoted && value[index] == '\u{00a4}' { + result.push('$'); + } else { + if !quoted && value[index] == '%' { + percent = true; + } + result.push(value[index]); + } + index += 1; + } + if quoted { None } else { Some((result, percent)) } + } + + fn parse_subpattern(value: &[char]) -> Option<(RustString, Vec, RustString, bool)> { + let mut quoted = false; + let mut numeric_start = None; + let mut index = 0; + while index < value.len() { + if value[index] == '\'' { + if index + 1 < value.len() && value[index + 1] == '\'' { + index += 2; + continue; + } + quoted = !quoted; + } else if !quoted && matches!(value[index], '#' | '0') { + numeric_start = Some(index); + break; + } + index += 1; + } + let numeric_start = numeric_start?; + let mut numeric_end = numeric_start; + while numeric_end < value.len() && matches!(value[numeric_end], '#' | '0' | ',' | '.') { + numeric_end += 1; + } + quoted = false; + index = numeric_end; + while index < value.len() { + if value[index] == '\'' { + if index + 1 < value.len() && value[index + 1] == '\'' { + index += 2; + continue; + } + quoted = !quoted; + } else if !quoted && matches!(value[index], '#' | '0' | ',' | '.') { + return None; + } + index += 1; + } + let (prefix, prefix_percent) = Self::parse_affix(&value[..numeric_start])?; + let (suffix, suffix_percent) = Self::parse_affix(&value[numeric_end..])?; + Some(( + prefix, + value[numeric_start..numeric_end].to_vec(), + suffix, + prefix_percent || suffix_percent, + )) + } + + fn parse_pattern_value(pattern: &str) -> Option { + let characters: Vec = pattern.chars().collect(); + let mut quoted = false; + let mut separator = None; + let mut index = 0; + while index < characters.len() { + if characters[index] == '\'' { + if index + 1 < characters.len() && characters[index + 1] == '\'' { + index += 2; + continue; + } + quoted = !quoted; + } else if !quoted && characters[index] == ';' { + if separator.is_some() { + return None; + } + separator = Some(index); + } + index += 1; + } + if quoted || characters.is_empty() { + return None; + } + + let positive = &characters[..separator.unwrap_or(characters.len())]; + let (positive_prefix, number, positive_suffix, positive_percent) = Self::parse_subpattern(positive)?; + if number.iter().filter(|character| **character == '.').count() > 1 { + return None; + } + let decimal_index = number.iter().position(|character| *character == '.').unwrap_or(number.len()); + let integer_pattern = &number[..decimal_index]; + let fraction_pattern = if decimal_index < number.len() { + &number[decimal_index + 1..] + } else { + &[] + }; + if integer_pattern.is_empty() + || integer_pattern.iter().any(|character| !matches!(character, '#' | '0' | ',')) + || fraction_pattern.iter().any(|character| !matches!(character, '#' | '0')) + { + return None; + } + let minimum_integer_digits = integer_pattern.iter().filter(|character| **character == '0').count() as i32; + let maximum_fraction_digits = fraction_pattern.len() as i32; + let minimum_fraction_digits = fraction_pattern.iter().filter(|character| **character == '0').count() as i32; + let grouping_position = integer_pattern.iter().rposition(|character| *character == ','); + let grouping_size = grouping_position + .map(|position| { + integer_pattern[position + 1..] + .iter() + .filter(|character| matches!(character, '#' | '0')) + .count() as i32 + }) + .unwrap_or(0); + if grouping_position.is_some() && grouping_size == 0 { + return None; + } + + let (negative_prefix, negative_suffix, negative_percent) = if let Some(separator) = separator { + let (prefix, negative_number, suffix, percent) = Self::parse_subpattern(&characters[separator + 1..])?; + if negative_number != number { + return None; + } + (prefix, suffix, percent) + } else { + (format!("-{positive_prefix}"), positive_suffix.clone(), positive_percent) + }; + + Some(DecimalPattern { + positive_prefix, + positive_suffix, + negative_prefix, + negative_suffix, + minimum_integer_digits, + maximum_fraction_digits, + minimum_fraction_digits, + grouping_used: grouping_position.is_some(), + grouping_size, + multiplier: if positive_percent || negative_percent { 100 } else { 1 }, + }) + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + let pattern = JavaLangString::from_rust_string(jvm, "#,##0.###").await?; + jvm.invoke_special(&this, "java/text/DecimalFormat", "", "(Ljava/lang/String;)V", (pattern,)) + .await + } + + async fn init_with_pattern(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, pattern: ClassInstanceRef) -> Result<()> { + if pattern.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "pattern").await); + } + let _: () = jvm.invoke_special(&this, "java/text/NumberFormat", "", "()V", ()).await?; + jvm.invoke_virtual(&this, "applyPattern", "(Ljava/lang/String;)V", (pattern,)).await + } + + async fn apply_pattern(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, pattern: ClassInstanceRef) -> Result<()> { + if pattern.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "pattern").await); + } + let value = JavaLangString::to_rust_string(jvm, &pattern).await?; + let Some(parsed) = Self::parse_pattern_value(&value) else { + return Err(jvm.exception("java/lang/IllegalArgumentException", "Malformed pattern").await); + }; + + let positive_prefix = JavaLangString::from_rust_string(jvm, &parsed.positive_prefix).await?; + let positive_suffix = JavaLangString::from_rust_string(jvm, &parsed.positive_suffix).await?; + let negative_prefix = JavaLangString::from_rust_string(jvm, &parsed.negative_prefix).await?; + let negative_suffix = JavaLangString::from_rust_string(jvm, &parsed.negative_suffix).await?; + jvm.put_field(&mut this, "pattern", "Ljava/lang/String;", pattern).await?; + jvm.put_field(&mut this, "positivePrefix", "Ljava/lang/String;", positive_prefix).await?; + jvm.put_field(&mut this, "positiveSuffix", "Ljava/lang/String;", positive_suffix).await?; + jvm.put_field(&mut this, "negativePrefix", "Ljava/lang/String;", negative_prefix).await?; + jvm.put_field(&mut this, "negativeSuffix", "Ljava/lang/String;", negative_suffix).await?; + jvm.put_field(&mut this, "multiplier", "I", parsed.multiplier).await?; + jvm.put_field(&mut this, "groupingSize", "I", parsed.grouping_size).await?; + jvm.put_field(&mut this, "groupingUsed", "Z", parsed.grouping_used).await?; + jvm.put_field(&mut this, "maximumIntegerDigits", "I", 309).await?; + jvm.put_field(&mut this, "minimumIntegerDigits", "I", parsed.minimum_integer_digits) + .await?; + jvm.put_field(&mut this, "maximumFractionDigits", "I", parsed.maximum_fraction_digits) + .await?; + jvm.put_field(&mut this, "minimumFractionDigits", "I", parsed.minimum_fraction_digits) + .await + } + + async fn append_formatted( + jvm: &Jvm, + this: &ClassInstanceRef, + negative: bool, + mut integer: RustString, + mut fraction: RustString, + buffer: ClassInstanceRef, + position: ClassInstanceRef, + ) -> Result> { + if buffer.is_null() || position.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "buffer or position").await); + } + let minimum_integer_digits: i32 = jvm.get_field(this, "minimumIntegerDigits", "I").await?; + let maximum_integer_digits: i32 = jvm.get_field(this, "maximumIntegerDigits", "I").await?; + let minimum_fraction_digits: i32 = jvm.get_field(this, "minimumFractionDigits", "I").await?; + let grouping_used: bool = jvm.get_field(this, "groupingUsed", "Z").await?; + let grouping_size: i32 = jvm.get_field(this, "groupingSize", "I").await?; + + if integer.len() > maximum_integer_digits.max(0) as usize { + integer = integer[integer.len() - maximum_integer_digits.max(0) as usize..].to_string(); + } + while integer.len() < minimum_integer_digits.max(0) as usize { + integer.insert(0, '0'); + } + if integer.is_empty() { + integer.push('0'); + } + if grouping_used && grouping_size > 0 { + let mut grouped = RustString::new(); + for (index, character) in integer.chars().rev().enumerate() { + if index > 0 && index % grouping_size as usize == 0 { + grouped.push(','); + } + grouped.push(character); + } + integer = grouped.chars().rev().collect(); + } + while fraction.ends_with('0') && fraction.len() > minimum_fraction_digits.max(0) as usize { + fraction.pop(); + } + while fraction.len() < minimum_fraction_digits.max(0) as usize { + fraction.push('0'); + } + + let prefix: ClassInstanceRef = jvm + .get_field(this, if negative { "negativePrefix" } else { "positivePrefix" }, "Ljava/lang/String;") + .await?; + let suffix: ClassInstanceRef = jvm + .get_field(this, if negative { "negativeSuffix" } else { "positiveSuffix" }, "Ljava/lang/String;") + .await?; + let prefix = JavaLangString::to_rust_string(jvm, &prefix).await?; + let suffix = JavaLangString::to_rust_string(jvm, &suffix).await?; + let mut formatted = format!("{prefix}{integer}"); + if !fraction.is_empty() { + formatted.push('.'); + formatted.push_str(&fraction); + } + formatted.push_str(&suffix); + + let base: i32 = jvm.invoke_virtual(&buffer, "length", "()I", ()).await?; + let field: i32 = jvm.invoke_virtual(&position, "getField", "()I", ()).await?; + if field == 0 { + let begin = base + prefix.chars().count() as i32; + let _: () = jvm.invoke_virtual(&position, "setBeginIndex", "(I)V", (begin,)).await?; + let _: () = jvm + .invoke_virtual(&position, "setEndIndex", "(I)V", (begin + integer.chars().count() as i32,)) + .await?; + } else if field == 1 && !fraction.is_empty() { + let begin = base + prefix.chars().count() as i32 + integer.chars().count() as i32 + 1; + let _: () = jvm.invoke_virtual(&position, "setBeginIndex", "(I)V", (begin,)).await?; + let _: () = jvm + .invoke_virtual(&position, "setEndIndex", "(I)V", (begin + fraction.chars().count() as i32,)) + .await?; + } + + let text = JavaLangString::from_rust_string(jvm, &formatted).await?; + jvm.invoke_virtual(&buffer, "append", "(Ljava/lang/String;)Ljava/lang/StringBuffer;", (text,)) + .await + } + + async fn format_double( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + value: f64, + buffer: ClassInstanceRef, + position: ClassInstanceRef, + ) -> Result> { + if buffer.is_null() || position.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "buffer or position").await); + } + if value.is_nan() { + let text = JavaLangString::from_rust_string(jvm, "NaN").await?; + return jvm + .invoke_virtual(&buffer, "append", "(Ljava/lang/String;)Ljava/lang/StringBuffer;", (text,)) + .await; + } + + let negative = value.is_sign_negative(); + let multiplier: i32 = jvm.get_field(&this, "multiplier", "I").await?; + let scaled = value.abs() * f64::from(multiplier); + if scaled.is_infinite() { + return Self::append_formatted(jvm, &this, negative, "\u{221e}".to_string(), RustString::new(), buffer, position).await; + } + let maximum_fraction_digits: i32 = jvm.get_field(&this, "maximumFractionDigits", "I").await?; + let precision = maximum_fraction_digits.clamp(0, 340) as usize; + let numeric = format!("{scaled:.precision$}"); + let (integer, fraction) = numeric + .split_once('.') + .map(|(integer, fraction)| (integer.to_string(), fraction.to_string())) + .unwrap_or((numeric, RustString::new())); + Self::append_formatted(jvm, &this, negative, integer, fraction, buffer, position).await + } + + async fn format_long( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + value: i64, + buffer: ClassInstanceRef, + position: ClassInstanceRef, + ) -> Result> { + let multiplier: i32 = jvm.get_field(&this, "multiplier", "I").await?; + let scaled = i128::from(value) * i128::from(multiplier); + let negative = scaled < 0; + Self::append_formatted( + jvm, + &this, + negative, + scaled.unsigned_abs().to_string(), + RustString::new(), + buffer, + position, + ) + .await + } + + async fn parse( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + source: ClassInstanceRef, + position: ClassInstanceRef, + ) -> Result> { + if source.is_null() || position.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "source or position").await); + } + let source = JavaLangString::to_rust_string(jvm, &source).await?; + let characters: Vec = source.chars().collect(); + let start: i32 = jvm.invoke_virtual(&position, "getIndex", "()I", ()).await?; + if start < 0 || start as usize > characters.len() { + let _: () = jvm.invoke_virtual(&position, "setErrorIndex", "(I)V", (start,)).await?; + return Ok(ClassInstanceRef::new(None)); + } + + let positive_prefix: ClassInstanceRef = jvm.get_field(&this, "positivePrefix", "Ljava/lang/String;").await?; + let positive_suffix: ClassInstanceRef = jvm.get_field(&this, "positiveSuffix", "Ljava/lang/String;").await?; + let negative_prefix: ClassInstanceRef = jvm.get_field(&this, "negativePrefix", "Ljava/lang/String;").await?; + let negative_suffix: ClassInstanceRef = jvm.get_field(&this, "negativeSuffix", "Ljava/lang/String;").await?; + let positive_prefix: Vec = JavaLangString::to_rust_string(jvm, &positive_prefix).await?.chars().collect(); + let positive_suffix: Vec = JavaLangString::to_rust_string(jvm, &positive_suffix).await?.chars().collect(); + let negative_prefix: Vec = JavaLangString::to_rust_string(jvm, &negative_prefix).await?.chars().collect(); + let negative_suffix: Vec = JavaLangString::to_rust_string(jvm, &negative_suffix).await?.chars().collect(); + + let mut index = start as usize; + let negative = if characters[index..].starts_with(&negative_prefix) && negative_prefix != positive_prefix { + index += negative_prefix.len(); + true + } else if characters[index..].starts_with(&positive_prefix) { + index += positive_prefix.len(); + false + } else { + let _: () = jvm.invoke_virtual(&position, "setErrorIndex", "(I)V", (start,)).await?; + return Ok(ClassInstanceRef::new(None)); + }; + + let parse_integer_only: bool = jvm.get_field(&this, "parseIntegerOnly", "Z").await?; + let mut normalized = RustString::new(); + if negative { + normalized.push('-'); + } + let mut digits = 0; + let mut decimal = false; + while index < characters.len() { + match characters[index] { + '0'..='9' => { + normalized.push(characters[index]); + digits += 1; + index += 1; + } + ',' if !decimal => index += 1, + '.' if !decimal && !parse_integer_only => { + normalized.push('.'); + decimal = true; + index += 1; + } + _ => break, + } + } + let suffix = if negative { &negative_suffix } else { &positive_suffix }; + if digits == 0 || !characters[index..].starts_with(suffix) { + let _: () = jvm.invoke_virtual(&position, "setErrorIndex", "(I)V", (index as i32,)).await?; + return Ok(ClassInstanceRef::new(None)); + } + index += suffix.len(); + + let Ok(mut value) = normalized.parse::() else { + let _: () = jvm.invoke_virtual(&position, "setErrorIndex", "(I)V", (start,)).await?; + return Ok(ClassInstanceRef::new(None)); + }; + let multiplier: i32 = jvm.get_field(&this, "multiplier", "I").await?; + if multiplier != 0 { + value /= f64::from(multiplier); + } + let _: () = jvm.invoke_virtual(&position, "setIndex", "(I)V", (index as i32,)).await?; + let _: () = jvm.invoke_virtual(&position, "setErrorIndex", "(I)V", (-1,)).await?; + + if multiplier == 1 + && !decimal + && let Ok(value) = normalized.parse::() + { + let result: ClassInstanceRef = jvm.new_class("java/lang/Long", "(J)V", (value,)).await?.into(); + return Ok(ClassInstanceRef::new(result.instance)); + } + if value.is_finite() && value.fract() == 0.0 && (-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0).contains(&value) { + let result: ClassInstanceRef = jvm.new_class("java/lang/Long", "(J)V", (value as i64,)).await?.into(); + return Ok(ClassInstanceRef::new(result.instance)); + } + let result: ClassInstanceRef = jvm.new_class("java/lang/Double", "(D)V", (value,)).await?.into(); + Ok(ClassInstanceRef::new(result.instance)) + } + + async fn to_pattern(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + jvm.get_field(&this, "pattern", "Ljava/lang/String;").await + } + + async fn get_multiplier(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "multiplier", "I").await + } + + async fn set_multiplier(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, multiplier: i32) -> Result<()> { + jvm.put_field(&mut this, "multiplier", "I", multiplier).await + } +} diff --git a/java_runtime/src/classes/java/text/field_position.rs b/java_runtime/src/classes/java/text/field_position.rs new file mode 100644 index 00000000..1f67d10a --- /dev/null +++ b/java_runtime/src/classes/java/text/field_position.rs @@ -0,0 +1,101 @@ +use alloc::{format, vec}; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{Object, String}, +}; + +// public class java.text.FieldPosition +pub struct FieldPosition; + +impl FieldPosition { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/text/FieldPosition", + parent_class: Some("java/lang/Object"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "(I)V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getField", "()I", Self::get_field, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getBeginIndex", "()I", Self::get_begin_index, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getEndIndex", "()I", Self::get_end_index, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("setBeginIndex", "(I)V", Self::set_begin_index, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("setEndIndex", "(I)V", Self::set_end_index, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("equals", "(Ljava/lang/Object;)Z", Self::equals, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("hashCode", "()I", Self::hash_code, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, MethodAccessFlags::PUBLIC), + ], + fields: vec![ + JavaFieldProto::new("field", "I", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("beginIndex", "I", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("endIndex", "I", FieldAccessFlags::PRIVATE), + ], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, field: i32) -> Result<()> { + let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + jvm.put_field(&mut this, "field", "I", field).await?; + jvm.put_field(&mut this, "beginIndex", "I", 0).await?; + jvm.put_field(&mut this, "endIndex", "I", 0).await + } + + async fn get_field(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "field", "I").await + } + + async fn get_begin_index(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "beginIndex", "I").await + } + + async fn get_end_index(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "endIndex", "I").await + } + + async fn set_begin_index(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, value: i32) -> Result<()> { + jvm.put_field(&mut this, "beginIndex", "I", value).await + } + + async fn set_end_index(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, value: i32) -> Result<()> { + jvm.put_field(&mut this, "endIndex", "I", value).await + } + + async fn equals(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + if other.is_null() || !jvm.is_instance(&**other, "java/text/FieldPosition") { + return Ok(false); + } + + let other: ClassInstanceRef = ClassInstanceRef::new(other.instance); + let field: i32 = jvm.get_field(&this, "field", "I").await?; + let other_field: i32 = jvm.get_field(&other, "field", "I").await?; + let begin_index: i32 = jvm.get_field(&this, "beginIndex", "I").await?; + let other_begin_index: i32 = jvm.get_field(&other, "beginIndex", "I").await?; + let end_index: i32 = jvm.get_field(&this, "endIndex", "I").await?; + let other_end_index: i32 = jvm.get_field(&other, "endIndex", "I").await?; + Ok(field == other_field && begin_index == other_begin_index && end_index == other_end_index) + } + + async fn hash_code(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let field: i32 = jvm.get_field(&this, "field", "I").await?; + let begin_index: i32 = jvm.get_field(&this, "beginIndex", "I").await?; + let end_index: i32 = jvm.get_field(&this, "endIndex", "I").await?; + Ok(field ^ begin_index.rotate_left(11) ^ end_index.rotate_left(22)) + } + + async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let field: i32 = jvm.get_field(&this, "field", "I").await?; + let begin_index: i32 = jvm.get_field(&this, "beginIndex", "I").await?; + let end_index: i32 = jvm.get_field(&this, "endIndex", "I").await?; + Ok(JavaLangString::from_rust_string( + jvm, + &format!("java.text.FieldPosition[field={field},beginIndex={begin_index},endIndex={end_index}]"), + ) + .await? + .into()) + } +} diff --git a/java_runtime/src/classes/java/text/format.rs b/java_runtime/src/classes/java/text/format.rs new file mode 100644 index 00000000..887e4eaa --- /dev/null +++ b/java_runtime/src/classes/java/text/format.rs @@ -0,0 +1,113 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use java_constants::{ClassAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, JavaError, Jvm, Result, runtime::JavaLangString}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::{ + lang::{Object, String, StringBuffer}, + text::{FieldPosition, ParseException, ParsePosition}, + }, +}; + +// public abstract class java.text.Format +pub struct Format; + +impl Format { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/text/Format", + parent_class: Some("java/lang/Object"), + interfaces: vec!["java/io/Serializable", "java/lang/Cloneable"], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PROTECTED), + JavaMethodProto::new( + "format", + "(Ljava/lang/Object;)Ljava/lang/String;", + Self::format, + MethodAccessFlags::PUBLIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new_abstract( + "format", + "(Ljava/lang/Object;Ljava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT, + ), + JavaMethodProto::new( + "parseObject", + "(Ljava/lang/String;)Ljava/lang/Object;", + Self::parse_object, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new_abstract( + "parseObject", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/lang/Object;", + MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT, + ), + JavaMethodProto::new("clone", "()Ljava/lang/Object;", Self::clone, MethodAccessFlags::PUBLIC), + ], + fields: vec![], + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::ABSTRACT, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await + } + + async fn format( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + object: ClassInstanceRef, + ) -> Result> { + let buffer: ClassInstanceRef = jvm.new_class("java/lang/StringBuffer", "()V", ()).await?.into(); + let position: ClassInstanceRef = jvm.new_class("java/text/FieldPosition", "(I)V", (0,)).await?.into(); + let buffer: ClassInstanceRef = jvm + .invoke_virtual( + &this, + "format", + "(Ljava/lang/Object;Ljava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + (object, buffer, position), + ) + .await?; + jvm.invoke_virtual(&buffer, "toString", "()Ljava/lang/String;", ()).await + } + + async fn parse_object( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + source: ClassInstanceRef, + ) -> Result> { + if source.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "source").await); + } + + let position: ClassInstanceRef = jvm.new_class("java/text/ParsePosition", "(I)V", (0,)).await?.into(); + let result: ClassInstanceRef = jvm + .invoke_virtual( + &this, + "parseObject", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/lang/Object;", + (source, position.clone()), + ) + .await?; + let index: i32 = jvm.invoke_virtual(&position, "getIndex", "()I", ()).await?; + if index == 0 { + let error_index: i32 = jvm.invoke_virtual(&position, "getErrorIndex", "()I", ()).await?; + let message = JavaLangString::from_rust_string(jvm, "Format.parseObject(String) failed").await?; + let exception: ClassInstanceRef = jvm + .new_class("java/text/ParseException", "(Ljava/lang/String;I)V", (message, error_index)) + .await? + .into(); + return Err(JavaError::JavaException(exception.into())); + } + Ok(result) + } + + async fn clone(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + Ok(jvm.shallow_clone(&this)?.into()) + } +} diff --git a/java_runtime/src/classes/java/text/number_format.rs b/java_runtime/src/classes/java/text/number_format.rs new file mode 100644 index 00000000..fab05451 --- /dev/null +++ b/java_runtime/src/classes/java/text/number_format.rs @@ -0,0 +1,531 @@ +use alloc::vec; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{Array, ClassInstanceRef, JavaError, Jvm, Result, runtime::JavaLangString}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::{ + lang::{Number, Object, String, StringBuffer}, + text::{FieldPosition, ParseException, ParsePosition}, + util::Locale, + }, +}; + +// public abstract class java.text.NumberFormat +pub struct NumberFormat; + +impl NumberFormat { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/text/NumberFormat", + parent_class: Some("java/text/Format"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::clinit, MethodAccessFlags::STATIC), + JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PROTECTED), + JavaMethodProto::new( + "format", + "(Ljava/lang/Object;Ljava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + Self::format_object, + MethodAccessFlags::PUBLIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "format", + "(D)Ljava/lang/String;", + Self::format_double, + MethodAccessFlags::PUBLIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "format", + "(J)Ljava/lang/String;", + Self::format_long, + MethodAccessFlags::PUBLIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new_abstract( + "format", + "(DLjava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT, + ), + JavaMethodProto::new_abstract( + "format", + "(JLjava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT, + ), + JavaMethodProto::new("parse", "(Ljava/lang/String;)Ljava/lang/Number;", Self::parse, MethodAccessFlags::PUBLIC), + JavaMethodProto::new_abstract( + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/lang/Number;", + MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT, + ), + JavaMethodProto::new( + "parseObject", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/lang/Object;", + Self::parse_object, + MethodAccessFlags::PUBLIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "getInstance", + "()Ljava/text/NumberFormat;", + Self::get_instance, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "getInstance", + "(Ljava/util/Locale;)Ljava/text/NumberFormat;", + Self::get_instance_with_locale, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "getNumberInstance", + "()Ljava/text/NumberFormat;", + Self::get_number_instance, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "getNumberInstance", + "(Ljava/util/Locale;)Ljava/text/NumberFormat;", + Self::get_number_instance_with_locale, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "getCurrencyInstance", + "()Ljava/text/NumberFormat;", + Self::get_currency_instance, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "getCurrencyInstance", + "(Ljava/util/Locale;)Ljava/text/NumberFormat;", + Self::get_currency_instance_with_locale, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "getPercentInstance", + "()Ljava/text/NumberFormat;", + Self::get_percent_instance, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "getPercentInstance", + "(Ljava/util/Locale;)Ljava/text/NumberFormat;", + Self::get_percent_instance_with_locale, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "getAvailableLocales", + "()[Ljava/util/Locale;", + Self::get_available_locales, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new("isGroupingUsed", "()Z", Self::is_grouping_used, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("setGroupingUsed", "(Z)V", Self::set_grouping_used, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("isParseIntegerOnly", "()Z", Self::is_parse_integer_only, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("setParseIntegerOnly", "(Z)V", Self::set_parse_integer_only, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "getMaximumIntegerDigits", + "()I", + Self::get_maximum_integer_digits, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "setMaximumIntegerDigits", + "(I)V", + Self::set_maximum_integer_digits, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "getMinimumIntegerDigits", + "()I", + Self::get_minimum_integer_digits, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "setMinimumIntegerDigits", + "(I)V", + Self::set_minimum_integer_digits, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "getMaximumFractionDigits", + "()I", + Self::get_maximum_fraction_digits, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "setMaximumFractionDigits", + "(I)V", + Self::set_maximum_fraction_digits, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "getMinimumFractionDigits", + "()I", + Self::get_minimum_fraction_digits, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "setMinimumFractionDigits", + "(I)V", + Self::set_minimum_fraction_digits, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("equals", "(Ljava/lang/Object;)Z", Self::equals, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("hashCode", "()I", Self::hash_code, MethodAccessFlags::PUBLIC), + ], + fields: vec![ + JavaFieldProto::new( + "INTEGER_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "FRACTION_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new("groupingUsed", "Z", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("parseIntegerOnly", "Z", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("maximumIntegerDigits", "I", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("minimumIntegerDigits", "I", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("maximumFractionDigits", "I", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("minimumFractionDigits", "I", FieldAccessFlags::PRIVATE), + ], + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::ABSTRACT, + } + } + + async fn clinit(jvm: &Jvm, _: &mut RuntimeContext) -> Result<()> { + jvm.put_static_field("java/text/NumberFormat", "INTEGER_FIELD", "I", 0).await?; + jvm.put_static_field("java/text/NumberFormat", "FRACTION_FIELD", "I", 1).await + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { + let _: () = jvm.invoke_special(&this, "java/text/Format", "", "()V", ()).await?; + jvm.put_field(&mut this, "groupingUsed", "Z", true).await?; + jvm.put_field(&mut this, "parseIntegerOnly", "Z", false).await?; + jvm.put_field(&mut this, "maximumIntegerDigits", "I", 40).await?; + jvm.put_field(&mut this, "minimumIntegerDigits", "I", 1).await?; + jvm.put_field(&mut this, "maximumFractionDigits", "I", 3).await?; + jvm.put_field(&mut this, "minimumFractionDigits", "I", 0).await + } + + async fn format_object( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + object: ClassInstanceRef, + buffer: ClassInstanceRef, + position: ClassInstanceRef, + ) -> Result> { + if object.is_null() || !jvm.is_instance(&**object, "java/lang/Number") { + return Err(jvm + .exception("java/lang/IllegalArgumentException", "Cannot format given Object as a Number") + .await); + } + if buffer.is_null() || position.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "buffer or position").await); + } + + if jvm.is_instance(&**object, "java/lang/Byte") + || jvm.is_instance(&**object, "java/lang/Short") + || jvm.is_instance(&**object, "java/lang/Integer") + || jvm.is_instance(&**object, "java/lang/Long") + { + let value: i64 = jvm.invoke_virtual(&object, "longValue", "()J", ()).await?; + return jvm + .invoke_virtual( + &this, + "format", + "(JLjava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + (value, buffer, position), + ) + .await; + } + + let value: f64 = jvm.invoke_virtual(&object, "doubleValue", "()D", ()).await?; + jvm.invoke_virtual( + &this, + "format", + "(DLjava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + (value, buffer, position), + ) + .await + } + + async fn format_double(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: f64) -> Result> { + let buffer: ClassInstanceRef = jvm.new_class("java/lang/StringBuffer", "()V", ()).await?.into(); + let position: ClassInstanceRef = jvm.new_class("java/text/FieldPosition", "(I)V", (0,)).await?.into(); + let buffer: ClassInstanceRef = jvm + .invoke_virtual( + &this, + "format", + "(DLjava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + (value, buffer, position), + ) + .await?; + jvm.invoke_virtual(&buffer, "toString", "()Ljava/lang/String;", ()).await + } + + async fn format_long(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: i64) -> Result> { + let buffer: ClassInstanceRef = jvm.new_class("java/lang/StringBuffer", "()V", ()).await?.into(); + let position: ClassInstanceRef = jvm.new_class("java/text/FieldPosition", "(I)V", (0,)).await?.into(); + let buffer: ClassInstanceRef = jvm + .invoke_virtual( + &this, + "format", + "(JLjava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + (value, buffer, position), + ) + .await?; + jvm.invoke_virtual(&buffer, "toString", "()Ljava/lang/String;", ()).await + } + + async fn parse( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + source: ClassInstanceRef, + ) -> Result> { + if source.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "source").await); + } + let position: ClassInstanceRef = jvm.new_class("java/text/ParsePosition", "(I)V", (0,)).await?.into(); + let result: ClassInstanceRef = jvm + .invoke_virtual( + &this, + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/lang/Number;", + (source, position.clone()), + ) + .await?; + let index: i32 = jvm.invoke_virtual(&position, "getIndex", "()I", ()).await?; + if index == 0 { + let error_index: i32 = jvm.invoke_virtual(&position, "getErrorIndex", "()I", ()).await?; + let message = JavaLangString::from_rust_string(jvm, "Unparseable number").await?; + let exception: ClassInstanceRef = jvm + .new_class("java/text/ParseException", "(Ljava/lang/String;I)V", (message, error_index)) + .await? + .into(); + return Err(JavaError::JavaException(exception.into())); + } + Ok(result) + } + + async fn parse_object( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + source: ClassInstanceRef, + position: ClassInstanceRef, + ) -> Result> { + let number: ClassInstanceRef = jvm + .invoke_virtual( + &this, + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/lang/Number;", + (source, position), + ) + .await?; + Ok(ClassInstanceRef::new(number.instance)) + } + + async fn get_instance(jvm: &Jvm, _: &mut RuntimeContext) -> Result> { + jvm.invoke_static("java/text/NumberFormat", "getNumberInstance", "()Ljava/text/NumberFormat;", ()) + .await + } + + async fn get_instance_with_locale(jvm: &Jvm, _: &mut RuntimeContext, locale: ClassInstanceRef) -> Result> { + jvm.invoke_static( + "java/text/NumberFormat", + "getNumberInstance", + "(Ljava/util/Locale;)Ljava/text/NumberFormat;", + (locale,), + ) + .await + } + + async fn get_number_instance(jvm: &Jvm, _: &mut RuntimeContext) -> Result> { + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + jvm.invoke_static( + "java/text/NumberFormat", + "getNumberInstance", + "(Ljava/util/Locale;)Ljava/text/NumberFormat;", + (locale,), + ) + .await + } + + async fn get_number_instance_with_locale(jvm: &Jvm, _: &mut RuntimeContext, locale: ClassInstanceRef) -> Result> { + if locale.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "locale").await); + } + let pattern = JavaLangString::from_rust_string(jvm, "#,##0.###").await?; + Ok(jvm + .new_class("java/text/DecimalFormat", "(Ljava/lang/String;)V", (pattern,)) + .await? + .into()) + } + + async fn get_currency_instance(jvm: &Jvm, _: &mut RuntimeContext) -> Result> { + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + jvm.invoke_static( + "java/text/NumberFormat", + "getCurrencyInstance", + "(Ljava/util/Locale;)Ljava/text/NumberFormat;", + (locale,), + ) + .await + } + + async fn get_currency_instance_with_locale( + jvm: &Jvm, + _: &mut RuntimeContext, + locale: ClassInstanceRef, + ) -> Result> { + if locale.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "locale").await); + } + let pattern = JavaLangString::from_rust_string(jvm, "\u{00a4}#,##0.00;-\u{00a4}#,##0.00").await?; + Ok(jvm + .new_class("java/text/DecimalFormat", "(Ljava/lang/String;)V", (pattern,)) + .await? + .into()) + } + + async fn get_percent_instance(jvm: &Jvm, _: &mut RuntimeContext) -> Result> { + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + jvm.invoke_static( + "java/text/NumberFormat", + "getPercentInstance", + "(Ljava/util/Locale;)Ljava/text/NumberFormat;", + (locale,), + ) + .await + } + + async fn get_percent_instance_with_locale(jvm: &Jvm, _: &mut RuntimeContext, locale: ClassInstanceRef) -> Result> { + if locale.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "locale").await); + } + let pattern = JavaLangString::from_rust_string(jvm, "#,##0%").await?; + Ok(jvm + .new_class("java/text/DecimalFormat", "(Ljava/lang/String;)V", (pattern,)) + .await? + .into()) + } + + async fn get_available_locales(jvm: &Jvm, _: &mut RuntimeContext) -> Result>> { + jvm.invoke_static("java/util/Locale", "getAvailableLocales", "()[Ljava/util/Locale;", ()) + .await + } + + async fn is_grouping_used(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "groupingUsed", "Z").await + } + + async fn set_grouping_used(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, value: bool) -> Result<()> { + jvm.put_field(&mut this, "groupingUsed", "Z", value).await + } + + async fn is_parse_integer_only(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "parseIntegerOnly", "Z").await + } + + async fn set_parse_integer_only(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, value: bool) -> Result<()> { + jvm.put_field(&mut this, "parseIntegerOnly", "Z", value).await + } + + async fn get_maximum_integer_digits(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "maximumIntegerDigits", "I").await + } + + async fn set_maximum_integer_digits(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, value: i32) -> Result<()> { + let value = value.max(0); + jvm.put_field(&mut this, "maximumIntegerDigits", "I", value).await?; + let minimum: i32 = jvm.get_field(&this, "minimumIntegerDigits", "I").await?; + if minimum > value { + jvm.put_field(&mut this, "minimumIntegerDigits", "I", value).await?; + } + Ok(()) + } + + async fn get_minimum_integer_digits(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "minimumIntegerDigits", "I").await + } + + async fn set_minimum_integer_digits(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, value: i32) -> Result<()> { + let value = value.max(0); + jvm.put_field(&mut this, "minimumIntegerDigits", "I", value).await?; + let maximum: i32 = jvm.get_field(&this, "maximumIntegerDigits", "I").await?; + if maximum < value { + jvm.put_field(&mut this, "maximumIntegerDigits", "I", value).await?; + } + Ok(()) + } + + async fn get_maximum_fraction_digits(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "maximumFractionDigits", "I").await + } + + async fn set_maximum_fraction_digits(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, value: i32) -> Result<()> { + let value = value.max(0); + jvm.put_field(&mut this, "maximumFractionDigits", "I", value).await?; + let minimum: i32 = jvm.get_field(&this, "minimumFractionDigits", "I").await?; + if minimum > value { + jvm.put_field(&mut this, "minimumFractionDigits", "I", value).await?; + } + Ok(()) + } + + async fn get_minimum_fraction_digits(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "minimumFractionDigits", "I").await + } + + async fn set_minimum_fraction_digits(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, value: i32) -> Result<()> { + let value = value.max(0); + jvm.put_field(&mut this, "minimumFractionDigits", "I", value).await?; + let maximum: i32 = jvm.get_field(&this, "maximumFractionDigits", "I").await?; + if maximum < value { + jvm.put_field(&mut this, "maximumFractionDigits", "I", value).await?; + } + Ok(()) + } + + async fn equals(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + if other.is_null() || !jvm.is_instance(&**other, "java/text/NumberFormat") { + return Ok(false); + } + let other: ClassInstanceRef = ClassInstanceRef::new(other.instance); + for (name, descriptor) in [ + ("groupingUsed", "Z"), + ("parseIntegerOnly", "Z"), + ("maximumIntegerDigits", "I"), + ("minimumIntegerDigits", "I"), + ("maximumFractionDigits", "I"), + ("minimumFractionDigits", "I"), + ] { + if descriptor == "Z" { + let value: bool = jvm.get_field(&this, name, descriptor).await?; + let other_value: bool = jvm.get_field(&other, name, descriptor).await?; + if value != other_value { + return Ok(false); + } + } else { + let value: i32 = jvm.get_field(&this, name, descriptor).await?; + let other_value: i32 = jvm.get_field(&other, name, descriptor).await?; + if value != other_value { + return Ok(false); + } + } + } + Ok(true) + } + + async fn hash_code(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let maximum_integer_digits: i32 = jvm.get_field(&this, "maximumIntegerDigits", "I").await?; + let maximum_fraction_digits: i32 = jvm.get_field(&this, "maximumFractionDigits", "I").await?; + Ok(maximum_integer_digits * 37 + maximum_fraction_digits) + } +} diff --git a/java_runtime/src/classes/java/text/parse_exception.rs b/java_runtime/src/classes/java/text/parse_exception.rs new file mode 100644 index 00000000..fc5fc011 --- /dev/null +++ b/java_runtime/src/classes/java/text/parse_exception.rs @@ -0,0 +1,43 @@ +use alloc::vec; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// public class java.text.ParseException +pub struct ParseException; + +impl ParseException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/text/ParseException", + parent_class: Some("java/lang/Exception"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "(Ljava/lang/String;I)V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getErrorOffset", "()I", Self::get_error_offset, MethodAccessFlags::PUBLIC), + ], + fields: vec![JavaFieldProto::new("errorOffset", "I", FieldAccessFlags::PRIVATE)], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn init( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + message: ClassInstanceRef, + error_offset: i32, + ) -> Result<()> { + let _: () = jvm + .invoke_special(&this, "java/lang/Exception", "", "(Ljava/lang/String;)V", (message,)) + .await?; + jvm.put_field(&mut this, "errorOffset", "I", error_offset).await + } + + async fn get_error_offset(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "errorOffset", "I").await + } +} diff --git a/java_runtime/src/classes/java/text/parse_position.rs b/java_runtime/src/classes/java/text/parse_position.rs new file mode 100644 index 00000000..e57f392c --- /dev/null +++ b/java_runtime/src/classes/java/text/parse_position.rs @@ -0,0 +1,89 @@ +use alloc::{format, vec}; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{Object, String}, +}; + +// public class java.text.ParsePosition +pub struct ParsePosition; + +impl ParsePosition { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/text/ParsePosition", + parent_class: Some("java/lang/Object"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "(I)V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getIndex", "()I", Self::get_index, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("setIndex", "(I)V", Self::set_index, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getErrorIndex", "()I", Self::get_error_index, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("setErrorIndex", "(I)V", Self::set_error_index, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("equals", "(Ljava/lang/Object;)Z", Self::equals, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("hashCode", "()I", Self::hash_code, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, MethodAccessFlags::PUBLIC), + ], + fields: vec![ + JavaFieldProto::new("index", "I", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("errorIndex", "I", FieldAccessFlags::PRIVATE), + ], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, index: i32) -> Result<()> { + let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + jvm.put_field(&mut this, "index", "I", index).await?; + jvm.put_field(&mut this, "errorIndex", "I", -1).await + } + + async fn get_index(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "index", "I").await + } + + async fn set_index(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, index: i32) -> Result<()> { + jvm.put_field(&mut this, "index", "I", index).await + } + + async fn get_error_index(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "errorIndex", "I").await + } + + async fn set_error_index(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, index: i32) -> Result<()> { + jvm.put_field(&mut this, "errorIndex", "I", index).await + } + + async fn equals(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + if other.is_null() || !jvm.is_instance(&**other, "java/text/ParsePosition") { + return Ok(false); + } + + let other: ClassInstanceRef = ClassInstanceRef::new(other.instance); + let index: i32 = jvm.get_field(&this, "index", "I").await?; + let other_index: i32 = jvm.get_field(&other, "index", "I").await?; + let error_index: i32 = jvm.get_field(&this, "errorIndex", "I").await?; + let other_error_index: i32 = jvm.get_field(&other, "errorIndex", "I").await?; + Ok(index == other_index && error_index == other_error_index) + } + + async fn hash_code(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let index: i32 = jvm.get_field(&this, "index", "I").await?; + let error_index: i32 = jvm.get_field(&this, "errorIndex", "I").await?; + Ok(index ^ error_index.rotate_left(16)) + } + + async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let index: i32 = jvm.get_field(&this, "index", "I").await?; + let error_index: i32 = jvm.get_field(&this, "errorIndex", "I").await?; + Ok( + JavaLangString::from_rust_string(jvm, &format!("java.text.ParsePosition[index={index},errorIndex={error_index}]")) + .await? + .into(), + ) + } +} diff --git a/java_runtime/src/classes/java/text/simple_date_format.rs b/java_runtime/src/classes/java/text/simple_date_format.rs new file mode 100644 index 00000000..35463dca --- /dev/null +++ b/java_runtime/src/classes/java/text/simple_date_format.rs @@ -0,0 +1,564 @@ +use alloc::{format, string::String as RustString, vec, vec::Vec}; + +use chrono::{DateTime, Datelike, NaiveDate, TimeZone as ChronoTimeZone, Timelike, Utc}; +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::{ + lang::{String, StringBuffer}, + text::{FieldPosition, ParsePosition}, + util::{Calendar, Date, Locale, TimeZone}, + }, +}; + +#[derive(Clone)] +enum DateToken { + Literal(RustString), + Field(char, usize), +} + +// public class java.text.SimpleDateFormat +pub struct SimpleDateFormat; + +impl SimpleDateFormat { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/text/SimpleDateFormat", + parent_class: Some("java/text/DateFormat"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_pattern, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "", + "(Ljava/lang/String;Ljava/util/Locale;)V", + Self::init_with_pattern_and_locale, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "format", + "(Ljava/util/Date;Ljava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + Self::format, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/util/Date;", + Self::parse, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("applyPattern", "(Ljava/lang/String;)V", Self::apply_pattern, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("toPattern", "()Ljava/lang/String;", Self::to_pattern, MethodAccessFlags::PUBLIC), + ], + fields: vec![JavaFieldProto::new("pattern", "Ljava/lang/String;", FieldAccessFlags::PRIVATE)], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + fn tokenize_pattern(pattern: &str) -> Option> { + let characters: Vec = pattern.chars().collect(); + let mut tokens = Vec::new(); + let mut literal = RustString::new(); + let mut quoted = false; + let mut index = 0; + while index < characters.len() { + let character = characters[index]; + if character == '\'' { + if index + 1 < characters.len() && characters[index + 1] == '\'' { + literal.push('\''); + index += 2; + continue; + } + quoted = !quoted; + index += 1; + continue; + } + if !quoted && character.is_ascii_alphabetic() { + if !matches!( + character, + 'G' | 'y' | 'M' | 'd' | 'h' | 'H' | 'm' | 's' | 'S' | 'E' | 'D' | 'F' | 'w' | 'W' | 'a' | 'k' | 'K' | 'z' + ) { + return None; + } + if !literal.is_empty() { + tokens.push(DateToken::Literal(core::mem::take(&mut literal))); + } + let mut count = 1; + while index + count < characters.len() && characters[index + count] == character { + count += 1; + } + tokens.push(DateToken::Field(character, count)); + index += count; + continue; + } + literal.push(character); + index += 1; + } + if quoted { + return None; + } + if !literal.is_empty() { + tokens.push(DateToken::Literal(literal)); + } + Some(tokens) + } + + fn parse_number(characters: &[char], index: &mut usize, maximum_digits: Option) -> Option { + let start = *index; + let mut value = 0i32; + while *index < characters.len() && characters[*index].is_ascii_digit() && maximum_digits.is_none_or(|maximum| *index - start < maximum) { + value = value.checked_mul(10)?.checked_add(characters[*index].to_digit(10)? as i32)?; + *index += 1; + } + if *index == start { None } else { Some(value) } + } + + fn starts_with_ignore_ascii_case(characters: &[char], index: usize, value: &str) -> bool { + let value: Vec = value.chars().collect(); + characters + .get(index..index + value.len()) + .is_some_and(|candidate| candidate.iter().zip(value).all(|(left, right)| left.eq_ignore_ascii_case(&right))) + } + + fn parse_timestamp(tokens: &[DateToken], characters: &[char], start: usize, default_offset: i32) -> core::result::Result<(i64, usize), usize> { + let months = [ + ("January", "Jan"), + ("February", "Feb"), + ("March", "Mar"), + ("April", "Apr"), + ("May", "May"), + ("June", "Jun"), + ("July", "Jul"), + ("August", "Aug"), + ("September", "Sep"), + ("October", "Oct"), + ("November", "Nov"), + ("December", "Dec"), + ]; + let weekdays = [ + ("Sunday", "Sun"), + ("Monday", "Mon"), + ("Tuesday", "Tue"), + ("Wednesday", "Wed"), + ("Thursday", "Thu"), + ("Friday", "Fri"), + ("Saturday", "Sat"), + ]; + + let mut index = start; + let mut era = 1; + let mut year = 1970; + let mut month = 1; + let mut day = 1; + let mut ordinal = None; + let mut month_set = false; + let mut day_set = false; + let mut hour = 0; + let mut twelve_hour = None; + let mut am_pm = 0; + let mut minute = 0; + let mut second = 0; + let mut millisecond = 0; + let mut offset = default_offset; + + for token in tokens { + match token { + DateToken::Literal(literal) => { + let value: Vec = literal.chars().collect(); + if !characters[index..].starts_with(&value) { + return Err(index); + } + index += value.len(); + } + DateToken::Field(character, count) => match character { + 'G' => { + if Self::starts_with_ignore_ascii_case(characters, index, "AD") { + era = 1; + index += 2; + } else if Self::starts_with_ignore_ascii_case(characters, index, "BC") { + era = 0; + index += 2; + } else { + return Err(index); + } + } + 'y' => { + let value = Self::parse_number(characters, &mut index, if *count > 1 { Some(*count) } else { None }).ok_or(index)?; + year = if *count == 2 { + if value <= 69 { 2000 + value } else { 1900 + value } + } else { + value + }; + } + 'M' if *count >= 3 => { + let mut parsed = None; + for (position, (full, short)) in months.iter().enumerate() { + let candidate = if *count >= 4 { *full } else { *short }; + if Self::starts_with_ignore_ascii_case(characters, index, candidate) { + parsed = Some((position as i32 + 1, candidate.len())); + break; + } + } + let Some((value, length)) = parsed else { + return Err(index); + }; + month = value; + month_set = true; + index += length; + } + 'M' => { + month = Self::parse_number(characters, &mut index, if *count > 1 { Some(*count) } else { None }).ok_or(index)?; + month_set = true; + } + 'd' => { + day = Self::parse_number(characters, &mut index, if *count > 1 { Some(*count) } else { None }).ok_or(index)?; + day_set = true; + } + 'H' => hour = Self::parse_number(characters, &mut index, if *count > 1 { Some(*count) } else { None }).ok_or(index)?, + 'k' => { + let value = Self::parse_number(characters, &mut index, if *count > 1 { Some(*count) } else { None }).ok_or(index)?; + if !(1..=24).contains(&value) { + return Err(index); + } + hour = value % 24; + } + 'h' | 'K' => { + let value = Self::parse_number(characters, &mut index, if *count > 1 { Some(*count) } else { None }).ok_or(index)?; + twelve_hour = Some((*character, value)); + } + 'm' => minute = Self::parse_number(characters, &mut index, if *count > 1 { Some(*count) } else { None }).ok_or(index)?, + 's' => second = Self::parse_number(characters, &mut index, if *count > 1 { Some(*count) } else { None }).ok_or(index)?, + 'S' => millisecond = Self::parse_number(characters, &mut index, Some((*count).max(1))).ok_or(index)?, + 'E' => { + let mut consumed = None; + for (full, short) in weekdays { + for candidate in [full, short] { + if Self::starts_with_ignore_ascii_case(characters, index, candidate) { + consumed = Some(candidate.len()); + break; + } + } + if consumed.is_some() { + break; + } + } + let Some(length) = consumed else { + return Err(index); + }; + index += length; + } + 'D' => ordinal = Some(Self::parse_number(characters, &mut index, if *count > 1 { Some(*count) } else { None }).ok_or(index)?), + 'F' | 'w' | 'W' => { + let _ = Self::parse_number(characters, &mut index, if *count > 1 { Some(*count) } else { None }).ok_or(index)?; + } + 'a' => { + if Self::starts_with_ignore_ascii_case(characters, index, "AM") { + am_pm = 0; + index += 2; + } else if Self::starts_with_ignore_ascii_case(characters, index, "PM") { + am_pm = 1; + index += 2; + } else { + return Err(index); + } + } + 'z' => { + if Self::starts_with_ignore_ascii_case(characters, index, "GMT") + || Self::starts_with_ignore_ascii_case(characters, index, "UTC") + { + index += 3; + } else { + return Err(index); + } + offset = 0; + if index < characters.len() && matches!(characters[index], '+' | '-') { + let sign = if characters[index] == '-' { -1 } else { 1 }; + index += 1; + let hours = Self::parse_number(characters, &mut index, Some(2)).ok_or(index)?; + if index >= characters.len() || characters[index] != ':' { + return Err(index); + } + index += 1; + let minutes = Self::parse_number(characters, &mut index, Some(2)).ok_or(index)?; + if hours > 23 || minutes > 59 { + return Err(index); + } + offset = sign * (hours * 60 + minutes) * 60 * 1000; + } + } + _ => return Err(index), + }, + } + } + + if let Some((kind, value)) = twelve_hour { + hour = match kind { + 'h' if (1..=12).contains(&value) => value % 12 + am_pm * 12, + 'K' if (0..=11).contains(&value) => value + am_pm * 12, + _ => return Err(index), + }; + } + if era == 0 { + year = 1 - year; + } + if let Some(ordinal) = ordinal.filter(|_| !month_set && !day_set) { + let Some(date) = NaiveDate::from_yo_opt(year, ordinal as u32) else { + return Err(index); + }; + month = date.month() as i32; + day = date.day() as i32; + } + if !(1..=12).contains(&month) + || !(1..=31).contains(&day) + || !(0..=23).contains(&hour) + || !(0..=59).contains(&minute) + || !(0..=59).contains(&second) + || !(0..=999).contains(&millisecond) + { + return Err(index); + } + let Some(date_time) = Utc + .with_ymd_and_hms(year, month as u32, day as u32, hour as u32, minute as u32, second as u32) + .single() + else { + return Err(index); + }; + let Some(timestamp) = date_time + .timestamp_millis() + .checked_add(i64::from(millisecond)) + .and_then(|value| value.checked_sub(i64::from(offset))) + else { + return Err(index); + }; + Ok((timestamp, index)) + } + + fn date_field(character: char) -> Option { + match character { + 'G' => Some(0), + 'y' => Some(1), + 'M' => Some(2), + 'd' => Some(3), + 'k' => Some(4), + 'H' => Some(5), + 'm' => Some(6), + 's' => Some(7), + 'S' => Some(8), + 'E' => Some(9), + 'D' => Some(10), + 'F' => Some(11), + 'w' => Some(12), + 'W' => Some(13), + 'a' => Some(14), + 'h' => Some(15), + 'K' => Some(16), + 'z' => Some(17), + _ => None, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + let pattern = JavaLangString::from_rust_string(jvm, "M/d/yy h:mm a").await?; + jvm.invoke_special(&this, "java/text/SimpleDateFormat", "", "(Ljava/lang/String;)V", (pattern,)) + .await + } + + async fn init_with_pattern(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, pattern: ClassInstanceRef) -> Result<()> { + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + jvm.invoke_special( + &this, + "java/text/SimpleDateFormat", + "", + "(Ljava/lang/String;Ljava/util/Locale;)V", + (pattern, locale), + ) + .await + } + + async fn init_with_pattern_and_locale( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + pattern: ClassInstanceRef, + locale: ClassInstanceRef, + ) -> Result<()> { + if pattern.is_null() || locale.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "pattern or locale").await); + } + let value = JavaLangString::to_rust_string(jvm, &pattern).await?; + if Self::tokenize_pattern(&value).is_none() { + return Err(jvm.exception("java/lang/IllegalArgumentException", "Illegal pattern character").await); + } + let _: () = jvm.invoke_special(&this, "java/text/DateFormat", "", "()V", ()).await?; + jvm.put_field(&mut this, "pattern", "Ljava/lang/String;", pattern).await + } + + async fn apply_pattern(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, pattern: ClassInstanceRef) -> Result<()> { + if pattern.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "pattern").await); + } + let value = JavaLangString::to_rust_string(jvm, &pattern).await?; + if Self::tokenize_pattern(&value).is_none() { + return Err(jvm.exception("java/lang/IllegalArgumentException", "Illegal pattern character").await); + } + jvm.put_field(&mut this, "pattern", "Ljava/lang/String;", pattern).await + } + + async fn to_pattern(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + jvm.get_field(&this, "pattern", "Ljava/lang/String;").await + } + + async fn format( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + date: ClassInstanceRef, + buffer: ClassInstanceRef, + position: ClassInstanceRef, + ) -> Result> { + if date.is_null() || buffer.is_null() || position.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "date, buffer, or position").await); + } + let pattern: ClassInstanceRef = jvm.get_field(&this, "pattern", "Ljava/lang/String;").await?; + let pattern = JavaLangString::to_rust_string(jvm, &pattern).await?; + let Some(tokens) = Self::tokenize_pattern(&pattern) else { + return Err(jvm.exception("java/lang/IllegalArgumentException", "Illegal pattern").await); + }; + let time: i64 = jvm.invoke_virtual(&date, "getTime", "()J", ()).await?; + let calendar: ClassInstanceRef = jvm.get_field(&this, "calendar", "Ljava/util/Calendar;").await?; + let time_zone: ClassInstanceRef = jvm.invoke_virtual(&calendar, "getTimeZone", "()Ljava/util/TimeZone;", ()).await?; + let offset: i32 = jvm.invoke_virtual(&time_zone, "getRawOffset", "()I", ()).await?; + let Some(adjusted) = time.checked_add(i64::from(offset)) else { + return Err(jvm.exception("java/lang/IllegalArgumentException", "date out of range").await); + }; + let Some(date_time) = DateTime::::from_timestamp_millis(adjusted) else { + return Err(jvm.exception("java/lang/IllegalArgumentException", "date out of range").await); + }; + + let months = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ]; + let short_months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + let weekdays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; + let short_weekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + let requested_field: i32 = jvm.invoke_virtual(&position, "getField", "()I", ()).await?; + let base: i32 = jvm.invoke_virtual(&buffer, "length", "()I", ()).await?; + let mut field_position_set = false; + let mut formatted = RustString::new(); + for token in tokens { + match token { + DateToken::Literal(value) => formatted.push_str(&value), + DateToken::Field(character, count) => { + let begin = formatted.chars().count() as i32; + match character { + 'G' => formatted.push_str(if date_time.year() <= 0 { "BC" } else { "AD" }), + 'y' => { + let year = if date_time.year() <= 0 { 1 - date_time.year() } else { date_time.year() }; + if count == 2 { + formatted.push_str(&format!("{:02}", year.rem_euclid(100))); + } else { + formatted.push_str(&format!("{year:0count$}")); + } + } + 'M' if count >= 4 => formatted.push_str(months[date_time.month0() as usize]), + 'M' if count == 3 => formatted.push_str(short_months[date_time.month0() as usize]), + 'M' => formatted.push_str(&format!("{:0count$}", date_time.month())), + 'd' => formatted.push_str(&format!("{:0count$}", date_time.day())), + 'h' => { + let hour = date_time.hour() % 12; + formatted.push_str(&format!("{:0count$}", if hour == 0 { 12 } else { hour })); + } + 'H' => formatted.push_str(&format!("{:0count$}", date_time.hour())), + 'm' => formatted.push_str(&format!("{:0count$}", date_time.minute())), + 's' => formatted.push_str(&format!("{:0count$}", date_time.second())), + 'S' => formatted.push_str(&format!("{:0count$}", date_time.timestamp_subsec_millis())), + 'E' if count >= 4 => formatted.push_str(weekdays[date_time.weekday().num_days_from_sunday() as usize]), + 'E' => formatted.push_str(short_weekdays[date_time.weekday().num_days_from_sunday() as usize]), + 'D' => formatted.push_str(&format!("{:0count$}", date_time.ordinal())), + 'F' | 'W' => formatted.push_str(&format!("{:0count$}", (date_time.day() - 1) / 7 + 1)), + 'w' => formatted.push_str(&format!("{:0count$}", date_time.iso_week().week())), + 'a' => formatted.push_str(if date_time.hour() < 12 { "AM" } else { "PM" }), + 'k' => formatted.push_str(&format!("{:0count$}", if date_time.hour() == 0 { 24 } else { date_time.hour() })), + 'K' => formatted.push_str(&format!("{:0count$}", date_time.hour() % 12)), + 'z' => { + if offset == 0 { + formatted.push_str("GMT"); + } else { + let absolute = offset.unsigned_abs() / 60_000; + formatted.push_str(&format!( + "GMT{}{:02}:{:02}", + if offset < 0 { '-' } else { '+' }, + absolute / 60, + absolute % 60 + )); + } + } + _ => return Err(jvm.exception("java/lang/IllegalArgumentException", "Illegal pattern").await), + } + if !field_position_set && Self::date_field(character) == Some(requested_field) { + let _: () = jvm.invoke_virtual(&position, "setBeginIndex", "(I)V", (base + begin,)).await?; + let _: () = jvm + .invoke_virtual(&position, "setEndIndex", "(I)V", (base + formatted.chars().count() as i32,)) + .await?; + field_position_set = true; + } + } + } + } + let text = JavaLangString::from_rust_string(jvm, &formatted).await?; + jvm.invoke_virtual(&buffer, "append", "(Ljava/lang/String;)Ljava/lang/StringBuffer;", (text,)) + .await + } + + async fn parse( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + source: ClassInstanceRef, + position: ClassInstanceRef, + ) -> Result> { + if source.is_null() || position.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "source or position").await); + } + let pattern: ClassInstanceRef = jvm.get_field(&this, "pattern", "Ljava/lang/String;").await?; + let pattern = JavaLangString::to_rust_string(jvm, &pattern).await?; + let Some(tokens) = Self::tokenize_pattern(&pattern) else { + return Err(jvm.exception("java/lang/IllegalArgumentException", "Illegal pattern").await); + }; + let source = JavaLangString::to_rust_string(jvm, &source).await?; + let characters: Vec = source.chars().collect(); + let start: i32 = jvm.invoke_virtual(&position, "getIndex", "()I", ()).await?; + if start < 0 || start as usize > characters.len() { + let _: () = jvm.invoke_virtual(&position, "setErrorIndex", "(I)V", (start,)).await?; + return Ok(ClassInstanceRef::new(None)); + } + let calendar: ClassInstanceRef = jvm.get_field(&this, "calendar", "Ljava/util/Calendar;").await?; + let time_zone: ClassInstanceRef = jvm.invoke_virtual(&calendar, "getTimeZone", "()Ljava/util/TimeZone;", ()).await?; + let offset: i32 = jvm.invoke_virtual(&time_zone, "getRawOffset", "()I", ()).await?; + match Self::parse_timestamp(&tokens, &characters, start as usize, offset) { + Ok((timestamp, index)) => { + let _: () = jvm.invoke_virtual(&position, "setIndex", "(I)V", (index as i32,)).await?; + let _: () = jvm.invoke_virtual(&position, "setErrorIndex", "(I)V", (-1,)).await?; + Ok(jvm.new_class("java/util/Date", "(J)V", (timestamp,)).await?.into()) + } + Err(error_index) => { + let _: () = jvm.invoke_virtual(&position, "setErrorIndex", "(I)V", (error_index as i32,)).await?; + Ok(ClassInstanceRef::new(None)) + } + } + } +} diff --git a/java_runtime/src/classes/java/util/calendar.rs b/java_runtime/src/classes/java/util/calendar.rs index 6440a65c..fed9991f 100644 --- a/java_runtime/src/classes/java/util/calendar.rs +++ b/java_runtime/src/classes/java/util/calendar.rs @@ -35,6 +35,9 @@ impl Calendar { JavaMethodProto::new("setTimeInMillis", "(J)V", Self::set_time_in_millis, Default::default()), JavaMethodProto::new("getTimeInMillis", "()J", Self::get_time_in_millis, Default::default()), JavaMethodProto::new("getTimeZone", "()Ljava/util/TimeZone;", Self::get_time_zone, Default::default()), + JavaMethodProto::new("setTimeZone", "(Ljava/util/TimeZone;)V", Self::set_time_zone, Default::default()), + JavaMethodProto::new("isLenient", "()Z", Self::is_lenient, Default::default()), + JavaMethodProto::new("setLenient", "(Z)V", Self::set_lenient, Default::default()), JavaMethodProto::new("equals", "(Ljava/lang/Object;)Z", Self::equals, Default::default()), JavaMethodProto::new("hashCode", "()I", Self::hash_code, Default::default()), JavaMethodProto::new("before", "(Ljava/lang/Object;)Z", Self::before, Default::default()), @@ -48,6 +51,7 @@ impl Calendar { JavaFieldProto::new("time", "J", Default::default()), JavaFieldProto::new("fields", "[I", Default::default()), JavaFieldProto::new("timeZone", "Ljava/util/TimeZone;", Default::default()), + JavaFieldProto::new("lenient", "Z", Default::default()), ], access_flags: ClassAccessFlags::ABSTRACT, } @@ -91,6 +95,7 @@ impl Calendar { .invoke_static("java/util/TimeZone", "getDefault", "()Ljava/util/TimeZone;", ()) .await?; jvm.put_field(&mut this, "timeZone", "Ljava/util/TimeZone;", time_zone).await?; + jvm.put_field(&mut this, "lenient", "Z", true).await?; Ok(()) } @@ -136,6 +141,25 @@ impl Calendar { jvm.get_field(&this, "timeZone", "Ljava/util/TimeZone;").await } + async fn set_time_zone(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, time_zone: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.util.Calendar::setTimeZone({this:?}, {time_zone:?})"); + if time_zone.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "timeZone").await); + } + jvm.put_field(&mut this, "timeZone", "Ljava/util/TimeZone;", time_zone).await?; + jvm.invoke_virtual(&this, "computeFields", "()V", ()).await + } + + async fn is_lenient(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.util.Calendar::isLenient({this:?})"); + jvm.get_field(&this, "lenient", "Z").await + } + + async fn set_lenient(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, lenient: bool) -> Result<()> { + tracing::debug!("java.util.Calendar::setLenient({this:?}, {lenient:?})"); + jvm.put_field(&mut this, "lenient", "Z", lenient).await + } + async fn equals(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { tracing::debug!("java.util.Calendar::equals({this:?}, {other:?})"); @@ -150,6 +174,12 @@ impl Calendar { return Ok(false); } + let lenient: bool = jvm.get_field(&this, "lenient", "Z").await?; + let other_lenient: bool = jvm.get_field(&other, "lenient", "Z").await?; + if lenient != other_lenient { + return Ok(false); + } + let time_zone: ClassInstanceRef = jvm.get_field(&this, "timeZone", "Ljava/util/TimeZone;").await?; let other_time_zone: ClassInstanceRef = jvm.get_field(&other, "timeZone", "Ljava/util/TimeZone;").await?; let raw_offset: i32 = jvm.invoke_virtual(&time_zone, "getRawOffset", "()I", ()).await?; @@ -171,7 +201,8 @@ impl Calendar { let raw_offset: i32 = jvm.invoke_virtual(&time_zone, "getRawOffset", "()I", ()).await?; let id: ClassInstanceRef = jvm.invoke_virtual(&time_zone, "getID", "()Ljava/lang/String;", ()).await?; let id_hash: i32 = jvm.invoke_virtual(&id, "hashCode", "()I", ()).await?; - Ok((time ^ ((time as u64 >> 32) as i64)) as i32 ^ raw_offset ^ id_hash) + let lenient: bool = jvm.get_field(&this, "lenient", "Z").await?; + Ok((time ^ ((time as u64 >> 32) as i64)) as i32 ^ raw_offset ^ id_hash ^ if lenient { 1 } else { 0 }) } async fn before(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { diff --git a/java_runtime/src/loader.rs b/java_runtime/src/loader.rs index 6ae4be7a..bee07725 100644 --- a/java_runtime/src/loader.rs +++ b/java_runtime/src/loader.rs @@ -101,6 +101,14 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::net::URLClassLoader::as_proto(), crate::classes::java::net::URLConnection::as_proto(), crate::classes::java::net::URLStreamHandler::as_proto(), + crate::classes::java::text::DateFormat::as_proto(), + crate::classes::java::text::DecimalFormat::as_proto(), + crate::classes::java::text::FieldPosition::as_proto(), + crate::classes::java::text::Format::as_proto(), + crate::classes::java::text::NumberFormat::as_proto(), + crate::classes::java::text::ParseException::as_proto(), + crate::classes::java::text::ParsePosition::as_proto(), + crate::classes::java::text::SimpleDateFormat::as_proto(), crate::classes::java::util::AbstractCollection::as_proto(), crate::classes::java::util::AbstractList::as_proto(), crate::classes::java::util::AbstractMap::as_proto(), diff --git a/java_runtime/tests/classes/java/mod.rs b/java_runtime/tests/classes/java/mod.rs index 97328cf6..998c6719 100644 --- a/java_runtime/tests/classes/java/mod.rs +++ b/java_runtime/tests/classes/java/mod.rs @@ -1,4 +1,5 @@ mod io; mod lang; mod net; +mod text; mod util; diff --git a/java_runtime/tests/classes/java/text/mod.rs b/java_runtime/tests/classes/java/text/mod.rs new file mode 100644 index 00000000..98da6c8b --- /dev/null +++ b/java_runtime/tests/classes/java/text/mod.rs @@ -0,0 +1,3 @@ +mod test_date_format; +mod test_format; +mod test_number_format; diff --git a/java_runtime/tests/classes/java/text/test_date_format.rs b/java_runtime/tests/classes/java/text/test_date_format.rs new file mode 100644 index 00000000..639b3816 --- /dev/null +++ b/java_runtime/tests/classes/java/text/test_date_format.rs @@ -0,0 +1,186 @@ +use java_runtime::classes::java::{ + lang::{String, StringBuffer}, + text::{DateFormat, FieldPosition, ParsePosition, SimpleDateFormat}, + util::{Calendar, Date, SimpleTimeZone, TimeZone}, +}; +use jvm::{ClassInstanceRef, JavaError, Result, runtime::JavaLangString}; + +use test_utils::test_jvm; + +#[tokio::test] +async fn test_date_format_factories_use_english_patterns() -> Result<()> { + let jvm = test_jvm().await?; + let date: ClassInstanceRef = jvm.new_class("java/util/Date", "(J)V", (0i64,)).await?.into(); + + let short: ClassInstanceRef = jvm + .invoke_static("java/text/DateFormat", "getDateInstance", "(I)Ljava/text/DateFormat;", (3,)) + .await?; + let text: ClassInstanceRef = jvm + .invoke_virtual(&short, "format", "(Ljava/util/Date;)Ljava/lang/String;", (date.clone(),)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "1/1/70"); + + let medium: ClassInstanceRef = jvm + .invoke_static("java/text/DateFormat", "getDateInstance", "(I)Ljava/text/DateFormat;", (2,)) + .await?; + let text: ClassInstanceRef = jvm + .invoke_virtual(&medium, "format", "(Ljava/util/Date;)Ljava/lang/String;", (date.clone(),)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "Jan 1, 1970"); + + let time: ClassInstanceRef = jvm + .invoke_static("java/text/DateFormat", "getTimeInstance", "(I)Ljava/text/DateFormat;", (3,)) + .await?; + let text: ClassInstanceRef = jvm + .invoke_virtual(&time, "format", "(Ljava/util/Date;)Ljava/lang/String;", (date,)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "12:00 AM"); + + Ok(()) +} + +#[tokio::test] +async fn test_simple_date_format_patterns_quotes_and_fields() -> Result<()> { + let jvm = test_jvm().await?; + let pattern = JavaLangString::from_rust_string(&jvm, "yyyy-MM-dd 'at' HH:mm:ss.SSS EEEE MMMM a z").await?; + let format: ClassInstanceRef = jvm + .new_class("java/text/SimpleDateFormat", "(Ljava/lang/String;)V", (pattern,)) + .await? + .into(); + let date: ClassInstanceRef = jvm.new_class("java/util/Date", "(J)V", (0i64,)).await?.into(); + let text: ClassInstanceRef = jvm + .invoke_virtual(&format, "format", "(Ljava/util/Date;)Ljava/lang/String;", (date.clone(),)) + .await?; + assert_eq!( + JavaLangString::to_rust_string(&jvm, &text).await?, + "1970-01-01 at 00:00:00.000 Thursday January AM GMT" + ); + + let field_pattern = JavaLangString::from_rust_string(&jvm, "yyyy-MM").await?; + let _: () = jvm + .invoke_virtual(&format, "applyPattern", "(Ljava/lang/String;)V", (field_pattern,)) + .await?; + let prefix = JavaLangString::from_rust_string(&jvm, "on ").await?; + let buffer: ClassInstanceRef = jvm.new_class("java/lang/StringBuffer", "(Ljava/lang/String;)V", (prefix,)).await?.into(); + let position: ClassInstanceRef = jvm.new_class("java/text/FieldPosition", "(I)V", (1,)).await?.into(); + let _: ClassInstanceRef = jvm + .invoke_virtual( + &format, + "format", + "(Ljava/util/Date;Ljava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + (date, buffer.clone(), position.clone()), + ) + .await?; + let text: ClassInstanceRef = jvm.invoke_virtual(&buffer, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "on 1970-01"); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getBeginIndex", "()I", ()).await?, 3); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getEndIndex", "()I", ()).await?, 7); + + Ok(()) +} + +#[tokio::test] +async fn test_date_format_timezone_and_calendar_state() -> Result<()> { + let jvm = test_jvm().await?; + let pattern = JavaLangString::from_rust_string(&jvm, "yyyy-MM-dd HH:mm:ss z").await?; + let format: ClassInstanceRef = jvm + .new_class("java/text/SimpleDateFormat", "(Ljava/lang/String;)V", (pattern,)) + .await? + .into(); + let id = JavaLangString::from_rust_string(&jvm, "KST").await?; + let timezone: ClassInstanceRef = jvm + .new_class("java/util/SimpleTimeZone", "(ILjava/lang/String;)V", (9 * 60 * 60 * 1000, id)) + .await? + .into(); + let _: () = jvm.invoke_virtual(&format, "setTimeZone", "(Ljava/util/TimeZone;)V", (timezone,)).await?; + + let date: ClassInstanceRef = jvm.new_class("java/util/Date", "(J)V", (0i64,)).await?.into(); + let text: ClassInstanceRef = jvm + .invoke_virtual(&format, "format", "(Ljava/util/Date;)Ljava/lang/String;", (date,)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "1970-01-01 09:00:00 GMT+09:00"); + + let actual: ClassInstanceRef = jvm.invoke_virtual(&format, "getTimeZone", "()Ljava/util/TimeZone;", ()).await?; + assert_eq!( + jvm.invoke_virtual::<_, i32>(&actual, "getRawOffset", "()I", ()).await?, + 9 * 60 * 60 * 1000 + ); + let _: () = jvm.invoke_virtual(&format, "setLenient", "(Z)V", (false,)).await?; + assert!(!jvm.invoke_virtual::<_, bool>(&format, "isLenient", "()Z", ()).await?); + + Ok(()) +} + +#[tokio::test] +async fn test_simple_date_format_parse_and_positions() -> Result<()> { + let jvm = test_jvm().await?; + let pattern = JavaLangString::from_rust_string(&jvm, "yyyy-MM-dd HH:mm:ss.SSS").await?; + let format: ClassInstanceRef = jvm + .new_class("java/text/SimpleDateFormat", "(Ljava/lang/String;)V", (pattern,)) + .await? + .into(); + let source = JavaLangString::from_rust_string(&jvm, "1970-01-02 03:04:05.006tail").await?; + let position: ClassInstanceRef = jvm.new_class("java/text/ParsePosition", "(I)V", (0,)).await?.into(); + let date: ClassInstanceRef = jvm + .invoke_virtual( + &format, + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/util/Date;", + (source, position.clone()), + ) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i64>(&date, "getTime", "()J", ()).await?, 97_445_006); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getIndex", "()I", ()).await?, 23); + + let text_pattern = JavaLangString::from_rust_string(&jvm, "MMMM d, yyyy h:mm a z").await?; + let _: () = jvm + .invoke_virtual(&format, "applyPattern", "(Ljava/lang/String;)V", (text_pattern,)) + .await?; + let source = JavaLangString::from_rust_string(&jvm, "January 2, 1970 3:04 PM GMT").await?; + let date: ClassInstanceRef = jvm + .invoke_virtual(&format, "parse", "(Ljava/lang/String;)Ljava/util/Date;", (source,)) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i64>(&date, "getTime", "()J", ()).await?, 140_640_000); + + let invalid = JavaLangString::from_rust_string(&jvm, "not a date").await?; + let result = jvm + .invoke_virtual::<_, ClassInstanceRef>(&format, "parse", "(Ljava/lang/String;)Ljava/util/Date;", (invalid,)) + .await; + assert!(matches!(result, Err(JavaError::JavaException(_)))); + + Ok(()) +} + +#[tokio::test] +async fn test_date_format_clone_and_calendar_leniency_are_isolated() -> Result<()> { + let jvm = test_jvm().await?; + let pattern = JavaLangString::from_rust_string(&jvm, "yyyy-MM-dd").await?; + let format: ClassInstanceRef = jvm + .new_class("java/text/SimpleDateFormat", "(Ljava/lang/String;)V", (pattern,)) + .await? + .into(); + let calendar: ClassInstanceRef = jvm.invoke_virtual(&format, "getCalendar", "()Ljava/util/Calendar;", ()).await?; + let _: () = jvm.invoke_virtual(&calendar, "setTimeInMillis", "(J)V", (0i64,)).await?; + + let cloned: ClassInstanceRef = jvm.invoke_virtual(&format, "clone", "()Ljava/lang/Object;", ()).await?; + let cloned_calendar: ClassInstanceRef = jvm.invoke_virtual(&cloned, "getCalendar", "()Ljava/util/Calendar;", ()).await?; + let _: () = jvm.invoke_virtual(&cloned_calendar, "setTimeInMillis", "(J)V", (86_400_000i64,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&calendar, "get", "(I)I", (5,)).await?, 1); + assert_eq!(jvm.invoke_virtual::<_, i32>(&cloned_calendar, "get", "(I)I", (5,)).await?, 2); + + let other: ClassInstanceRef = jvm + .invoke_static("java/util/Calendar", "getInstance", "()Ljava/util/Calendar;", ()) + .await?; + let _: () = jvm.invoke_virtual(&other, "setTimeInMillis", "(J)V", (0i64,)).await?; + assert!( + jvm.invoke_virtual::<_, bool>(&calendar, "equals", "(Ljava/lang/Object;)Z", (other.clone(),)) + .await? + ); + let _: () = jvm.invoke_virtual(&other, "setLenient", "(Z)V", (false,)).await?; + assert!( + !jvm.invoke_virtual::<_, bool>(&calendar, "equals", "(Ljava/lang/Object;)Z", (other,)) + .await? + ); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/text/test_format.rs b/java_runtime/tests/classes/java/text/test_format.rs new file mode 100644 index 00000000..29856c64 --- /dev/null +++ b/java_runtime/tests/classes/java/text/test_format.rs @@ -0,0 +1,110 @@ +use java_runtime::classes::java::{ + lang::{Long, Object, String}, + text::{FieldPosition, Format, NumberFormat, ParseException, ParsePosition}, +}; +use jvm::{ClassInstanceRef, Result, runtime::JavaLangString}; + +use test_utils::test_jvm; + +#[tokio::test] +async fn test_text_common_class_hierarchy() -> Result<()> { + let jvm = test_jvm().await?; + + let format = jvm.resolve_class("java/text/Format").await?; + assert!(jvm.is_inherited_from(&*format.definition, "java/lang/Object")); + + let parse_exception = jvm.resolve_class("java/text/ParseException").await?; + assert!(jvm.is_inherited_from(&*parse_exception.definition, "java/lang/Exception")); + + Ok(()) +} + +#[tokio::test] +async fn test_field_position_accessors_and_equality() -> Result<()> { + let jvm = test_jvm().await?; + let position: ClassInstanceRef = jvm.new_class("java/text/FieldPosition", "(I)V", (1,)).await?.into(); + + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getField", "()I", ()).await?, 1); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getBeginIndex", "()I", ()).await?, 0); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getEndIndex", "()I", ()).await?, 0); + + let _: () = jvm.invoke_virtual(&position, "setBeginIndex", "(I)V", (3,)).await?; + let _: () = jvm.invoke_virtual(&position, "setEndIndex", "(I)V", (7,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getBeginIndex", "()I", ()).await?, 3); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getEndIndex", "()I", ()).await?, 7); + + let same: ClassInstanceRef = jvm.new_class("java/text/FieldPosition", "(I)V", (1,)).await?.into(); + let _: () = jvm.invoke_virtual(&same, "setBeginIndex", "(I)V", (3,)).await?; + let _: () = jvm.invoke_virtual(&same, "setEndIndex", "(I)V", (7,)).await?; + assert!( + jvm.invoke_virtual::<_, bool>(&position, "equals", "(Ljava/lang/Object;)Z", (same.clone(),)) + .await? + ); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&position, "hashCode", "()I", ()).await?, + jvm.invoke_virtual::<_, i32>(&same, "hashCode", "()I", ()).await? + ); + + Ok(()) +} + +#[tokio::test] +async fn test_parse_position_accessors_and_equality() -> Result<()> { + let jvm = test_jvm().await?; + let position: ClassInstanceRef = jvm.new_class("java/text/ParsePosition", "(I)V", (2,)).await?.into(); + + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getIndex", "()I", ()).await?, 2); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getErrorIndex", "()I", ()).await?, -1); + + let _: () = jvm.invoke_virtual(&position, "setIndex", "(I)V", (5,)).await?; + let _: () = jvm.invoke_virtual(&position, "setErrorIndex", "(I)V", (4,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getIndex", "()I", ()).await?, 5); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getErrorIndex", "()I", ()).await?, 4); + + let same: ClassInstanceRef = jvm.new_class("java/text/ParsePosition", "(I)V", (5,)).await?.into(); + let _: () = jvm.invoke_virtual(&same, "setErrorIndex", "(I)V", (4,)).await?; + assert!( + jvm.invoke_virtual::<_, bool>(&position, "equals", "(Ljava/lang/Object;)Z", (same,)) + .await? + ); + + Ok(()) +} + +#[tokio::test] +async fn test_parse_exception_error_offset_and_message() -> Result<()> { + let jvm = test_jvm().await?; + let message = JavaLangString::from_rust_string(&jvm, "bad date").await?; + let exception: ClassInstanceRef = jvm + .new_class("java/text/ParseException", "(Ljava/lang/String;I)V", (message, 6)) + .await? + .into(); + + assert_eq!(jvm.invoke_virtual::<_, i32>(&exception, "getErrorOffset", "()I", ()).await?, 6); + let actual: ClassInstanceRef = jvm.invoke_virtual(&exception, "getMessage", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &actual).await?, "bad date"); + + Ok(()) +} + +#[tokio::test] +async fn test_format_polymorphic_format_and_parse_object() -> Result<()> { + let jvm = test_jvm().await?; + let number: ClassInstanceRef = jvm + .invoke_static("java/text/NumberFormat", "getInstance", "()Ljava/text/NumberFormat;", ()) + .await?; + let format: ClassInstanceRef = ClassInstanceRef::new(number.instance); + let value: ClassInstanceRef = jvm.new_class("java/lang/Long", "(J)V", (1234i64,)).await?.into(); + let text: ClassInstanceRef = jvm + .invoke_virtual(&format, "format", "(Ljava/lang/Object;)Ljava/lang/String;", (value,)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "1,234"); + + let source = JavaLangString::from_rust_string(&jvm, "1,234").await?; + let parsed: ClassInstanceRef = jvm + .invoke_virtual(&format, "parseObject", "(Ljava/lang/String;)Ljava/lang/Object;", (source,)) + .await?; + assert!(jvm.is_instance(&**parsed, "java/lang/Long")); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/text/test_number_format.rs b/java_runtime/tests/classes/java/text/test_number_format.rs new file mode 100644 index 00000000..7536499d --- /dev/null +++ b/java_runtime/tests/classes/java/text/test_number_format.rs @@ -0,0 +1,171 @@ +use java_runtime::classes::java::{ + lang::{Double, Long, Number, String, StringBuffer}, + text::{DecimalFormat, FieldPosition, NumberFormat, ParsePosition}, +}; +use jvm::{ClassInstanceRef, Result, runtime::JavaLangString}; + +use test_utils::test_jvm; + +#[tokio::test] +async fn test_number_format_factories_format_values() -> Result<()> { + let jvm = test_jvm().await?; + + let number: ClassInstanceRef = jvm + .invoke_static("java/text/NumberFormat", "getInstance", "()Ljava/text/NumberFormat;", ()) + .await?; + let integer: ClassInstanceRef = jvm.invoke_virtual(&number, "format", "(J)Ljava/lang/String;", (1_234_567i64,)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &integer).await?, "1,234,567"); + let decimal: ClassInstanceRef = jvm.invoke_virtual(&number, "format", "(D)Ljava/lang/String;", (1234.5f64,)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &decimal).await?, "1,234.5"); + + let percent: ClassInstanceRef = jvm + .invoke_static("java/text/NumberFormat", "getPercentInstance", "()Ljava/text/NumberFormat;", ()) + .await?; + let text: ClassInstanceRef = jvm.invoke_virtual(&percent, "format", "(D)Ljava/lang/String;", (0.12f64,)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "12%"); + + let currency: ClassInstanceRef = jvm + .invoke_static("java/text/NumberFormat", "getCurrencyInstance", "()Ljava/text/NumberFormat;", ()) + .await?; + let text: ClassInstanceRef = jvm.invoke_virtual(¤cy, "format", "(D)Ljava/lang/String;", (1234.5f64,)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "$1,234.50"); + + Ok(()) +} + +#[tokio::test] +async fn test_number_format_digit_and_grouping_settings() -> Result<()> { + let jvm = test_jvm().await?; + let number: ClassInstanceRef = jvm + .invoke_static("java/text/NumberFormat", "getNumberInstance", "()Ljava/text/NumberFormat;", ()) + .await?; + + let _: () = jvm.invoke_virtual(&number, "setGroupingUsed", "(Z)V", (false,)).await?; + let _: () = jvm.invoke_virtual(&number, "setMinimumFractionDigits", "(I)V", (2,)).await?; + let text: ClassInstanceRef = jvm.invoke_virtual(&number, "format", "(D)Ljava/lang/String;", (1234.5f64,)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "1234.50"); + + let _: () = jvm.invoke_virtual(&number, "setMaximumFractionDigits", "(I)V", (1,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&number, "getMinimumFractionDigits", "()I", ()).await?, 1); + assert!(!jvm.invoke_virtual::<_, bool>(&number, "isGroupingUsed", "()Z", ()).await?); + + Ok(()) +} + +#[tokio::test] +async fn test_decimal_format_pattern_and_field_position() -> Result<()> { + let jvm = test_jvm().await?; + let pattern = JavaLangString::from_rust_string(&jvm, "0000.00").await?; + let format: ClassInstanceRef = jvm + .new_class("java/text/DecimalFormat", "(Ljava/lang/String;)V", (pattern,)) + .await? + .into(); + + let text: ClassInstanceRef = jvm.invoke_virtual(&format, "format", "(D)Ljava/lang/String;", (12.3f64,)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "0012.30"); + + let grouping = JavaLangString::from_rust_string(&jvm, "#,##0.###").await?; + let _: () = jvm.invoke_virtual(&format, "applyPattern", "(Ljava/lang/String;)V", (grouping,)).await?; + let prefix = JavaLangString::from_rust_string(&jvm, "pre ").await?; + let buffer: ClassInstanceRef = jvm.new_class("java/lang/StringBuffer", "(Ljava/lang/String;)V", (prefix,)).await?.into(); + let position: ClassInstanceRef = jvm.new_class("java/text/FieldPosition", "(I)V", (0,)).await?.into(); + let _: ClassInstanceRef = jvm + .invoke_virtual( + &format, + "format", + "(DLjava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + (1234.5f64, buffer.clone(), position.clone()), + ) + .await?; + let text: ClassInstanceRef = jvm.invoke_virtual(&buffer, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "pre 1,234.5"); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getBeginIndex", "()I", ()).await?, 4); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getEndIndex", "()I", ()).await?, 9); + + Ok(()) +} + +#[tokio::test] +async fn test_number_format_parse_and_positions() -> Result<()> { + let jvm = test_jvm().await?; + let number: ClassInstanceRef = jvm + .invoke_static("java/text/NumberFormat", "getInstance", "()Ljava/text/NumberFormat;", ()) + .await?; + let source = JavaLangString::from_rust_string(&jvm, "1,234.5rest").await?; + let position: ClassInstanceRef = jvm.new_class("java/text/ParsePosition", "(I)V", (0,)).await?.into(); + let parsed: ClassInstanceRef = jvm + .invoke_virtual( + &number, + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/lang/Number;", + (source, position.clone()), + ) + .await?; + assert_eq!(jvm.invoke_virtual::<_, f64>(&parsed, "doubleValue", "()D", ()).await?, 1234.5); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getIndex", "()I", ()).await?, 7); + + let integer_source = JavaLangString::from_rust_string(&jvm, "42").await?; + let parsed: ClassInstanceRef = jvm + .invoke_virtual(&number, "parse", "(Ljava/lang/String;)Ljava/lang/Number;", (integer_source,)) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i64>(&parsed, "longValue", "()J", ()).await?, 42); + + let invalid = JavaLangString::from_rust_string(&jvm, "not-a-number").await?; + let position: ClassInstanceRef = jvm.new_class("java/text/ParsePosition", "(I)V", (0,)).await?.into(); + let parsed: ClassInstanceRef = jvm + .invoke_virtual( + &number, + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/lang/Number;", + (invalid, position.clone()), + ) + .await?; + assert!(parsed.is_null()); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getIndex", "()I", ()).await?, 0); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getErrorIndex", "()I", ()).await?, 0); + + Ok(()) +} + +#[tokio::test] +async fn test_decimal_format_quoted_affixes_and_integer_boundaries() -> Result<()> { + let jvm = test_jvm().await?; + let pattern = JavaLangString::from_rust_string(&jvm, "'%'0").await?; + let format: ClassInstanceRef = jvm + .new_class("java/text/DecimalFormat", "(Ljava/lang/String;)V", (pattern,)) + .await? + .into(); + let text: ClassInstanceRef = jvm.invoke_virtual(&format, "format", "(J)Ljava/lang/String;", (12i64,)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "%12"); + + let source = JavaLangString::from_rust_string(&jvm, "%12").await?; + let parsed: ClassInstanceRef = jvm + .invoke_virtual(&format, "parse", "(Ljava/lang/String;)Ljava/lang/Number;", (source,)) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i64>(&parsed, "longValue", "()J", ()).await?, 12); + + let apostrophe_pattern = JavaLangString::from_rust_string(&jvm, "''0").await?; + let _: () = jvm + .invoke_virtual(&format, "applyPattern", "(Ljava/lang/String;)V", (apostrophe_pattern,)) + .await?; + let text: ClassInstanceRef = jvm.invoke_virtual(&format, "format", "(J)Ljava/lang/String;", (12i64,)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "'12"); + + let plain_pattern = JavaLangString::from_rust_string(&jvm, "0").await?; + let _: () = jvm + .invoke_virtual(&format, "applyPattern", "(Ljava/lang/String;)V", (plain_pattern,)) + .await?; + let maximum = JavaLangString::from_rust_string(&jvm, "9223372036854775807").await?; + let parsed: ClassInstanceRef = jvm + .invoke_virtual(&format, "parse", "(Ljava/lang/String;)Ljava/lang/Number;", (maximum,)) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i64>(&parsed, "longValue", "()J", ()).await?, i64::MAX); + + let overflow = JavaLangString::from_rust_string(&jvm, "9223372036854775808").await?; + let parsed: ClassInstanceRef = jvm + .invoke_virtual(&format, "parse", "(Ljava/lang/String;)Ljava/lang/Number;", (overflow,)) + .await?; + assert!(jvm.is_instance(&**parsed, "java/lang/Double")); + + Ok(()) +} From cab6756385b4a9e5a8544f87f1e3d733301ccfd0 Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Sun, 19 Jul 2026 09:13:39 +0900 Subject: [PATCH 2/3] Add integer number format factories --- .../src/classes/java/text/number_format.rs | 36 +++++++++++++++++++ .../classes/java/text/test_number_format.rs | 32 +++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/java_runtime/src/classes/java/text/number_format.rs b/java_runtime/src/classes/java/text/number_format.rs index fab05451..dc3463e7 100644 --- a/java_runtime/src/classes/java/text/number_format.rs +++ b/java_runtime/src/classes/java/text/number_format.rs @@ -89,6 +89,18 @@ impl NumberFormat { Self::get_number_instance_with_locale, MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, ), + JavaMethodProto::new( + "getIntegerInstance", + "()Ljava/text/NumberFormat;", + Self::get_integer_instance, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "getIntegerInstance", + "(Ljava/util/Locale;)Ljava/text/NumberFormat;", + Self::get_integer_instance_with_locale, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), JavaMethodProto::new( "getCurrencyInstance", "()Ljava/text/NumberFormat;", @@ -368,6 +380,30 @@ impl NumberFormat { .into()) } + async fn get_integer_instance(jvm: &Jvm, _: &mut RuntimeContext) -> Result> { + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + jvm.invoke_static( + "java/text/NumberFormat", + "getIntegerInstance", + "(Ljava/util/Locale;)Ljava/text/NumberFormat;", + (locale,), + ) + .await + } + + async fn get_integer_instance_with_locale(jvm: &Jvm, _: &mut RuntimeContext, locale: ClassInstanceRef) -> Result> { + if locale.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "locale").await); + } + let pattern = JavaLangString::from_rust_string(jvm, "#,##0").await?; + let format: ClassInstanceRef = jvm + .new_class("java/text/DecimalFormat", "(Ljava/lang/String;)V", (pattern,)) + .await? + .into(); + let _: () = jvm.invoke_virtual(&format, "setParseIntegerOnly", "(Z)V", (true,)).await?; + Ok(format) + } + async fn get_currency_instance(jvm: &Jvm, _: &mut RuntimeContext) -> Result> { let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; jvm.invoke_static( diff --git a/java_runtime/tests/classes/java/text/test_number_format.rs b/java_runtime/tests/classes/java/text/test_number_format.rs index 7536499d..8cbef50d 100644 --- a/java_runtime/tests/classes/java/text/test_number_format.rs +++ b/java_runtime/tests/classes/java/text/test_number_format.rs @@ -1,6 +1,7 @@ use java_runtime::classes::java::{ lang::{Double, Long, Number, String, StringBuffer}, text::{DecimalFormat, FieldPosition, NumberFormat, ParsePosition}, + util::Locale, }; use jvm::{ClassInstanceRef, Result, runtime::JavaLangString}; @@ -33,6 +34,37 @@ async fn test_number_format_factories_format_values() -> Result<()> { Ok(()) } +#[tokio::test] +async fn test_number_format_integer_factories() -> Result<()> { + let jvm = test_jvm().await?; + + let default: ClassInstanceRef = jvm + .invoke_static("java/text/NumberFormat", "getIntegerInstance", "()Ljava/text/NumberFormat;", ()) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&default, "getMaximumFractionDigits", "()I", ()).await?, 0); + assert!(jvm.invoke_virtual::<_, bool>(&default, "isParseIntegerOnly", "()Z", ()).await?); + + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + let integer: ClassInstanceRef = jvm + .invoke_static( + "java/text/NumberFormat", + "getIntegerInstance", + "(Ljava/util/Locale;)Ljava/text/NumberFormat;", + (locale,), + ) + .await?; + let text: ClassInstanceRef = jvm.invoke_virtual(&integer, "format", "(D)Ljava/lang/String;", (1234.6f64,)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "1,235"); + + let source = JavaLangString::from_rust_string(&jvm, "1,234.5").await?; + let parsed: ClassInstanceRef = jvm + .invoke_virtual(&integer, "parse", "(Ljava/lang/String;)Ljava/lang/Number;", (source,)) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i64>(&parsed, "longValue", "()J", ()).await?, 1234); + + Ok(()) +} + #[tokio::test] async fn test_number_format_digit_and_grouping_settings() -> Result<()> { let jvm = test_jvm().await?; From 84bf15cab42f6a409a8b35912e3b53eadfabd2b1 Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Sun, 19 Jul 2026 09:31:39 +0900 Subject: [PATCH 3/3] Fix text format position handling --- .../src/classes/java/text/decimal_format.rs | 67 +++++++++--- .../classes/java/text/simple_date_format.rs | 25 +++-- .../classes/java/text/test_date_format.rs | 55 ++++++++++ .../classes/java/text/test_number_format.rs | 100 +++++++++++++++++- 4 files changed, 224 insertions(+), 23 deletions(-) diff --git a/java_runtime/src/classes/java/text/decimal_format.rs b/java_runtime/src/classes/java/text/decimal_format.rs index 71e90808..d4a7ea57 100644 --- a/java_runtime/src/classes/java/text/decimal_format.rs +++ b/java_runtime/src/classes/java/text/decimal_format.rs @@ -341,16 +341,16 @@ impl DecimalFormat { let base: i32 = jvm.invoke_virtual(&buffer, "length", "()I", ()).await?; let field: i32 = jvm.invoke_virtual(&position, "getField", "()I", ()).await?; if field == 0 { - let begin = base + prefix.chars().count() as i32; + let begin = base + prefix.encode_utf16().count() as i32; let _: () = jvm.invoke_virtual(&position, "setBeginIndex", "(I)V", (begin,)).await?; let _: () = jvm - .invoke_virtual(&position, "setEndIndex", "(I)V", (begin + integer.chars().count() as i32,)) + .invoke_virtual(&position, "setEndIndex", "(I)V", (begin + integer.encode_utf16().count() as i32,)) .await?; } else if field == 1 && !fraction.is_empty() { - let begin = base + prefix.chars().count() as i32 + integer.chars().count() as i32 + 1; + let begin = base + prefix.encode_utf16().count() as i32 + integer.encode_utf16().count() as i32 + 1; let _: () = jvm.invoke_virtual(&position, "setBeginIndex", "(I)V", (begin,)).await?; let _: () = jvm - .invoke_virtual(&position, "setEndIndex", "(I)V", (begin + fraction.chars().count() as i32,)) + .invoke_virtual(&position, "setEndIndex", "(I)V", (begin + fraction.encode_utf16().count() as i32,)) .await?; } @@ -428,11 +428,22 @@ impl DecimalFormat { } let source = JavaLangString::to_rust_string(jvm, &source).await?; let characters: Vec = source.chars().collect(); + let mut utf16_indices = Vec::with_capacity(characters.len() + 1); + let mut utf16_index = 0; + for character in &characters { + utf16_indices.push(utf16_index); + utf16_index += character.len_utf16(); + } + utf16_indices.push(utf16_index); let start: i32 = jvm.invoke_virtual(&position, "getIndex", "()I", ()).await?; - if start < 0 || start as usize > characters.len() { + if start < 0 { let _: () = jvm.invoke_virtual(&position, "setErrorIndex", "(I)V", (start,)).await?; return Ok(ClassInstanceRef::new(None)); } + let Some(start_index) = utf16_indices.iter().position(|index| *index == start as usize) else { + let _: () = jvm.invoke_virtual(&position, "setErrorIndex", "(I)V", (start,)).await?; + return Ok(ClassInstanceRef::new(None)); + }; let positive_prefix: ClassInstanceRef = jvm.get_field(&this, "positivePrefix", "Ljava/lang/String;").await?; let positive_suffix: ClassInstanceRef = jvm.get_field(&this, "positiveSuffix", "Ljava/lang/String;").await?; @@ -443,8 +454,8 @@ impl DecimalFormat { let negative_prefix: Vec = JavaLangString::to_rust_string(jvm, &negative_prefix).await?.chars().collect(); let negative_suffix: Vec = JavaLangString::to_rust_string(jvm, &negative_suffix).await?.chars().collect(); - let mut index = start as usize; - let negative = if characters[index..].starts_with(&negative_prefix) && negative_prefix != positive_prefix { + let mut index = start_index; + let prefix_negative = if characters[index..].starts_with(&negative_prefix) && negative_prefix != positive_prefix { index += negative_prefix.len(); true } else if characters[index..].starts_with(&positive_prefix) { @@ -457,9 +468,6 @@ impl DecimalFormat { let parse_integer_only: bool = jvm.get_field(&this, "parseIntegerOnly", "Z").await?; let mut normalized = RustString::new(); - if negative { - normalized.push('-'); - } let mut digits = 0; let mut decimal = false; while index < characters.len() { @@ -478,12 +486,41 @@ impl DecimalFormat { _ => break, } } - let suffix = if negative { &negative_suffix } else { &positive_suffix }; - if digits == 0 || !characters[index..].starts_with(suffix) { - let _: () = jvm.invoke_virtual(&position, "setErrorIndex", "(I)V", (index as i32,)).await?; + if digits == 0 { + let _: () = jvm + .invoke_virtual(&position, "setErrorIndex", "(I)V", (utf16_indices[index] as i32,)) + .await?; return Ok(ClassInstanceRef::new(None)); } - index += suffix.len(); + let negative = if negative_prefix == positive_prefix { + let positive_matches = characters[index..].starts_with(&positive_suffix); + let negative_matches = characters[index..].starts_with(&negative_suffix); + if negative_matches && (!positive_matches || negative_suffix.len() > positive_suffix.len()) { + index += negative_suffix.len(); + true + } else if positive_matches { + index += positive_suffix.len(); + false + } else { + let _: () = jvm + .invoke_virtual(&position, "setErrorIndex", "(I)V", (utf16_indices[index] as i32,)) + .await?; + return Ok(ClassInstanceRef::new(None)); + } + } else { + let suffix = if prefix_negative { &negative_suffix } else { &positive_suffix }; + if !characters[index..].starts_with(suffix) { + let _: () = jvm + .invoke_virtual(&position, "setErrorIndex", "(I)V", (utf16_indices[index] as i32,)) + .await?; + return Ok(ClassInstanceRef::new(None)); + } + index += suffix.len(); + prefix_negative + }; + if negative { + normalized.insert(0, '-'); + } let Ok(mut value) = normalized.parse::() else { let _: () = jvm.invoke_virtual(&position, "setErrorIndex", "(I)V", (start,)).await?; @@ -493,7 +530,7 @@ impl DecimalFormat { if multiplier != 0 { value /= f64::from(multiplier); } - let _: () = jvm.invoke_virtual(&position, "setIndex", "(I)V", (index as i32,)).await?; + let _: () = jvm.invoke_virtual(&position, "setIndex", "(I)V", (utf16_indices[index] as i32,)).await?; let _: () = jvm.invoke_virtual(&position, "setErrorIndex", "(I)V", (-1,)).await?; if multiplier == 1 diff --git a/java_runtime/src/classes/java/text/simple_date_format.rs b/java_runtime/src/classes/java/text/simple_date_format.rs index 35463dca..5216307b 100644 --- a/java_runtime/src/classes/java/text/simple_date_format.rs +++ b/java_runtime/src/classes/java/text/simple_date_format.rs @@ -463,7 +463,7 @@ impl SimpleDateFormat { match token { DateToken::Literal(value) => formatted.push_str(&value), DateToken::Field(character, count) => { - let begin = formatted.chars().count() as i32; + let begin = formatted.encode_utf16().count() as i32; match character { 'G' => formatted.push_str(if date_time.year() <= 0 { "BC" } else { "AD" }), 'y' => { @@ -512,7 +512,7 @@ impl SimpleDateFormat { if !field_position_set && Self::date_field(character) == Some(requested_field) { let _: () = jvm.invoke_virtual(&position, "setBeginIndex", "(I)V", (base + begin,)).await?; let _: () = jvm - .invoke_virtual(&position, "setEndIndex", "(I)V", (base + formatted.chars().count() as i32,)) + .invoke_virtual(&position, "setEndIndex", "(I)V", (base + formatted.encode_utf16().count() as i32,)) .await?; field_position_set = true; } @@ -541,22 +541,35 @@ impl SimpleDateFormat { }; let source = JavaLangString::to_rust_string(jvm, &source).await?; let characters: Vec = source.chars().collect(); + let mut utf16_indices = Vec::with_capacity(characters.len() + 1); + let mut utf16_index = 0; + for character in &characters { + utf16_indices.push(utf16_index); + utf16_index += character.len_utf16(); + } + utf16_indices.push(utf16_index); let start: i32 = jvm.invoke_virtual(&position, "getIndex", "()I", ()).await?; - if start < 0 || start as usize > characters.len() { + if start < 0 { let _: () = jvm.invoke_virtual(&position, "setErrorIndex", "(I)V", (start,)).await?; return Ok(ClassInstanceRef::new(None)); } + let Some(start_index) = utf16_indices.iter().position(|index| *index == start as usize) else { + let _: () = jvm.invoke_virtual(&position, "setErrorIndex", "(I)V", (start,)).await?; + return Ok(ClassInstanceRef::new(None)); + }; let calendar: ClassInstanceRef = jvm.get_field(&this, "calendar", "Ljava/util/Calendar;").await?; let time_zone: ClassInstanceRef = jvm.invoke_virtual(&calendar, "getTimeZone", "()Ljava/util/TimeZone;", ()).await?; let offset: i32 = jvm.invoke_virtual(&time_zone, "getRawOffset", "()I", ()).await?; - match Self::parse_timestamp(&tokens, &characters, start as usize, offset) { + match Self::parse_timestamp(&tokens, &characters, start_index, offset) { Ok((timestamp, index)) => { - let _: () = jvm.invoke_virtual(&position, "setIndex", "(I)V", (index as i32,)).await?; + let _: () = jvm.invoke_virtual(&position, "setIndex", "(I)V", (utf16_indices[index] as i32,)).await?; let _: () = jvm.invoke_virtual(&position, "setErrorIndex", "(I)V", (-1,)).await?; Ok(jvm.new_class("java/util/Date", "(J)V", (timestamp,)).await?.into()) } Err(error_index) => { - let _: () = jvm.invoke_virtual(&position, "setErrorIndex", "(I)V", (error_index as i32,)).await?; + let _: () = jvm + .invoke_virtual(&position, "setErrorIndex", "(I)V", (utf16_indices[error_index] as i32,)) + .await?; Ok(ClassInstanceRef::new(None)) } } diff --git a/java_runtime/tests/classes/java/text/test_date_format.rs b/java_runtime/tests/classes/java/text/test_date_format.rs index 639b3816..73603f3d 100644 --- a/java_runtime/tests/classes/java/text/test_date_format.rs +++ b/java_runtime/tests/classes/java/text/test_date_format.rs @@ -151,6 +151,61 @@ async fn test_simple_date_format_parse_and_positions() -> Result<()> { Ok(()) } +#[tokio::test] +async fn test_simple_date_format_uses_utf16_positions() -> Result<()> { + let jvm = test_jvm().await?; + let pattern = JavaLangString::from_rust_string(&jvm, "'\u{1f600}'yyyy").await?; + let format: ClassInstanceRef = jvm + .new_class("java/text/SimpleDateFormat", "(Ljava/lang/String;)V", (pattern,)) + .await? + .into(); + let date: ClassInstanceRef = jvm.new_class("java/util/Date", "(J)V", (0i64,)).await?.into(); + let buffer: ClassInstanceRef = jvm.new_class("java/lang/StringBuffer", "()V", ()).await?.into(); + let position: ClassInstanceRef = jvm.new_class("java/text/FieldPosition", "(I)V", (1,)).await?.into(); + let _: ClassInstanceRef = jvm + .invoke_virtual( + &format, + "format", + "(Ljava/util/Date;Ljava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + (date, buffer.clone(), position.clone()), + ) + .await?; + let text: ClassInstanceRef = jvm.invoke_virtual(&buffer, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "\u{1f600}1970"); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getBeginIndex", "()I", ()).await?, 2); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getEndIndex", "()I", ()).await?, 6); + + let pattern = JavaLangString::from_rust_string(&jvm, "yyyy-MM-dd").await?; + let _: () = jvm.invoke_virtual(&format, "applyPattern", "(Ljava/lang/String;)V", (pattern,)).await?; + let source = JavaLangString::from_rust_string(&jvm, "\u{1f600}1970-01-02").await?; + let position: ClassInstanceRef = jvm.new_class("java/text/ParsePosition", "(I)V", (2,)).await?.into(); + let date: ClassInstanceRef = jvm + .invoke_virtual( + &format, + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/util/Date;", + (source, position.clone()), + ) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i64>(&date, "getTime", "()J", ()).await?, 86_400_000); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getIndex", "()I", ()).await?, 12); + + let source = JavaLangString::from_rust_string(&jvm, "\u{1f600}197x").await?; + let position: ClassInstanceRef = jvm.new_class("java/text/ParsePosition", "(I)V", (2,)).await?.into(); + let date: ClassInstanceRef = jvm + .invoke_virtual( + &format, + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/util/Date;", + (source, position.clone()), + ) + .await?; + assert!(date.is_null()); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getErrorIndex", "()I", ()).await?, 5); + + Ok(()) +} + #[tokio::test] async fn test_date_format_clone_and_calendar_leniency_are_isolated() -> Result<()> { let jvm = test_jvm().await?; diff --git a/java_runtime/tests/classes/java/text/test_number_format.rs b/java_runtime/tests/classes/java/text/test_number_format.rs index 8cbef50d..8d6e5975 100644 --- a/java_runtime/tests/classes/java/text/test_number_format.rs +++ b/java_runtime/tests/classes/java/text/test_number_format.rs @@ -56,11 +56,18 @@ async fn test_number_format_integer_factories() -> Result<()> { let text: ClassInstanceRef = jvm.invoke_virtual(&integer, "format", "(D)Ljava/lang/String;", (1234.6f64,)).await?; assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "1,235"); - let source = JavaLangString::from_rust_string(&jvm, "1,234.5").await?; + let source = JavaLangString::from_rust_string(&jvm, "1,234.5rest").await?; + let position: ClassInstanceRef = jvm.new_class("java/text/ParsePosition", "(I)V", (0,)).await?.into(); let parsed: ClassInstanceRef = jvm - .invoke_virtual(&integer, "parse", "(Ljava/lang/String;)Ljava/lang/Number;", (source,)) + .invoke_virtual( + &integer, + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/lang/Number;", + (source, position.clone()), + ) .await?; assert_eq!(jvm.invoke_virtual::<_, i64>(&parsed, "longValue", "()J", ()).await?, 1234); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getIndex", "()I", ()).await?, 5); Ok(()) } @@ -201,3 +208,92 @@ async fn test_decimal_format_quoted_affixes_and_integer_boundaries() -> Result<( Ok(()) } + +#[tokio::test] +async fn test_decimal_format_uses_utf16_positions_and_negative_suffixes() -> Result<()> { + let jvm = test_jvm().await?; + let pattern = JavaLangString::from_rust_string(&jvm, "'\u{1f600}'0.0").await?; + let format: ClassInstanceRef = jvm + .new_class("java/text/DecimalFormat", "(Ljava/lang/String;)V", (pattern,)) + .await? + .into(); + + let buffer: ClassInstanceRef = jvm.new_class("java/lang/StringBuffer", "()V", ()).await?.into(); + let integer_position: ClassInstanceRef = jvm.new_class("java/text/FieldPosition", "(I)V", (0,)).await?.into(); + let _: ClassInstanceRef = jvm + .invoke_virtual( + &format, + "format", + "(DLjava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + (12.3f64, buffer, integer_position.clone()), + ) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&integer_position, "getBeginIndex", "()I", ()).await?, 2); + assert_eq!(jvm.invoke_virtual::<_, i32>(&integer_position, "getEndIndex", "()I", ()).await?, 4); + + let buffer: ClassInstanceRef = jvm.new_class("java/lang/StringBuffer", "()V", ()).await?.into(); + let fraction_position: ClassInstanceRef = jvm.new_class("java/text/FieldPosition", "(I)V", (1,)).await?.into(); + let _: ClassInstanceRef = jvm + .invoke_virtual( + &format, + "format", + "(DLjava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + (12.3f64, buffer, fraction_position.clone()), + ) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&fraction_position, "getBeginIndex", "()I", ()).await?, 5); + assert_eq!(jvm.invoke_virtual::<_, i32>(&fraction_position, "getEndIndex", "()I", ()).await?, 6); + + let plain_pattern = JavaLangString::from_rust_string(&jvm, "0").await?; + let _: () = jvm + .invoke_virtual(&format, "applyPattern", "(Ljava/lang/String;)V", (plain_pattern,)) + .await?; + let source = JavaLangString::from_rust_string(&jvm, "\u{1f600}12").await?; + let position: ClassInstanceRef = jvm.new_class("java/text/ParsePosition", "(I)V", (2,)).await?.into(); + let parsed: ClassInstanceRef = jvm + .invoke_virtual( + &format, + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/lang/Number;", + (source, position.clone()), + ) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i64>(&parsed, "longValue", "()J", ()).await?, 12); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getIndex", "()I", ()).await?, 4); + + let prefix_pattern = JavaLangString::from_rust_string(&jvm, "'\u{1f600}'0x").await?; + let _: () = jvm + .invoke_virtual(&format, "applyPattern", "(Ljava/lang/String;)V", (prefix_pattern,)) + .await?; + let source = JavaLangString::from_rust_string(&jvm, "\u{1f600}12y").await?; + let position: ClassInstanceRef = jvm.new_class("java/text/ParsePosition", "(I)V", (0,)).await?.into(); + let parsed: ClassInstanceRef = jvm + .invoke_virtual( + &format, + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/lang/Number;", + (source, position.clone()), + ) + .await?; + assert!(parsed.is_null()); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getErrorIndex", "()I", ()).await?, 4); + + let suffix_pattern = JavaLangString::from_rust_string(&jvm, "0;0-").await?; + let _: () = jvm + .invoke_virtual(&format, "applyPattern", "(Ljava/lang/String;)V", (suffix_pattern,)) + .await?; + let source = JavaLangString::from_rust_string(&jvm, "12-").await?; + let position: ClassInstanceRef = jvm.new_class("java/text/ParsePosition", "(I)V", (0,)).await?.into(); + let parsed: ClassInstanceRef = jvm + .invoke_virtual( + &format, + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/lang/Number;", + (source, position.clone()), + ) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i64>(&parsed, "longValue", "()J", ()).await?, -12); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getIndex", "()I", ()).await?, 3); + + Ok(()) +}