I have a date in the future and have to format the remaining time until this day like so.
4 days
1 month, 4 days
1 year, 1 month
I have looked at the DateUtils documentation but haven't seen this exact format.
I'm also fine using an external library like threetenabp.
Is there a library that can handle both the time calculation and the localization of the strings?
I wrote this blog a while ago, it shows how to do the opposite of what you are asking :-) https://blog.blundellapps.co.uk/creating-comments-with-timestamps-like-youtube/ i.e. given a time, say how long ago that was.
Android also offers this solution for times in the past: https://developer.android.com/reference/android/text/format/DateUtils.html#getRelativeDateTimeString(android.content.Context,%20long,%20long,%20long,%20int).
So you need the opposite of these!
It should not be too hard to inverse. Instead of using the time now and negating the difference of a time in the past. You use the time now and add the time in the future.
So the inverse of: https://github.com/blundell/YouTubeTimeStamps/blob/master/app/src/main/java/com/blundell/tut/TimeStampFormatter.kt
The main difference being you want a difference between now and a date in the future, like so:
private fun getMillisFromNow(futureTime: Date): Long {
val futureTimeMillis = futureTime.time
val nowMillis = System.currentTimeMillis()
return futureTimeMillis - nowMillis
}
And then format it. Something like this:
fun format(timestamp: Date): String {
val millisFromNow = getMillisFromNow(timestamp)
val minutesFromNow = TimeUnit.MILLISECONDS.toMinutes(millisFromNow)
if (minutesFromNow < 1) {
return "about now"
}
val hoursFromNow = TimeUnit.MILLISECONDS.toHours(millisFromNow)
if (hoursFromNow < 1) {
return formatMinutes(minutesFromNow)
}
val daysFromNow = TimeUnit.MILLISECONDS.toDays(millisFromNow)
if (daysFromNow < 1) {
return formatHours(hoursFromNow)
}
val weeksFromNow = TimeUnit.MILLISECONDS.toDays(millisFromNow) / 7
if (weeksFromNow < 1) {
return formatDays(daysFromNow)
}
val monthsFromNow = TimeUnit.MILLISECONDS.toDays(millisFromNow) / 30
if (monthsFromNow < 1) {
return formatWeeks(weeksFromNow)
}
val yearsFromNow = TimeUnit.MILLISECONDS.toDays(millisFromNow) / 365
return if (yearsFromNow < 1) {
formatMonths(monthsFromNow)
} else formatYears(yearsFromNow)
}
private fun getMillisFromNow(futureTime: Date): Long {
val futureTimeMillis = futureTime.time
val nowMillis = System.currentTimeMillis()
return futureTimeMillis - nowMillis
}
private fun formatMinutes(minutes: Long): String {
return format(minutes, " minute to go", " minutes to go")
}
private fun formatHours(hours: Long): String {
return format(hours, " hour to go", " hours to go")
}
private fun formatDays(days: Long): String {
return format(days, " day to go", " days to go")
}
private fun formatWeeks(weeks: Long): String {
return format(weeks, " week to go", " weeks to go")
}
private fun formatMonths(months: Long): String {
return format(months, " month to go", " months to go")
}
private fun formatYears(years: Long): String {
return format(years, " year to go", " years to go")
}
private fun format(hand: Long, singular: String, plural: String): String {
return if (hand == 1L) {
hand.toString() + singular
} else {
hand.toString() + plural
}
}
Just sanity checked it with this test:
#Test
fun test() {
val twoDaysInMillisInstant = Instant.now().plusMillis(TimeUnit.HOURS.toMillis(50))
val result = format(Date.from(twoDaysInMillisInstant))
assertEquals("2 days to go", result)
}
For the localization, you can convert this to using Strings.xml.
using threeten's LocalDate you can get the remaining years, months and day from one date to another. Having that values you can 0-check them and show only ones that are > 0.
Simple example:
val futureDate = LocalDate.of(2020,2,20)
val todayDate = LocalDate.now()
val remainingYears = futureDate.year - todayDate.year //output 0
val remainingMonth = futureDate.monthValue - todayDate.monthValue //output 0
val remainingDays = futureDate.dayOfMonth - todayDate.dayOfMonth // output 16
I hope that is what you wanted, cheers!
Related
How can I get the dates of the current week using kotlinx.datetime KMM library?
eg. I want to get a list of current week's date like:
("Monday, 07", "Tuesday, 08", "Wednesday, 09", ... )
This is a similar impementation using Calendar:
fun getDaysOfWeek(): Array<String?> {
val dateFormat = SimpleDateFormat("EEEEE\ndd", Locale.getDefault())
val calendar = Calendar.getInstance()
calendar.firstDayOfWeek = Calendar.MONDAY
calendar.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY)
val days = arrayOfNulls<String>(7)
for (i in 0..6) {
days[i] = dateFormat.format(calendar.time)
calendar.add(Calendar.DAY_OF_MONTH, 1)
}
return days
}
val today = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date
val days = mutableListOf<LocalDate>()
val firstWeekDay = today.daysShift(-DayOfWeek.values().indexOf(today.dayOfWeek))
for (i in 0 until DayOfWeek.values().count()) {
days.add(firstWeekDay.daysShift(i))
}
val dayStrings = days.map { "${it.dayOfWeek}, ${it.dayOfMonth}" }
println("$dayStrings")
fun LocalDate.daysShift(days: Int): LocalDate = when {
days < 0 -> {
minus(DateTimeUnit.DayBased(-days))
}
days > 0 -> {
plus(DateTimeUnit.DayBased(days))
}
else -> this
}
I'm trying to create a flow with coroutines but it's not giving to me the expected result.
What I'd like to have is giving an expiration time (doesn't matter if it's in millis, seconds, etc..) when the time arrives to 0 it stops the countdown. What I have now is :
private fun tickerFlow(start: Long, end: Long = 0L) = flow {
var count = start
while (count >= end) {
emit(Unit)
count--
delay(1_000L)
}
}
And then I call this function as :
val expireDate = LocalDateTime.now().plusSeconds(10L).toEpochSecond(ZoneOffset.UTC)
tickerFlow(expireDate)
.map { LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) - expireDate }
.distinctUntilChanged { old, new ->
old == new
}
.onEach {
//Here I should print the timer going down with this pattern
//00h: 00m: 00s I did it with String.format("%02dh: %02dm: %02ds") and it works though.
}
.onCompletion {
//Setting the text when completed
}
.launchIn(scope = scope)
But even with this test that what I'm trying is to have the expiry time as 10 seconds from now it doesn't print nor end as I would. Am I missing something? Is there any way I could emit the local date time so I have the hours, minutes and seconds? perhaps I have to do the calculus to get the seconds, minutes hours from milis / seconds.
TLDR;
I'm getting from backend an expiry date, and I want to know when this expiry date finish so I have to calculate it with the now() and check when it is expired.
You don't really need a flow here. Try this code:
val expireDate = LocalDateTime.now().plusSeconds(10L).toEpochSecond(ZoneOffset.UTC)
val currentTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC)
for (i in (expireDate - currentTime) downTo 1) {
println("$i seconds remaining") // Format the remaining seconds however you wish
delay(1_000) // Delay for 1 second
}
println("TIME UP") // Run your completion code here
Also, this code is safe to run on main thread as delay doesn't block.
In your code, the problem is that you are passing the expireDate itself to tickerFlow. expireDate contains the time in seconds from epoch and not the seconds difference from current time. Just pass expireDate - LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) to tickerFlow and it will work.
EDIT: Complete implementation using flow
private fun tickerFlow(start: Long, end: Long = 0L) = flow {
for (i in start downTo end) {
emit(i)
delay(1_000)
}
}
val expireDate = LocalDateTime.now().plusSeconds(10L).toEpochSecond(ZoneOffset.UTC)
val currentTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC)
tickerFlow(expireDate - currentTime)
.onEach { secondsRemaining ->
// Format and display the time
}
.onCompletion {
// Handle completion
}
.launchIn(scope)
Solution with flow
private fun countDownFlow(
start: Long,
delayInSeconds: Long = 1_000L,
) = flow {
var count = start
while (count >= 0L) {
emit(count--)
delay(delayInSeconds)
}
}
And then given an expiration date, get the current date subtract them and pass it as start.
val expireDate = LocalDateTime.now().plusSeconds(10L).toEpochSecond(ZoneOffset.UTC)
val currentTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC)
tickerFlow(expireDate - currentTime)
.onEach {
binding.yourTimer.text = String.format(
"%02dh: %02dm: %02ds",
TimeUnit.SECONDS.toHours(it),
TimeUnit.SECONDS.toMinutes(it),
TimeUnit.SECONDS.toSeconds(it),
)
}
.onCompletion {
//Update UI
}
.launchIn(coroutineScope)
If in hurry copy/paste this!
fun getFlow( delayTimeMilliseconds: Long, startValue: Long, stepValue : Long = 1, endValue : Long =0): Flow<Long> =
( startValue downTo endValue step stepValue ).asFlow().flowOn(Dispatchers.IO)
.onEach { delay( delayTimeMilliseconds) }
.onStart { emit( startValue) }
.conflate()
.transform { remainingValue: Long ->
if(remainingValue<0) emit(0)
else emit( remainingValue)
}
On my production app I use timer as an UseCase as per clean architecture. Like this :
class TimerFlowUseCase constructor() : StateFlowUseCase<TimerFlowUseCase.Params, Long>() {
override suspend fun getFlow(params: Params): Flow<Long> =
(params.startValue downTo params.endValue step params.stepValue ).asFlow().flowOn(Dispatchers.IO)
.onEach { delay(params.delayTimeMilliseconds) }
.onStart { emit(params.startValue) } // Emits total value on start
.conflate()
.transform { remainingValue: Long ->
if(remainingValue<0) emit(0)
else emit( remainingValue)
}
data class Params( val delayTimeMilliseconds: Long, val startValue: Long, val stepValue : Long = 1, val endValue : Long =0)
}
Where superclass is:
abstract class StateFlowUseCase<P, R> {
suspend operator fun invoke(params: P , coroutineScope: CoroutineScope): StateFlow<R> {
return getFlow(params).stateIn(coroutineScope)
}
abstract suspend fun getFlow(params: P): Flow<R>
}
I want to create a function that returns a list of dates in the given range, with recursion. I will provide the starting date, the ending date, and the type of recursion. I am not sure how to start. Any suggestions would be helpful. Is there any library for this, or do I have to do it on my own?
data class Date(
val day: Int,
val month: Int,
val year: Int
)
enum class Recursion {
NEVER,
EVERY_DAY,
EVERY_WORK_DAY,
EVERY_WEEK,
EVERY_MONTH,
ANNUAL
}
fun createListOfEvents(startDate: Date, endDate: Date, recursion: Recursion): List<Date>{
}
You need to enable desugaring if targeting SDK < 26.
Then you can use the LocalDate class for this. Your Date class is kind of redundant, but you could convert inside the function to LocalDate if you want to keep it.
fun createListOfEvents(startDate: LocalDate, endDate: LocalDate, recursion: Recursion): List<LocalDate> {
val step = when (recursion) {
Recursion.NEVER -> return emptyList()
Recursion.EVERY_DAY, Recursion.EVERY_WORK_DAY -> Period.ofDays(1)
Recursion.EVERY_WEEK -> Period.ofWeeks(1)
Recursion.EVERY_MONTH -> Period.ofMonths(1)
Recursion.ANNUAL -> Period.ofYears(1)
}
var date = startDate
val list = mutableListOf<LocalDate>()
while (date <= endDate) {
list.add(date)
date += step
}
if (recursion == Recursion.EVERY_WORK_DAY) {
val weekend = listOf(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY)
list.removeAll { it.dayOfWeek in weekend }
}
return list
}
Well, basically this is an example of how you can do this:
data class Recursion(val field : TemporalUnit, val step: Long)
val step1Day = Recursion(field = ChronoUnit.DAYS, step = 1)
fun createListOfEvents(startDate: LocalDate, endDate: LocalDate, recursion: Recursion): List<LocalDate>{
var currentDate = startDate
val listOfDates = mutableListOf<LocalDate>()
while (currentDate.isBefore(endDate)) {
listOfDates.add(currentDate)
currentDate = startDate.plus(recursion.step, recursion.field)
}
return listOfDates
}
This method returns list of dates from startDate until endDate with the step of Recursion.
As you can see I've used java.time.* classes for this, but eventually you can convert them into your own Date and Recursion and back.
Here TemporalUnit can be DAYS, WEEKS, MONTHS, YEARS (and other).
It covers most of your needs, working days you will have to manage manually.
Hope this makes sense )
I'm building an Android App with Kotlin that uses various NYTimes APIs to fetch different news data..
Here I have a search screen where I want the user to be able to enter a search query (i.e. "Japan") and check off any checkbox and they will even be able to add a begin or end date to refine their search :
Typing something in the search query and checking off at least one box are the only requirements, everything else will be mandatory. And once they hit "Search", it will pass the data they entered to a second activity which will use the data to put together an api call, make the api call, and populate a recyclerview , like so:
Now here is my issue...
I have the SEARCH button only sending data if the search query has been entered and if the travel checkbox has been checked, and as you can imagine there are a TON of combinations.. I don't know how I can pass over all of those combinations and make each API Call accordingly without having an extremely long If/Else Block that'll take forever...
Using Kotlin, there has to be a more efficient way right ?
Here is my Search Activity:
class SearchActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_search)
search_query_edittext.getBackground().clearColorFilter();
actionBar?.setDisplayHomeAsUpEnabled(true)
Enter_Begin_Date.setPaintFlags(Enter_Begin_Date.getPaintFlags())
Enter_End_Date.setPaintFlags(Enter_End_Date.getPaintFlags())
// Calendar
val c = Calendar.getInstance()
val year = c.get(Calendar.YEAR)
val month = c.get(Calendar.MONTH)
val day = c.get(Calendar.DAY_OF_MONTH)
// TextView Clicked to show Date Picker Dialog
Enter_Begin_Date.setOnClickListener {
val dpd = DatePickerDialog(
this,
DatePickerDialog.OnDateSetListener { view, Year, Month, Day ->
// set to textView
if (Day < 10 && Month < 10) {
Enter_Begin_Date.text =
"0" + Day + "/0" + Month.toInt().plus(1) + "/" + Year
} else if (Day < 10 && Month >= 10) {
Enter_Begin_Date.text = "0" + Day + "/" + Month.toInt().plus(1) + "/" + Year
} else if (Day >= 10 && Month < 10) {
Enter_Begin_Date.text = "" + Day + "/0" + Month.toInt().plus(1) + "/" + Year
}
},
year,
month,
day
)
// show dialog
dpd.show()
}
Enter_End_Date.setOnClickListener {
val dpd = DatePickerDialog(
this,
DatePickerDialog.OnDateSetListener { view, Year, Month, Day ->
// set to textView
if (Day < 10 && Month < 10) {
Enter_End_Date.text = "0" + Day + "/0" + Month.toInt().plus(1) + "/" + Year
} else if (Day < 10 && Month >= 10) {
Enter_End_Date.text = "0" + Day + "/" + Month.toInt().plus(1) + "/" + Year
} else if (Day >= 10 && Month < 10) {
Enter_End_Date.text = "" + Day + "/0" + Month.toInt().plus(1) + "/" + Year
}
},
year,
month,
day
)
// show dialog
dpd.show()
}
searchButton.setOnClickListener {
if ((search_query_edittext.text.isNotEmpty()
&& Enter_Begin_Date.text.isEmpty()
&& Enter_End_Date.text.isEmpty()) && TravelTextBox.isChecked && !SportsTextBox.isChecked && !PoliticsTextBox.isChecked && !EntrepreneursTextBox.isChecked && !BusinessTextBox.isChecked && !ArtsCheckBox.isChecked
) {
val searchQuery: String = search_query_edittext.text.toString()
val query = searchQuery
val travelCategory: String = "Travel"
val intent = Intent(this#SearchActivity, ResultsActivity::class.java)
intent.putExtra("query", query)
intent.putExtra("travelCategory", travelCategory)
startActivity(intent)
}
}
}
}
My Result Activity:
class ResultsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_results)
val bundle: Bundle? = intent.extras
val myQuery = bundle!!.getString("query")
val myTravelCategory = bundle!!.getString("travelCategory")
if (bundle.containsKey("query") && bundle.containsKey("travelCategory")) {
lifecycleScope.launch(IO) {
val result = ApiClient.getClient.getSearchResultWithCheckbox(query = myQuery!!,
category = myTravelCategory!!,
api_key = (MYAPIKEY)
withContext(Main) {
SearchResultsRecyclerView.apply {
layoutManager = LinearLayoutManager(this#ResultsActivity)
adapter = SearchResultsAdapter(result.response.docs)
}
}
}
}
}
}
As you can see, I only have coded enough to cover for the user to enter in the search query , to check the travel checkbox and to hit enter, there are like 145 more combinations (search query + begin date + politics checkbox, search query + end date + sports checkbox, etc etc..)
How can I add in all of the other combinations without using an extremely long If/Else block?
Anything to push me in the right direction would be highly appreciated , thanks!
check for the condition you want, not the combinations that produce it.
Check if the search query is filled and that at least one checkbox is checked. Without knowing how the API works is hard to recommend a particular way to pass the data but taking advantage of nullable types and/or optional paramaters with something like this should work:
private fun getCheckedCategories() : List<String> = listOfNotNull(
"Travel".takeIf { TravelTextBox.isChecked },
...
)
private fun atLeastOneCheckBoxChecked() = getCheckedCategories().isNotEmpty()
With this helper functions you can build a listener similar to this:
searchButton.setOnClickListener {
if (search_query_edittext.text.isNotEmpty() && atLeastOneCheckBoxChecked()) {
val searchQuery: String = search_query_edittext.text.toString()
val checkedCategories = getCheckedCategories()
val beginDate : String? = TODO()
val endDate : String? = TODO()
val intent = Intent(this#SearchActivity, ResultsActivity::class.java)
intent.putExtra("query", searchQuery)
intent.putStringArrayList("checkedCategories", ArrayList(checkedCategories))
intent.putExtra("beginDate", beginDate)
intent.putExtra("endDate", endDate)
startActivity(intent)
}
}
In the other end the bundle?.getString("beginDate") will return null if not passed, and whatever value it has if passed. Or retrieve the list of passed checkboxes with bundle?getStringArrayList("checkedCategories").orEmpty()
I have a web site running on IIS that will take two time variables, calculate those variables and produce a time differnce in -Minutes (eg StartTime = "18:00", EndTime = "18:30", Diff = "-30")
I have this working perfectly in VBScript using the DateDiff() function. I have searched high and low, looking for a similar solution for both Android and iOS so I can build the time calculation into my mobile app and have it available off line(if there is not internet connectivity for example).
This is part of the code I use in VBScript:
//Grab the values from the form controls
occtime = occDate.Value //from form controls
testTime = testDate.Value //from form controls
//work out the time difference in minutes. Should always a negative number.
timeCalc = DateDiff("n", testTime, occtime)
Using either a time variable, HH:MM or using a full date dd/mm/yyyy HH:MM will give me the result I need, however I'm struggling getting this to work in either iOS(Swift) or Android Studio.
Thanks in Advance for any help.
For iOS please try this Swift code in the XCode playgorund:
import Foundation
let curerntDate = NSDate()
let futureDate = NSDate(timeInterval: 36*60*60, sinceDate: curerntDate)
let currentCalendar = NSCalendar.currentCalendar()
let differenceDateComponents = currentCalendar.components([.Era, .Year, .Month, .Day, .Hour, .Minute, .Second], fromDate: curerntDate, toDate: futureDate, options: NSCalendarOptions(rawValue: 0))
print(differenceDateComponents)
Swift 2.2 compatible extension:
extension NSDate {
func yearsFrom(date:NSDate) -> Int{
return NSCalendar.currentCalendar().components(.Year, fromDate: date, toDate: self, options: []).year
}
func monthsFrom(date:NSDate) -> Int{
return NSCalendar.currentCalendar().components(.Month, fromDate: date, toDate: self, options: []).month
}
func weeksFrom(date:NSDate) -> Int{
return NSCalendar.currentCalendar().components(.WeekOfYear, fromDate: date, toDate: self, options: []).weekOfYear
}
func daysFrom(date:NSDate) -> Int{
return NSCalendar.currentCalendar().components(.Day, fromDate: date, toDate: self, options: []).day
}
func hoursFrom(date:NSDate) -> Int{
return NSCalendar.currentCalendar().components(.Hour, fromDate: date, toDate: self, options: []).hour
}
func minutesFrom(date:NSDate) -> Int{
return NSCalendar.currentCalendar().components(.Minute, fromDate: date, toDate: self, options: []).minute
}
func secondsFrom(date:NSDate) -> Int{
return NSCalendar.currentCalendar().components(.Second, fromDate: date, toDate: self, options: []).second
}
func offsetFrom(date:NSDate) -> String {
if yearsFrom(date) > 0 { return "\(yearsFrom(date))y" }
if monthsFrom(date) > 0 { return "\(monthsFrom(date))M" }
if weeksFrom(date) > 0 { return "\(weeksFrom(date))w" }
if daysFrom(date) > 0 { return "\(daysFrom(date))d" }
if hoursFrom(date) > 0 { return "\(hoursFrom(date))h" }
if minutesFrom(date) > 0 { return "\(minutesFrom(date))m" }
if secondsFrom(date) > 0 { return "\(secondsFrom(date))s" }
return ""
}
}
For Android these methods working good:
public static int calcDaysDiff(Date day1, Date day2) {
Date d1 = new Date(day1.getTime());
Date d2 = new Date(day2.getTime());
Calendar date1 = Calendar.getInstance();
date1.setTime(d1);
Calendar dateCpy = (Calendar) date1.clone();
Calendar date2 = Calendar.getInstance();
date2.setTime(d2);
//checks if the start date is later then the end date - gives 0 if it is
if (date1.get(Calendar.YEAR) >= date2.get(Calendar.YEAR)) {
if (date1.get(Calendar.DAY_OF_YEAR) >= date2.get(Calendar.DAY_OF_YEAR)) {
return 0;
}
}
//checks if there is a daylight saving change between the two dates
boolean isDate1Summer = TimeZone.getDefault().inDaylightTime(d1);
boolean isDate2Summer = TimeZone.getDefault().inDaylightTime(d2);
int offset = 0;
//check if there as been a change in winter/summer time and adds/reduces an hour
if (isDate1Summer && !isDate2Summer) {
offset = 1;
}
if (!isDate1Summer && isDate2Summer) {
offset = -1;
}
return calcDaysDiffAux(dateCpy, date2) + checkFullDay(dateCpy, date2, offset);
}
// check if there is a 24 hour diff between the 2 dates including the daylight saving offset
public static int checkFullDay(Calendar day1, Calendar day2, int offset) {
if (day1.get(Calendar.HOUR_OF_DAY) <= day2.get(Calendar.HOUR_OF_DAY) + offset) {
return 0;
}
return -1;
}
// find the number of days between the 2 dates. check only the dates and not the hours
public static int calcDaysDiffAux(Calendar day1, Calendar day2) {
Calendar dayOne = (Calendar) day1.clone(),
dayTwo = (Calendar) day2.clone();
if (dayOne.get(Calendar.YEAR) == dayTwo.get(Calendar.YEAR)) {
return Math.abs(dayOne.get(Calendar.DAY_OF_YEAR) - dayTwo.get(Calendar.DAY_OF_YEAR));
} else {
if (dayTwo.get(Calendar.YEAR) > dayOne.get(Calendar.YEAR)) {
//swap them
Calendar temp = dayOne;
dayOne = dayTwo;
dayTwo = temp;
}
int extraDays = 0;
while (dayOne.get(Calendar.YEAR) > dayTwo.get(Calendar.YEAR)) {
dayOne.add(Calendar.YEAR, -1);
// getActualMaximum() important for leap years
extraDays += dayOne.getActualMaximum(Calendar.DAY_OF_YEAR);
}
return extraDays - dayTwo.get(Calendar.DAY_OF_YEAR) + dayOne.get(Calendar.DAY_OF_YEAR);
}
}