Does Moshi support Iso8601 dates? - kotlin

I have the following Retrofit/Moshi setup to support Rfc3339 Dates:
val retrofit = Retrofit.Builder()
.baseUrl(Config.getApiUrl())
.client(okHttpClient)
.addConverterFactory(
MoshiConverterFactory.create(
Moshi.Builder().add(Date::class.java, Rfc3339DateJsonAdapter())
.add(KotlinJsonAdapterFactory()).build()
)
)
.build()
Sadly it throws this error, when it retrieves the expires_at below:
Not an RFC 3339 date: 2021-03-20T20:24:00
I realised then that the API returns ISO 8601 dates. (Fast-API in Python)
{
"expires_at": "2021-03-21T17:08:00",
}
Moshi Adapter doesn't seem to include an adapter for ISO 8601. Unless I'm not seeing the woods for the trees.
What am I missing please?
UPDATE:
Is it recommended using the java.util.Date for the date serialisation, when utilising the Rfc3339DateJsonAdapter?
import java.util.*
#JsonClass(generateAdapter = true)
data class SignInJson(
val product_id: String?,
val expires_at: Date,
)

Related

How can I convert a data class to a room entity class and a UI class?

I'm pulling values ​​like conversion_rates from the exchange api and I don't want to use all of these values. Because there are data that I do not want to use in the data coming from the api. In addition, I want to save the data I want to use in the room and perform operations such as reading and updating from there. But I couldn't find how to convert the data class to room entity and the data class I want to show in ui.
The data in the api is as follows, for example. This example uses USD currency
{
"result":"success",
"documentation":"https://www.exchangerate-api.com/docs",
"terms_of_use":"https://www.exchangerate-api.com/terms",
"time_last_update_unix":1669852801,
"time_last_update_utc":"Thu, 01 Dec 2022 00:00:01 +0000",
"time_next_update_unix":1669939201,
"time_next_update_utc":"Fri, 02 Dec 2022 00:00:01 +0000",
"base_code":"USD",
"conversion_rates":{
"USD":1,
"AED":3.6725,
"AFN":88.4843,
"ALL":112.6064,
"AMD":395.1203,
"ANG":1.7900,
"AOA":509.9729,
"ARS":166.6930,
"AUD":1.4810,
"AWG":1.7900,
"AZN":1.6987,
"BAM":1.8843,
"BBD":2.0000,
"BDT":101.1341,
"BGN":1.8844,
"BHD":0.3760,
"BIF":2059.0872,
"BMD":1.0000,
"BND":1.3652,
"BOB":6.9223,
"BRL":5.2810,
"BSD":1.0000,
"BTN":81.2806,
"BWP":12.8706,
"BYN":2.8013,
"BZD":2.0000,
"CAD":1.3488,
"CDF":2054.0886,
"CHF":0.9472,
"CLP":900.6901,
"CNY":7.0769,
"COP":4807.5892,
"CRC":599.5200,
"CUP":24.0000,
"CVE":106.2304,
"CZK":23.4794,
"DJF":177.7210,
"DKK":7.1874,
"DOP":54.5873,
"DZD":138.5566,
"EGP":24.5835,
"ERN":15.0000,
"ETB":53.4589,
"EUR":0.9631,
"FJD":2.2145,
"FKP":0.8319,
"FOK":7.1874,
"GBP":0.8317,
"GEL":2.7155,
"GGP":0.8319,
"GHS":14.2807,
"GIP":0.8319,
"GMD":63.3901,
"GNF":8622.0204,
"GTQ":7.8226,
"GYD":209.1401,
"HKD":7.8054,
"HNL":24.6994,
"HRK":7.2588,
"HTG":140.6488,
"HUF":393.5594,
"IDR":15633.2624,
"ILS":3.4355,
"IMP":0.8319,
"INR":81.2818,
"IQD":1458.8962,
"IRR":42024.7114,
"ISK":141.8861,
"JEP":0.8319,
"JMD":154.0163,
"JOD":0.7090,
"JPY":138.2686,
"KES":122.8172,
"KGS":84.4061,
"KHR":4125.2930,
"KID":1.4813,
"KMF":473.9665,
"KRW":1311.7736,
"KWD":0.2996,
"KYD":0.8333,
"KZT":469.2553,
"LAK":17329.4289,
"LBP":1507.5000,
"LKR":363.3087,
"LRD":153.9036,
"LSL":17.0637,
"LYD":4.8894,
"MAD":10.7013,
"MDL":19.4034,
"MGA":4347.5707,
"MKD":59.4442,
"MMK":2452.0310,
"MNT":3422.5815,
"MOP":8.0398,
"MRU":38.0126,
"MUR":43.6263,
"MVR":15.4107,
"MWK":1029.7547,
"MXN":19.2883,
"MYR":4.4641,
"MZN":64.0629,
"NAD":17.0637,
"NGN":443.7037,
"NIO":36.3802,
"NOK":9.8760,
"NPR":130.0490,
"NZD":1.5947,
"OMR":0.3845,
"PAB":1.0000,
"PEN":3.8417,
"PGK":3.5202,
"PHP":56.3904,
"PKR":223.9704,
"PLN":4.4951,
"PYG":7228.9088,
"QAR":3.6400,
"RON":4.7382,
"RSD":113.3592,
"RUB":60.9665,
"RWF":1099.6940,
"SAR":3.7500,
"SBD":8.1340,
"SCR":13.0123,
"SDG":568.2933,
"SEK":10.5247,
"SGD":1.3649,
"SHP":0.8319,
"SLE":18.6210,
"SLL":18620.9957,
"SOS":568.2695,
"SRD":30.9539,
"SSP":643.6471,
"STN":23.6035,
"SYP":2502.5362,
"SZL":17.0637,
"THB":35.2322,
"TJS":10.1162,
"TMT":3.4979,
"TND":3.1050,
"TOP":2.3745,
"TRY":18.6339,
"TTD":6.7542,
"TVD":1.4813,
"TWD":30.6481,
"TZS":2333.7110,
"UAH":36.4908,
"UGX":3747.1520,
"UYU":39.4702,
"UZS":11228.0193,
"VES":11.0789,
"VND":24625.3969,
"VUV":121.1513,
"WST":2.7337,
"XAF":631.9553,
"XCD":2.7000,
"XDR":0.7612,
"XOF":631.9553,
"XPF":114.9656,
"YER":250.0464,
"ZAR":17.0568,
"ZMW":17.0409,
"ZWL":653.4299
}
}
but here, for example, I don't want to save
time_next_update_utc,
"time_next_update_unix",
"time_last_update_utc,
time_last_update_unix,
terms_of_use",
documentation
data to the room, and I don't want to show them to the user either.
because data is useless to me. I just want to save the following data in the room and show it to the user
"result",
"base_code",
"conversion_rates"
and I prepared something like that.
Room
Entity
#Entity(tableName = "ExchangeValues")
data class ExchangeEntity(
#ColumnInfo(name = "allData") val allData: AllData,
#PrimaryKey(autoGenerate = true) val uid:Int?=null
)
Dao
#Dao
interface ExchangeDao {
#Query("SELECT * FROM ExchangeValues")
suspend fun getAll() : List<ExchangeEntity>
#Query("UPDATE ExchangeValues SET ")
suspend fun update()
}
Remote
Dto
#Serializable
data class AllData(
val allData:List<ExchangeDto>
)
#Serializable
data class ExchangeDto(
val base_code: String,
val conversion_rates: ConversionRates,
val documentation: String,
val result: String,
val terms_of_use: String,
val time_last_update_unix: Int,
val time_last_update_utc: String,
val time_next_update_unix: Int,
val time_next_update_utc: String
)
#Serializable
data class ConversionRates(
val conversionRates : Map<String,Double>
)
In this Dto, I wrote extension functions such as toEntity to convert it to an entity or to convert it to a model class that I will display ui, but I couldn't make it up. I'm a little lacking in this
Repository
interface ExchangeRepository {
suspend fun getAll() : Flow<List<AllData>>
suspend fun update(exchange: ExchangeDto)
}
RepositoryImpl
class ExchangeRepositoryImpl #Inject constructor(
private val dao:ExchangeDao
) : ExchangeRepository{
override suspend fun getAll() : Flow<List<AllData>> {
return flow {
emit(dao.getAll().map {it.allData}) // this part is wrong
}
}
override suspend fun update(exchange: ExchangeDto) {
dao.update(exchange.base_code,exchange.result,exchange.conversion_rates) // While updating, it is important that it is compatible with my entity class. There is also a problem here.
}
}
I guess I didn't prepare the dto properly. Actually, I'm sorry that I may have shared the codes a bit incompletely and badly because I don't know exactly what to do, but I hope you understand what I want to do.
The problem is when I getAll in the ExchangeRepositoryImpl , the ExchangeEntity is constantly coming, and I can't translate it either. At the same time, I couldn't quite figure out how to adapt the update in ExchangeRepositoryImpl to them. because i need to update data every 24 hours according to data in api.

