Skip to content

Allow getting the week number of a LocalDate and a LocalDateTime #129

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
LouisCAD opened this issue Jun 24, 2021 · 25 comments
Open

Allow getting the week number of a LocalDate and a LocalDateTime #129

LouisCAD opened this issue Jun 24, 2021 · 25 comments
Labels
cookbook Useful snippets enhancement New feature or request waiting for clarification Additional information is needed from the user

Comments

@LouisCAD
Copy link

LouisCAD commented Jun 24, 2021

Hello,

I think it'd be great to have natively the ability to know which week number a particular date is from.
It's very easy to do with GregorianCalendar in the JVM or Android (calendar.get(Calendar.WEEK_OF_YEAR)), but I see no such facility for Kotlin common code yet.

Have a great day!

@dkhalanskyjb
Copy link
Collaborator

Hi @LouisCAD! Sure, we could add such functionality, but could you please describe why you need it?

@dkhalanskyjb dkhalanskyjb added enhancement New feature or request waiting for clarification Additional information is needed from the user labels Jun 29, 2021
@LouisCAD
Copy link
Author

LouisCAD commented Jul 2, 2021

Hello @dkhalanskyjb!
To be honest, I don't need it personally at the moment, but a colleague needed it in a NodeJS server, for some processing that's done a per week basis, and were curious as to how to do it in Kotlin.

I guess it'd also be useful for other use cases since a bunch of corporations use week numbers as a reference.

@hrach
Copy link

hrach commented Jul 2, 2021

https://en.wikipedia.org/wiki/Week#Week_numbering

In some countries, though, the numbering system is different from the ISO standard. At least six numberings are in use

The ISO is probably good compromise, yet the usage / usefulness is quite questionable then.

@LouisCAD
Copy link
Author

LouisCAD commented Jul 2, 2021

For reference, moment.js has both locale dependent and ISO week number: https://momentjs.com/docs/#/get-set/week/

@4shutosh
Copy link

4shutosh commented Jul 5, 2021

This feature would be a good addition.

I would be very happy to work on this feature request.
If anybody can help me to get started that would be highly appreciated.

@dkhalanskyjb
Copy link
Collaborator

for some processing that's done a per week basis

It's still not clear what exactly people do with the week numbers. Let's say they got the week number of a LocalDate, but what do they do with that number next?

I'm asking this because use cases dictate the way we implement this. For example, to tell which week it is, we need to know on which day a week starts (in some countries it's Monday, in some it's Sunday). Should we add the ability to configure this? It depends entirely on the use cases that we aim to support. If we add a configuration option "just in case" and then nobody passes anything other than MONDAY to it, we've made the lives of everyone just slightly more complicated.

Also, about a symmetrical operation: do people also need to be able to construct a LocalDate given a year, a week number, and a day-of-week? If so, also, why?

The bottom line: please feel free to share your use cases, we'll be glad to discuss them even if you feel they are pretty obscure.

@4shutosh
Copy link

4shutosh commented Jul 8, 2021

One use case that I can think of is check if two dates belong to same week, to show a header or something like "This week". Although I guess this can be achieved by floorDiv of dayOfYear by 7.

@dkhalanskyjb
Copy link
Collaborator

two dates belong to same week

This can vary from locale to locale, as in some countries, last Sunday is in the same week, and in some, it's not. "This week" would be difficult to implement reliably even if we did provide such a function.

I guess this can be achieved by floorDiv of dayOfYear by 7.

A year doesn't always start on Mondays, so I don't think this would work.

@4shutosh
Copy link

4shutosh commented Jul 8, 2021

It seems like the whole week number scenario will be implemented correctly only after taking the whole calendar into consideration. Although there are not many use cases I can think of. 🤔

@LouisCAD
Copy link
Author

LouisCAD commented Jul 8, 2021

I agree, week comparison is something I'd want to do, though there are other ways to do it, without the week number, but simply checking the day in the week, and counting how many days two dates are apart.

@carlosefonseca
Copy link

I'd also like to have weekOfYear. I'm making an app that shows a calendar and the week number is required. I understand that there are some options regarding what is a week and what's not… java.time.temporal has WeekFields that allow configuring that, but having the ISO version of it would already help.

@dkhalanskyjb
Copy link
Collaborator

@carlosefonseca Thanks for the use case! I don't completely get it though, could you clarify it?

Where would the week number be displayed? Next to the list of the days of the week? If so, then you need to get the week numbers in a batch, not for just a couple of dates, and wouldn't a function that returned the week number on the given date be cumbersome to use in such a case?

Isn't it a better solution for your case to just iterate once over the Mondays (or Sundays, it's up to you!) of the year, assigning to each of them their week number, counting up from 1?

@julioromano
Copy link

This is very much useful for any calendar-style apps.
Most of these apps implement some kind of "week view" where the user can page between each week.
To implement this we'd need to have an index for each page, the most useful kind of index would be the number of the week itself.
We could use the week number both it as a key for each page and also to construct each individual page (i.e. from the week number we could obtain the seven LocalDate the week is comprised of).

