Skip to content
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

Cannot format date when a time component value is zero and allowedComponents does not contains lower time components (ie. cannot print "today") #370

Closed
andriichernenko opened this issue Jan 6, 2017 · 2 comments
Assignees
Labels
Milestone

Comments

@andriichernenko
Copy link

Previous versions of SwiftDate allowed formatting date as "yesterday", "today", "tomorrow". However, at some point during the development of version 4.0 date formatting underwent significant changes and as a result it is no longer possible to format the date as "today" if it is in today. At least the localized string is missing (for example, in https://github.com/malcommac/SwiftDate/blob/master/Sources/SwiftDate/SwiftDate.bundle/en-US.lproj/SwiftDate.strings).

A possible solution would be to format the date as "just now", "today", "this week", "this month" etc. depending on the allowedComponents property of the formatter instead of just formatting it as "just now".

@andriichernenko
Copy link
Author

andriichernenko commented Jan 6, 2017

Something along these lines:

  /// Print a colloquial representation of the difference between two dates.
  /// For example "1 year ago", "just now", "3s" etc.
  ///
  /// - parameter fDate: date a
  /// - parameter tDate: date b
  ///
  /// - throws: throw `.DifferentCalendar` if dates are expressed in a different calendar, `.MissingRsrcBundle` if
  ///   required localized string are missing from the SwiftDate bundle
  ///
  /// - returns: a colloquial string representing the difference between two dates
  public func colloquial(from fDate: DateInRegion, to tDate: DateInRegion) throws -> (colloquial: String, time: String?) {
    guard fDate.region.calendar == tDate.region.calendar else {
      throw DateError.DifferentCalendar
    }
    
    let cal = fDate.region.calendar
    let cmp = cal.dateComponents(self.allowedComponents, from: fDate.absoluteDate, to: tDate.absoluteDate)
    
    let allComponents: [(Calendar.Component, Int?)] = [
      (.year, cmp.year), (.month, cmp.month), (.weekOfYear, cmp.weekOfYear), (.day, cmp.day),
      (.hour, cmp.hour), (.minute, cmp.minute), (.second, cmp.second)
    ]
    
    let allowedComponents = allComponents
      .flatMap({ (unit, value) in value != nil ? (unit, value!) : nil })
      .filter({ (unit, _) in self.allowedComponents.contains(unit) })

    if let leastGranularAllowedNonZeroComponent = allowedComponents.first(where: { _, value in value != 0 }) {
      let (unit, value) = (leastGranularAllowedNonZeroComponent.0, leastGranularAllowedNonZeroComponent.1)
      let position: PositionInTime = fDate > tDate ? .future : .past
      
      let colloquialDate = try self.localized(unit: unit, withValue: value, position: position, args: abs(value))
      let colloquialTime = try self.colloquial_time(forUnit: unit, withValue: value, date: fDate)
      
      return (colloquialDate, colloquialTime)
    } else if let leastGranularAllowedComponent = allowedComponents.first {
      let unit = leastGranularAllowedComponent.0
      
      let (position, value): (PositionInTime, Int)
      if fDate.isIn(date: tDate, granularity: unit) {
        (position, value) = (.now, 0)
      } else {
        (position, value) = fDate > tDate ? (.future, 1) : (.past, -1)
      }
      
      let colloquialDate = try self.localized(unit: unit, withValue: value, position: position, args: abs(value))
      let colloquialTime = try self.colloquial_time(forUnit: unit, withValue: value, date: fDate)
      
      return (colloquialDate, colloquialTime)
    } else {
      throw DateError.FailedToCalculate
    }
  }

  /// Return the colloquial representation of a value for a particular calendar component
  ///
  /// - parameter unit:     unit of calendar component
  /// - parameter value:    the value associated with the unit
  /// - parameter asFuture: `true` if  value referred to a future, `false` if it's a past event
  /// - parameter args:     variadic arguments to append
  ///
  /// - throws: throw `.MissingRsrcBundle` if required localized string are missing from the SwiftDate bundle
  ///
  /// - returns: localized colloquial string with passed unit of time
  private func localized(unit: Calendar.Component, withValue value: Int, position: PositionInTime, args: CVarArg...) throws -> String {
    guard let bundle = self.localizedResourceBundle() else {
      throw DateError.MissingRsrcBundle
    }
    
    let identifier: String
    if unit == .second || (unit == .minute && abs(value) < 5 && self.useImminentInterval) {
      identifier = "colloquial_now"
    } else {
      let positionKey = position.localizationKey
      let unitStr = self.localized(unit: unit, value: value, position: position)

      identifier = "colloquial_\(positionKey)_\(unitStr)"
    }
    
    let localized_date = withVaList(args) { (pointer: CVaListPointer) -> String in
      let localized = NSLocalizedString(identifier, tableName: "SwiftDate", bundle: bundle, value: "", comment: "")
      return NSString(format: localized, arguments: pointer) as String
    }
    
    return localized_date
  }

enum PositionInTime: Int {
  case past, now, future
  
  var localizationKey: String {
    switch self {
    case .past:   return "p"
    case .now:    return "n"
    case .future: return "f"
    }
  }
}

And, of course, localization files will have to be modified:

"colloquial_f_d"	=	"tomorrow";        // day,future,singular:    "tomorrow"
"colloquial_f_dd"	=	"in %d days";      // day,future,plural:      "in 3 days"
"colloquial_n_d"	=	"today";           // day,now:                "today"
"colloquial_p_d"	=	"yesterday";       // day,past,singular:      "yesterday"
"colloquial_p_dd"	=	"%d days ago";     // day,past,plural:        "3 days ago"

This feature has a lot of edge cases, and I have probably broke some of them. It would be nice to look at the requirements, are they specified somewhere?

@malcommac malcommac modified the milestones: 4.0.0, 4.0.11 Jan 8, 2017
@malcommac malcommac added the bug label Jan 8, 2017
@malcommac malcommac self-assigned this Jan 8, 2017
malcommac added a commit that referenced this issue Jan 8, 2017
Fix for #370: when `allowedComponents` does not contains lower time components than the current evaluated, and evaluated is zero, "this <component>" is printed (this allows you to print "today" or "this month" for example).
@malcommac
Copy link
Owner

Thank you for your report.
This happens when current evaluated time component's value is zero but allowedComponents does not contains lower time components than the current. In this case we should have a "this [component]" translation.
I've just commited it in d481882 and scheduled for 4.0.11.
I've also updated Localizable strings with these variants:

"colloquial_n_0y"					=	"this year";		// this year
"colloquial_n_0m"					=	"this month";		// this month
"colloquial_n_0w"					=	"this week";		// this week
"colloquial_n_0d"					=	"today";			// this day
"colloquial_n_0h"					=	"now";				// this hour
"colloquial_n_0M"					=	"now";				// this minute
"colloquial_n_0s"					=	"now";				// this second

@malcommac malcommac changed the title Cannot format date as "today" Cannot format date when a time component value is zero and allowedComponents does not contains lower time components (ie. cannot print "today") Jan 8, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants