2

I have three variables :

val months: Long
val days: Long
val hours: Long

and I want to return something like this :

3 months, 2 days and 5 hours

Now this would simply translate to as :

val str = "$months months, $days days and $hours hours"

And if my months had to be 0 and days as 1 and hours as 0 then it will come like '0 months, 1 days and 0 hours'

But what I am looking for is "1 days" instead. How can I get it done?

I can definitely use some sort of conditional StringBuilder to get this done, but is there something better and elegant?

3
  • wondering for what you want to use this formatting... where does the amount of months actually come from? I wonder because a month can have different amount of days... so I think it is relative to some date? are you on the JVM? Commented Apr 25, 2022 at 15:49
  • @Roland - the difference is with current time. So there's a dateTime field for which I have to calculate the delta in a readable format (x months, y days and z hours ago) wrt current dateTime. Do you know of inbuilt feature that can do this? Commented Apr 26, 2022 at 3:29
  • Just thought, maybe there is something similar as getDisplayName(TextStyle, Locale) also for java.time.Duration or similar... but doesn't look like it... but maybe it's also interesting for you... Commented Apr 27, 2022 at 8:26

2 Answers 2

1

How does this look to you?

fun formatTime(months: Long, days: Long, hours: Long): String =
  listOf(
    months to "months",
    days to "days",
    hours to "hours"
  )
  .filter { (length,_) -> length > 0L }
  // You can add an optional map predicate to make singular and plurals
  .map { (amount, label) -> amount to if(abs(amount)==1L) label.replace("s", "") else label }
  .joinToString(separator=", ") { (length, label) -> "$length $label" }
  .addressLastItem()

fun String.addressLastItem() =
  if(this.count { it == ','} >= 1) 
    // Dirty hack to get it working quickly
    this.reversed().replaceFirst(" ,", " dna ").reversed() 
  else 
    this

You can see it working over here

Sign up to request clarification or add additional context in comments.

3 Comments

Looks really cool ;)
You wouldn't need the dirty hack if you included the serial comma. (I've never understood why everyone doesn't use it: it's more logical, less ambiguous, more consistent, and requires less thought.)
What do you mean "include the comma"? Inside the label? Then you have the edge case for single cases where you just have "1 month," and gotta clean up the comma anyway.
1

Another variant without replacing, counting or reversing the list:

fun formatTime(months: Long, days: Long, hours: Long): String {
  val list = listOfNotNull(
    months.formatOrNull("month", "months"),
    days.formatOrNull("day", "days"),
    hours.formatOrNull("hour", "hours"),
  )
  return if (list.isEmpty()) "all values <= 0"
  else
    listOfNotNull(
      list.take(list.lastIndex).joinToString().takeIf(String::isNotEmpty),
      list.lastOrNull()
    ).joinToString(" and ")
}

fun Long.formatOrNull(singular: String, plural: String = "${singular}s") = when {
  this == 1L -> "$this $singular"
  this > 1L -> "$this $plural"
  else -> null
}

It also has a fallback if all values are <= 0... you could also just use an empty string or whatever you prefer.

If you do not like that there are intermediate lists created to concatenate the string, you may also just use something as follows instead in the else path:

list.iterator().run {
  buildString {
    while (hasNext()) {
      val part = next()
      if (length > 0)
        if (hasNext())
          append(", ")
        else
          append(" and ")
      append(part)
    }
  }
}

1 Comment

Liked this approach and you cleverly handled singular and plural too.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.