2

I'm currently trying to build my first app in Swift, and am looking to find the total sum of part of an array relative to the current month. Here is my code:

struct Hour {
    var date: String?
    var time: String?

    init(date: String?, time: String?) {
        self.date = date
        self.time = time
    }
}


let hoursData = [
    Hour(date: "Nov 29, 2015", time: "7"),
    Hour(date: "Dec 12, 2015", time: "7"),
    Hour(date: "Dec 14, 2015", time: "7"),
    Hour(date: "Dec 25, 2015", time: "7") ]

I was wondering how I'd go about making a variable which contains the sum of the time data for the current month? Or any month for that matter? Any help you guys can give would be really appreciated.

Isaac Pevy
  • 23
  • 2
  • what is parameter time? a point in time? a duration? Why is it a String and not a number type? for date you should use NSDate or NSDateComponents objects. – vikingosegundo Dec 22 '15 at 23:06
  • Viking makes a good point, generally it is a Bad Idea (TM) to try and make your own date structure. Use NSDateComponents if you want to work with some unit of the date. – Kevin Dec 22 '15 at 23:12
  • @vikingosegundo the parameter time is an hour, so say 7 hours of time. It made sense in my head haha, sorry I didn't explain it very well. They're both strings because they're entered by a user and put into a table. As I say I'm new to this so this seemed to be the best way to do it. The date is entered using NSDate however. – Isaac Pevy Dec 22 '15 at 23:23
  • you should transform user's input into the best type to work with. if a string parameter will always contain numeric string, make it a numeric parameter! Same for dates. – vikingosegundo Dec 22 '15 at 23:30

3 Answers3

2

First I'd rewrite your struct a bit,
both the properties can be immutable.

Instead of a String for date, I use a NSDate, and a double for the duration

struct Hour {
    let date: NSDate
    let duration: Double

    init(date: NSDate, duration: Double) {
        self.date = date
        self.duration = duration
    }
}

I create the hours like

let hoursData = [
    Hour(date: { let c = NSDateComponents(); c.day = 29; c.month = 11; c.year = 2015;  return NSCalendar.currentCalendar().dateFromComponents(c)!}(), duration: 7),
    Hour(date: { let c = NSDateComponents(); c.day = 12; c.month = 12; c.year = 2015;  return NSCalendar.currentCalendar().dateFromComponents(c)!}(), duration: 7),
    Hour(date: { let c = NSDateComponents(); c.day = 14; c.month = 12; c.year = 2015;  return NSCalendar.currentCalendar().dateFromComponents(c)!}(), duration: 7),
    Hour(date: { let c = NSDateComponents(); c.day = 15; c.month = 12; c.year = 2015;  return NSCalendar.currentCalendar().dateFromComponents(c)!}(), duration: 7)]

if you wonder about this syntax: I use implicit unnamed closures to created the NSDate parameters

now I filter for month and year an use the reduce method to sum up the filtered objects

let today = NSDate()
let totalDuration = hoursData.filter{
    let objectsComps = NSCalendar.currentCalendar().components([.Month, .Year], fromDate: $0.date)
    let todaysComps  = NSCalendar.currentCalendar().components([.Month, .Year], fromDate: today)
    return objectsComps.month == todaysComps.month && objectsComps.year == todaysComps.year
}.reduce(0) { (duration, hour) -> Double in
    duration + hour.duration
}

Though it is a good answer, I want to point out that Rob's answer has a little flaw: The struct Hour hides the dependency it has that the date string need to be of a certain format. This violates the Dependency Inversion Principle, the D in the SOLID Principles. But this is easy to fix.

Just pass in the date formatter when creating the Hour instances.

struct Hour {
    let date: NSDate
    let duration: Double

    init(date: NSDate, duration: Double) {
        self.date = date
        self.duration = duration
    }

    init (dateFormatter:NSDateFormatter, dateString:String, duration:Double) {
        self.init(date: dateFormatter.dateFromString(dateString)!, duration:duration)
    }
}

let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "MMM d, y"

let hoursData = [
    Hour(dateFormatter: dateFormatter, dateString: "Nov 29, 2015", duration: 7),
    Hour(dateFormatter: dateFormatter, dateString: "Dec 12, 2015", duration: 7),
    Hour(dateFormatter: dateFormatter, dateString: "Dec 14, 2015", duration: 7),
    Hour(dateFormatter: dateFormatter, dateString: "Dec 25, 2015", duration: 7),
    Hour(dateFormatter: dateFormatter, dateString: "Dec 25, 2017", duration: 7)
]

Now who ever uses Hour can define the format as needed, might be helpful in localized apps.

The filtering and reducing stays the same.


But now we have a new issue: NSDateformatter's dateFromString() might return nil. currently we force unwrap it with !, but this might be bad in a production app.

We should allow proper error handling, by allowing the convenience init to throw errors

enum HourError: ErrorType {
    case InvalidDate
}

struct Hour {

    let date: NSDate
    let duration: Double

    init(date: NSDate, duration: Double) {
        self.date = date
        self.duration = duration
    }

    init (dateFormatter:NSDateFormatter, dateString:String, duration:Double) throws {
        let date = dateFormatter.dateFromString(dateString)
        guard date != nil else { throw HourError.InvalidDate}
        self.init(date: date!, duration:duration)
    }
}