@dkhalanskyjb
Copy link
Collaborator

For now, the only clear choice is to implement the ISO-8601 numbering, which defines the weeks to start on Mondays, and, in case a week is shared between two years, to handle the split week by assigning it to the year that has the majority of days in it (at least 4): https://en.wikipedia.org/wiki/Week#Determining_Week_1
However, such behavior may be unexpected and wrong in some cases, and if so, we need to provide and prominently feature a more general function that allows parameterizing those things, instead of promoting the ISO-8601 variant, which may lead to subtle mistakes.

To implement this we'd need to have an index for each page, the most useful kind of index would be the number of the week itself.

  • Would your application allow something other than Monday (for example, Sunday) to be treated as the first day of the week?
  • What happens to the week that is split between two years (and has Jan 1st in it)—is it assigned a number in terms of the earlier year, or the later year, or does the answer vary?

We could use the week number both it as a key for each page and also to construct each individual page (i.e. from the week number we could obtain the seven LocalDate the week is comprised of).

Constructing a LocalDate from a week number is even more obscure. In your case, you could keep a reference point start that you consider the start of the first week (and this is something you can do however you choose, without relying on arbitrary choices on our part), and then obtain the LocalDate of the first day of the nth week by doing weekStart = start.plus(n, DateTimeUnit.WEEK). Then, to obtain the other days of the week, you could do something like weekStart.plus(2, DateTimeUnit.DAY) (this returns the third day of the week). I think this would be much clearer than relying on difficult-to-comprehend semantics of the week numbering.

@julioromano
Copy link

Would your application allow something other than Monday (for example, Sunday) to be treated as the first day of the week?

Yes (via a user controllable setting).

What happens to the week that is split between two years (and has Jan 1st in it)—is it assigned a number in terms of the earlier year, or the later year, or does the answer vary?

We follow ISO 8601.

Constructing a LocalDate from a week number is even more obscure. In your case, you could keep a reference point start that you consider the start of the first week (and this is something you can do however you choose, without relying on arbitrary choices on our part), and then obtain the LocalDate of the first day of the nth week by doing weekStart = start.plus(n, DateTimeUnit.WEEK). Then, to obtain the other days of the week, you could do something like weekStart.plus(2, DateTimeUnit.DAY) (this returns the third day of the week). I think this would be much clearer than relying on difficult-to-comprehend semantics of the week numbering.

I'll give it a try, thanks for the hint!

@dkhalanskyjb
Copy link
Collaborator

Let's go back to the use case of checking if two dates belong to the same week. How would one implement that, if we did provide the week numbering?

// wrong attempt 1
fun LocalDate.sameWeekAs(other: LocalDate) =
    weekNumber(weekStart = DayOfWeek.MONDAY) ==
    other.weekNumber(weekStart = DayOfWeek.MONDAY)

Obviously, this won't work if the dates are from different years. Jan 1st, 2007 is not the same week as Jan 1st, 2023.

// wrong attempt 2
fun LocalDate.sameWeekAs(other: LocalDate) =
    year == other.year &&
    weekNumber(weekStart = DayOfWeek.MONDAY) ==
    other.weekNumber(weekStart = DayOfWeek.MONDAY)

This is much less obviously wrong, but still wrong since some weeks are shared by two years.

// wrong attempt 3
fun LocalDate.sameWeekAs(other: LocalDate) =
    monthsUntil(other).absoluteValue < 12 &&
    weekNumber(weekStart = DayOfWeek.MONDAY) ==
    other.weekNumber(weekStart = DayOfWeek.MONDAY)

Now, this will almost always work, but not always! For example, Jan 1st, 2007, is week 1, but Dec 31st, 2007, is also week 1. The reason is that, by the ISO-8601 numbering, if a week is split between two years, it is assigned a number in terms of the year that has the bigger part of the week.

I don't see a simple way to check if two dates are in the same week using a week numbering. Here's a working approach though:

fun LocalDate.sameWeekAs(other: LocalDate, firstDayOfWeek: DayOfWeek): Boolean {
    fun LocalDate.firstDayInWeek(): LocalDate =
        minus((dayOfWeek.isoDayNumber - firstDayOfWeek.isoDayNumber).mod(7), DateTimeUnit.DAY)
    return firstDayInWeek() == other.firstDayInWeek()
}

Maybe this is the approach we should be expanding, especially if operations like "find next Monday" or "find last day before this one that was in January" have some other uses.

@dkhalanskyjb
Copy link
Collaborator

If there is enough demand specifically for implementing the ISO-8601 week numbering for interoperability with other systems, maybe a better choice would be to introduce a separate data structure, called something like LocalWeekBasedDate, which would use the ISO week-numbering years and would be defined by fields like weekYear, weekNumber, dayOfWeek.

This way, those that want to know the ISO-8601 week number for some data processing will be able to do so (and with the week-year to match it, without which the week number is difficult to interpret, as shown above), but accessing it will also be difficult enough that people won't be encouraged to try to use it in day-to-day scenarios, where this would lead to counterintuitive results.

@johannesrave
Copy link

johannesrave commented Jul 24, 2023

Leaving this here as a workaround for some specific use-cases:
I also just had to work with an ISO-week-number as input for a webapp that has some calendar-like views. I am doing this:

import kotlinx.datetime.LocalDate
import kotlinx.datetime.toKotlinLocalDate
import java.time.format.DateTimeFormatter
import java.time.LocalDate as jtLocalDate

fun getFirstDayOfWeek(year: Int, week: Int): LocalDate {
    val isoString = "$year-W${week.toString().padStart(2, '0')}-1"
    val date = jtLocalDate.parse(isoString, DateTimeFormatter.ISO_WEEK_DATE)
    return date.toKotlinLocalDate()
}

The LocalDate.parse() from kotlinx.datetime doesn't expose the formatter as a parameter, but I could just copy from the implementation and use DateTimeFormatter.ISO_WEEK_DATE which parses yyyy-Www-d ( eg. 2012-W48-6).

This is for a toy project, so performance is not a consideration for me, but it's probably not great.

@ferinagy
Copy link

I would have another use-case. We have releases every 4 weeks and the name of the release is constructed as <year>.<week>, eg. 24.02 for 2nd week of 2024. I would like to create a method that would take LocalDate that would return the name of the release that it belongs to.

Currently, we have a java solution using java.time.LocalDate: val currentCalendarWeek = date.get(IsoFields.WEEK_OF_WEEK_BASED_YEAR). I am now trying to migrate this functionality to Kotlin Multiplatform and here I am. 😄

@dkhalanskyjb
Copy link
Collaborator

What you can do in kotlinx-datetime for your use case instead is

val currentDate = LocalDate(2024, 2, 14)

val firstDayOfWeek = DayOfWeek.MONDAY // maybe SUNDAY, up to you

fun LocalDate.firstDayInWeek(): LocalDate =
    minus((dayOfWeek.isoDayNumber - firstDayOfWeek.isoDayNumber).mod(7), DateTimeUnit.DAY)

val startOfWeekWithJan1st = LocalDate(currentDate.year, 1, 1).firstDayInWeek()

val currentWeekNumber = startOfWeekWithJan1st.until(currentDate, DateTimeUnit.WEEK) + 1

Essentially, this code calculates how many weeks have passed since the week of Jan 1st.

@ferinagy
Copy link

@ferinagy, if you publish a release on the last week of December this year, your release will be called 2024.01

Yes, the 01 week number is as expected from the ISO week numbering. For year, on jvm there is IsoFields.WEEK_BASED_YEAR, but handling the edge case here feels simpler.

What you can do in kotlinx-datetime for your use case instead...

Yes, but it's not the ISO numbering. Maybe I should be clearer on that - we already use ISO week numbers and would like to convert it to kotlin multiplatform, not come up with a new numbering scheme.

But based on your suggestion, I tried to replicate the ISO week calculation:

private fun LocalDate.isoWeekNumber(): Int {
    if (firstWeekInYearStart(year + 1) < this) return 1

    val currentYearStart = firstWeekInYearStart(year)
    val start = if (this < currentYearStart) firstWeekInYearStart(year - 1) else currentYearStart

    val currentCalendarWeek = start.until(this, DateTimeUnit.WEEK) + 1

    return currentCalendarWeek
}

private fun firstWeekInYearStart(year: Int): LocalDate {
    val jan1st = LocalDate(year, 1, 1)
    val previousMonday = jan1st.minus(jan1st.dayOfWeek.ordinal, DateTimeUnit.DAY)

    return if (jan1st.dayOfWeek <= DayOfWeek.THURSDAY) previousMonday else previousMonday.plus(1, DateTimeUnit.WEEK)
}

Still, would be nice to have it provided by datetime library, rather than having to write it every time IMHO.

@dkhalanskyjb
Copy link
Collaborator

Oh, ok, thanks. It's simply surprising that anyone would consciously want to number their version with repeats like 2024-01, ..., 2024-52, 2024-01, 2025-02. Typically, the ISO week numbers should be used in the context of IsoFields.WEEK_BASED_YEAR. So, your use case of "normal year" + "ISO week number" is far from common.

@ferinagy
Copy link

So, your use case of "normal year" + "ISO week number" is far from common.

Ah, but that's not what we use. The release would be 25.01, I just focused on the week number as that's what the issue is about.

@thimmwork
Copy link

thimmwork commented Apr 7, 2025

@ferinagy, if you publish a release on the last week of December this year, your release will be called 2024.01

But based on your suggestion, I tried to replicate the ISO week calculation:

private fun LocalDate.isoWeekNumber(): Int {
if (firstWeekInYearStart(year + 1) < this) return 1

@ferinagy
You might want to change that to <= this to avoid a bug that returns week 53 for monday Dec 29th 2025 instead of week 1 (2026).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cookbook Useful snippets enhancement New feature or request waiting for clarification Additional information is needed from the user
Projects
None yet
Development

No branches or pull requests

10 participants