kmongo connot save kotlinx-datetime objects

I am trying to use kmongo with kotlinx-datetime. The data class looks like this:
#Serializable
data class CustomerCount(
#Contextual val _id: Id<CustomerCount>?,
val counter: Int,
#Contextual val date: LocalDateTime
) {
constructor(counter: Int): this(
null,
counter,
Clock.System.now().toLocalDateTime(TimeZone.of("Europe/Zurich"))
)
}
When I now try to save an instance of CustomerCount to MongoDB I get the following error:
com.mongodb.MongoWriteException: Write operation error on server localhost:27017. Write error: WriteError{code=2, message=''date' must be present and contain a valid BSON UTC datetime value', details={}}.
How can I save kotlinx-datetime objects to MongoDB?

kotlin SimpleDateFormat Unparseable date

How to format date from String in kotlin?
I tried to parse it with a SimpleDateFormat but it always throws an Exception saying Unparseable date: "21 Agt 2022" when I try to parse the String.
This my code:
var spf = SimpleDateFormat("dd MMM yyyy")
val newDate = spf.parse("21 Agt 2022") // <- always error in this line
spf = SimpleDateFormat("yyyy-MM-dd")
val result = newDate?.let { it1 -> spf.format(it1) }.toString()
My app is running on API 21, so can't use a java.time.LocalDate.
You can use java.time and its LocalDate, you have two options so far:
the ThreeTen Android Backport: a library that makes most java.time functionality available, you will have to import it
Android API Desugaring available from Android Gradle Plugin 4.0.0 or higher
The reason for the error is the missing Locale, which would be a problem in java.time as well:
The abbreviation Agt for August is only used in two Locales: Indonesia (where you seem to be from, at least judging from your profile page) and Kenia.
That means you can use your code, you just have to apply an Indonesian Locale:
fun main(args: Array<String>) {
// prepare the locale your input was created in
val indonesia = Locale.forLanguageTag("id-ID")
// use it in your SimpleDateFormat
var spf = SimpleDateFormat("dd MMM yyyy", indonesia)
// parse the value
val newDate = spf.parse("21 Agt 2022")
// print the value
println(newDate)
}
Output:
Sun Aug 21 00:00:00 CEST 2022
This will create a java.util.Date which is in fact more than day of month, month of year and year… It has a time of day, too, but your input String does not contain any. That means it will add one, the start of day, most likely.
Better / Newer / Date only: java.time
fun main(args: Array<String>) {
// your input String
val input = "21 Agt 2022"
// prepare the locale your input uses
val indonesia = Locale.forLanguageTag("id-ID")
// prepare a DateTimeFormatter that considers Indonesian months
val dtf = DateTimeFormatter.ofPattern("dd MMM uuuu", indonesia)
// parse the String using the DateTimeFormatter
val localDate = LocalDate.parse(input, dtf)
// print the result
println(localDate)
}
Output:
2022-08-21