if we use it like

do {
    let now = NSDate()
    let dateFormatter = NSDateFormatter()
    dateFormatter.dateFormat = "MMM d, y"

    let hoursData = [
        try Hour(dateFormatter: dateFormatter, dateString: "Nov 29, 2015", duration: 7),
        try Hour(dateFormatter: dateFormatter, dateString: "Dec 12, 2015", duration: 7),
        try Hour(dateFormatter: dateFormatter, dateString: "Dec 14, 2015", duration: 7),
        try Hour(dateFormatter: dateFormatter, dateString: "Dec 25, 2015", duration: 7),
/*⛈*/  try Hour(dateFormatter: dateFormatter, dateString: " 25, 2017", duration: 7)
    ]



    let totalDuration = hoursData.filter{
        let objectsComps = NSCalendar.currentCalendar().components([.Month, .Year], fromDate: $0.date)
        let todaysComps  = NSCalendar.currentCalendar().components([.Month, .Year], fromDate: now)
        return objectsComps.month == todaysComps.month && objectsComps.year == todaysComps.year
        }.reduce(0) {
            $0 + $1.duration
    }
    print(totalDuration)
} catch HourError.InvalidDate{
    print("one or more datestrings must be wrong")
}

the error will be caught.


The full code

vikingosegundo
  • 52,040
  • 14
  • 137
  • 178
0

You could also do it functionally

let sum = hoursData
    .filter { $0.date?.hasPrefix("Dec") ?? false } // Only get dates in the correct month
    .flatMap { $0.time }                           // Map to an array of non nil times
    .flatMap { Int($0) }                           // Convert strings to ints
    .reduce(0) { $0 + $1 }                         // Sum up the times

This can be a lot simpler if you store non optional Ints instead of optional Strings. Or you could just use NSDateComponents.

Craig Siemens
  • 12,942
  • 1
  • 34
  • 51
0

I might do something like:

// get prefix for current month

let formatter = NSDateFormatter()
formatter.dateFormat = "MMM"
let monthString = formatter.stringFromDate(NSDate())

// now add up the time values for that month

let results = hoursData.filter { $0.date?.hasPrefix(monthString) ?? false }
    .reduce(0) { $0 + (Int($1.time ?? "0") ?? 0) }

Or, if you wanted to add a check for year, too:

let formatter = NSDateFormatter()
formatter.dateFormat = "MMM"
let monthString = formatter.stringFromDate(NSDate())

formatter.dateFormat = "yyyy"
let yearString = formatter.stringFromDate(NSDate())

let results = hoursData.filter { $0.date?.hasPrefix(monthString) ?? false && $0.date?.hasSuffix(yearString) ?? false }
    .reduce(0) { $0 + (Int($1.time ?? "0") ?? 0) }

print(results)

Note, in both of the above, since you're using optionals, I'm using ?? to handle when the values are nil (as well as if there was a non-numeric string in time).


Personally, I'd suggest that Hour use NSDate and Float rather than String, not use optionals unless you really need them, and use let instead of var:

struct Hour {
    let date: NSDate
    let time: Float

    init(dateString: String, time: Float)  {
        let formatter = NSDateFormatter()
        formatter.dateFormat = "MMM, d, y"
        self.date = formatter.dateFromString(dateString)!
        self.time = time
    }
}

Then the code becomes:

let hoursData = [
    Hour(dateString: "Nov 29, 2015", time: 7),
    Hour(dateString: "Dec 12, 2015", time: 7),
    Hour(dateString: "Dec 14, 2015", time: 7),
    Hour(dateString: "Dec 25, 2015", time: 7),
    Hour(dateString: "Dec 25, 2017", time: 7)
]

let calendar = NSCalendar.currentCalendar()
let currentComponents = calendar.components([.Month, .Year], fromDate: NSDate())

let results = hoursData.filter {
    let components = calendar.components([.Month, .Year], fromDate: $0.date)
    return components.month == currentComponents.month && components.year == currentComponents.year
}.reduce(0) { return $0 + $1.time }

print(results)

There are further optimizations possible (e.g. not repeated instantiate NSDateFormatter), but it illustrates the idea that using NSDate objects lets you use calendrical calculations, rather than looking for substrings.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • This is really great, perfect for what I was looking for! I'm having some trouble though, I'm using the results as the data for a UILabel, however when I add data to the array (by way of an add hours screen) the label doesn't update. Can you point me in the right direction of how I'd do this? – Isaac Pevy Dec 23 '15 at 00:41
  • You'd do something like `self.label.text = "\(results)"`. You might want to do something like `self.label.text = "total = \(results)"` and see if `total =` portion of the string shows up too. That narrows it down to some problem with the label vs some problem calculating `results`. If you're doing this from a background thread (e.g. from within a `NSURLSession` task's completion handler), then remember to dispatch this to the main queue, e.g. `dispatch_async(dispatch_get_main_queue()) { self.label.text = "total = \(results)" }`. – Rob Dec 23 '15 at 00:47
  • @IsaacPevy - If you're still having problems updating label having successfully calculating the total (e.g. you've printed the total, and that's fine), then post question focusing on the label update process showing us not the above calculation stuff, but just the label update stuff. – Rob Dec 23 '15 at 00:48