Convert string into LocalDate Kotlin

I want to convert string value
this is my code like
val dateFirst = "20 Aug 2012"
val dateSecond = "12/16/2020 12:00:00 AM"
val dateFirstConverted = LocalDate.parse(dateFirst, DateTimeFormatter.BASIC_ISO_DATE)
val dateSecondConverted = LocalDate.parse(dateSecond, DateTimeFormatter.BASIC_ISO_DATE)
println(dateFirstConverted)
println(dateSecondConverted)
then i get error like this.
Exception in thread "main" java.time.format.DateTimeParseException: Text '20 Aug 2012' could not be parsed at index 0
at java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:1949)
at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1851)
at java.time.LocalDate.parse(LocalDate.java:400)
at App.TryKt.main(try.kt:11)
at App.TryKt.main(try.kt)
can someone help me how to fix this ?
you have problem because the format of the date is not supported, I invite you to read this article https://www.claudebueno.com/programmation/comment-gerer-la-date-et-lheure-avec-kotlin.htm but in your case if you want that the code runs, change the format of date, like this:
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.time.LocalDate
fun main() {
//example
val current = LocalDateTime.now()
val formatter = DateTimeFormatter.BASIC_ISO_DATE
val formatted = current.format(formatter)
println("Current Date is: $formatted")
//your code
val dates = /*"20 Aug 2012"*/ "20120820"
val datess = LocalDate.parse(dates, DateTimeFormatter.BASIC_ISO_DATE)
println(datess)
}
tl;dr ⇒ You are using the wrong pattern for parsing
Your date String is of the format dd MMM uuuu (a non ISO format) but you are trying to parse it with a DateTimeFormatter.ISO_LOCAL_DATE
Your datetime String is of the format MM/dd/uuuu hh:mm:ss a (non ISO) but you are trying to parse it with a DateTimeFormatter.ISO_LOCAL_DATE, which is at least doubly wrong because that formatter tries to parse an ISO date. Your String is non ISO and contains more information (time of day) than this formatter is able to parse.
There are several built-in DateTimeFormatters, like the one you are currently using, but you need to use a correct one or if there is none, create one that covers your String(s) yourself (either by DateTimeFormatter.ofPattern(...) or by using a DateTimeFormatterBuilder).
Here's a small example for your String examples:
fun main(args: Array<String>) {
// your example Strings
val dateFirst = "20 Aug 2012"
val dateSecond = "12/16/2020 12:00:00 AM"
// you need two different formatters here, your Strings differ in format and content
val firstFormatter = DateTimeFormatter.ofPattern("dd MMM uuuu", Locale.ENGLISH)
val secondFormatter = DateTimeFormatter.ofPattern("MM/dd/uuuu hh:mm:ss a", Locale.ENGLISH)
// then use those according to what you want to parse
val localDate = LocalDate.parse(dateFirst, firstFormatter)
val localDateTime = LocalDateTime.parse(dateSecond, secondFormatter)
// use the built-in formatters for output
println(localDate.format(DateTimeFormatter.ISO_LOCAL_DATE))
println(localDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME))
}
Output (in ISO):
2012-08-20
2020-12-16T00:00:00

How to deserialize dates with offset ("2019-01-29+01:00") to `java.time` related classes?

I've refactored some legacy code within Spring Boot (2.1.2) system and migrated from java.util.Date to java.time based classes (jsr310). The system expects the dates in a ISO8601 formated string, whereas some are complete timestamps with time information (e.g. "2019-01-29T15:29:34+01:00") while others are only dates with offset (e.g. "2019-01-29+01:00"). Here is the DTO (as Kotlin data class):
data class Dto(
// ...
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ssXXX")
#JsonProperty("processingTimestamp")
val processingTimestamp: OffsetDateTime,
// ...
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-ddXXX")
#JsonProperty("orderDate")
val orderDate: OffsetDateTime,
// ...
)
While Jackson perfectly deserializes processingTimestamp, it fails with orderDate:
Caused by: java.time.DateTimeException: Unable to obtain OffsetDateTime from TemporalAccessor: {OffsetSeconds=32400},ISO resolved to 2018-10-23 of type java.time.format.Parsed
at java.time.OffsetDateTime.from(OffsetDateTime.java:370) ~[na:1.8.0_152]
at com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer.deserialize(InstantDeserializer.java:207) ~[jackson-datatype-jsr310-2.9.8.jar:2.9.8]
This makes sense to me, since OffsetDateTime cannot find any time information necessary to construct the instant. If I change to val orderDate: LocalDate Jackson can successfully deserialize, but then the offset information is gone (which I need to convert to Instant later).
Question
My current workaround is to use OffsetDateTime, in combination with a custom deserializer (see below). But I'm wondering, if there is a better solution for this?
Also, I'd wish for a more appropriate data type like OffsetDate, but I cannot find it in java.time.
PS
I was asking myself if "2019-01-29+01:00" is a valid for ISO8601. However, since I found that java.time.DateTimeFormatter.ISO_DATE is can correctly parse it and I cannot change the format how the clients send data, I put aside this question.
Workaround
data class Dto(
// ...
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-ddXXX")
#JsonProperty("catchDate")
#JsonDeserialize(using = OffsetDateDeserializer::class)
val orderDate: OffsetDateTime,
// ...
)
class OffsetDateDeserializer(
private val formatter: DateTimeFormatter = DateTimeFormatter.ISO_DATE
) : JSR310DateTimeDeserializerBase<OffsetDateTime>(OffsetDateTime::class.java, formatter) {
override fun deserialize(parser: JsonParser, context: DeserializationContext): OffsetDateTime? {
if (parser.hasToken(JsonToken.VALUE_STRING)) {
val string = parser.text.trim()
if (string.isEmpty()) {
return null
}
val parsed: TemporalAccessor = formatter.parse(string)
val offset = if(parsed.isSupported(ChronoField.OFFSET_SECONDS)) ZoneOffset.from(parsed) else ZoneOffset.UTC
val localDate = LocalDate.from(parsed)
return OffsetDateTime.of(localDate.atStartOfDay(), offset)
}
throw context.wrongTokenException(parser, _valueClass, parser.currentToken, "date with offset must be contained in string")
}
override fun withDateFormat(otherFormatter: DateTimeFormatter?): JsonDeserializer<OffsetDateTime> = OffsetDateDeserializer(formatter)
}
As #JodaStephen explained in the comments, OffsetDate was not included in java.time to have a minimal set of classes. So, OffsetDateTime is the best option.
He also suggested to use DateTimeFormatterBuilder and parseDefaulting to create a DateTimeFormatter instance, to directly create OffsetDateTime from the formatters parsing result (TemporalAccessor). AFAIK, I still need to create a custom deserializer to use the formatter. Here is code, which solved my problem:
class OffsetDateDeserializer: JsonDeserializer<OffsetDateTime>() {
private val formatter = DateTimeFormatterBuilder()
.append(DateTimeFormatter.ISO_DATE)
.parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
.parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
.parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0)
.parseDefaulting(ChronoField.MILLI_OF_SECOND, 0)
.parseDefaulting(ChronoField.OFFSET_SECONDS, 0)
.toFormatter()
override fun deserialize(parser: JsonParser, context: DeserializationContext): OffsetDateTime? {
if (parser.hasToken(JsonToken.VALUE_STRING)) {
val string = parser.text.trim()
if (string.isEmpty()) {
return null
}
try {
return OffsetDateTime.from(formatter.parse(string))
} catch (e: DateTimeException){
throw context.wrongTokenException(parser, OffsetDateTime::class.java, parser.currentToken, "error while parsing date: ${e.message}")
}
}
throw context.wrongTokenException(parser, OffsetDateTime::class.java, parser.currentToken, "date with offset must be contained in string")
}
}