3

I am making one app that can run AppleScript via NSAppleScript. Everything had been fine but I haven't been able to figure out how to pass date information from my app to AppleScript. (Since AppleScript has date type, I suppose this is possible) The way I pass parameters to AppleScript is through NSAppleEventDescriptor. I learned from Google that I could pass it as typeLongDateTime type:

- (id)initWithDate:(NSDate *)date {
     LongDateTime ldt;  
     UCConvertCFAbsoluteTimeToLongDateTime(CFDateGetAbsoluteTime((CFDateRef)date), &ldt);
     return [self initWithDescriptorType:typeLongDateTime
                                   bytes:&ldt
                                  length:sizeof(ldt)];
}

Unfortunately, the type LongDateTime had long gone, because I am using Swift and under OS X 10.10. Even the Core Services function UCConvertCFAbsoluteTimeToLongDateTime has already been removed from 10.10.3.

Now I am stuck.

Do you have any ideas that inspire?

Terry
  • 337
  • 2
  • 9

4 Answers4

2

Is seems that LongDateTime is a signed 64-bit integer which represents a date d as the number of seconds since January 1, 1904, GMT, adjusted by the time-zone offset for d in the local time zone (including daylight-savings time offset if DST is active at d).

The following code gives the same result as your Objective-C code for all dates that I tested (winter time and summer time).

class DateEventDescriptor : NSAppleEventDescriptor {

    convenience init?(date : NSDate) {
        let secondsSince2001 = Int64(date.timeIntervalSinceReferenceDate)
        var secondsSince1904 = secondsSince2001 + 3061152000
        secondsSince1904 += Int64(NSTimeZone.localTimeZone().secondsFromGMTForDate(date))
        self.init(descriptorType: DescType(typeLongDateTime),
            bytes: &secondsSince1904, length: sizeofValue(secondsSince1904))
    }
}

Update for Swift 3:

class DateEventDescriptor : NSAppleEventDescriptor {

    convenience init?(date: Date) {
        let secondsSince2001 = Int64(date.timeIntervalSinceReferenceDate)
        var secondsSince1904 = secondsSince2001 + 3061152000
        secondsSince1904 += Int64(NSTimeZone.local.secondsFromGMT(for: date))
        self.init(descriptorType: DescType(typeLongDateTime),
                  bytes: &secondsSince1904, length: MemoryLayout.size(ofValue: secondsSince1904))
    }
}

Update for macOS 10.11:

As of macOS 10.11 there is a

 NSAppleEventDescriptor(date: Date)

initializer, so that the above workaround is no longer necessary. (Thanks to @Wevah for this information.)

Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • Thanks, Martin. Your suggestion works. However, it seems to have one glitch: the seconds difference between 1904 and 2001 should be 3061180800 (rather than 3061152000), would you please confirm? – Terry Jun 02 '15 at 10:29
  • I am pretty sure that 3061152000 is correct. Try `let date = NSDate(timeIntervalSinceReferenceDate: -3061152000) ; println(date)` Output: `1904-01-01 00:00:00 +0000` . – Martin R Jun 02 '15 at 11:24
  • Martin, I think you are right on this. But the reason why I thought it was wrong was because I got incorrect date value in the AppleScript when pass longDateTime in the 3061152000 way. I suspect this may have something to do with the time zone. I haven't figured it out completely yet. – Terry Jun 02 '15 at 13:38
  • @Terry: Yes, probably a time zone issue. But I don't know how to help you with that one without any further information. Note that `println(date)` always prints the time in GMT. – Martin R Jun 02 '15 at 17:18
  • Martin, please give a look at my own answer. – Terry Jun 03 '15 at 05:21
  • @Terry: I have compared my code, your code and the result of the Objective-C code (using UCConvertCFAbsoluteTimeToLongDateTime) for various dates. It turned out that UCConvertCFAbsoluteTimeToLongDateTime indeed does a correction for the local time zone. But is seems that your result differs from the UCConvertCFAbsoluteTimeToLongDateTime result if daylight saving time is active for the converted date. I have updated my code, and it now seems to always produce the same result as UCConvertCFAbsoluteTimeToLongDateTime. – Martin R Jun 03 '15 at 07:26
  • 1
    Great, thanks. I will leave my answer for further reference.:) – Terry Jun 03 '15 at 08:11
  • As of 10.11, there's a new public API: `+[NSAppleEventDescriptor descriptorWithDate:]`, which I think should be callable from Swift. There's also a private method on `NSDate`, `_scriptingDateDescriptor`. (Adding this comment in case someone needs this info like I did!) – Wevah Jan 12 '18 at 07:16
  • @Wevah: You are right, thanks. I have added that information and also an updated Swift version for macOS <= 10.10. – Martin R Jan 12 '18 at 08:35
1

Inspired by Martin, I got to know that the LongDateTime type is just something that records time interval since the date 1904-01-01 midnight. And AppleScript utilizes it to represent dates. However, one weird thing in AppleScript is that there is no time zone concept for date type. So, simply passing the time interval since 1904-01-01 00:00:00 +0000, would only make the resulted date in AppleScript show the time in GMT. That was why I tried Martin's suggestion but got wrong time shown from the AppleScript. Since it is a data involving time difference, I got the following way works for me:

convenience init?(date: NSDate) {
    struct StaticWrapper {
        static var longDateTimeReferenceDate: NSDate!
    }
    if StaticWrapper.longDateTimeReferenceDate == nil {
        let formatter = NSDateFormatter()
        let c = NSCalendar(calendarIdentifier: NSCalendarIdentifierGregorian)
        formatter.calendar = c
        formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
        StaticWrapper.longDateTimeReferenceDate = formatter.dateFromString("1904-01-01 00:00:00")
    }

    var secondsSince1904 = Int64(date.timeIntervalSinceDate(StaticWrapper.longDateTimeReferenceDate))
    self.init(descriptorType: DescType(typeLongDateTime), bytes: &secondsSince1904, length: sizeofValue(secondsSince1904))
}

The time zone information is not given in the date formatter, which implicitly includes the current time zone. Therefore, the resulted time interval will make the AppleScript to show the time in local time zone. Which behaves like the AppleScript command "current date".

Terry
  • 337
  • 2
  • 9
1

There is a little-known CoreFoundation constant kCFAbsoluteTimeIntervalSince1904 representing the difference between 1904 and 2001. This NSDate extension converts NSDate to NSAppleEventDescriptor and vice versa

extension NSDate {

  func appleScriptDate() -> NSAppleEventDescriptor
  {
    var secondsSince1904 = Int64(self.timeIntervalSinceReferenceDate + kCFAbsoluteTimeIntervalSince1904)
    return NSAppleEventDescriptor(descriptorType: DescType(typeLongDateTime), bytes: &secondsSince1904, length: sizeofValue(secondsSince1904))!
  }

  convenience init(appleScriptDate : NSAppleEventDescriptor)
  {
    var secondsSince1904 : Int64 = 0
    let data = appleScriptDate.data
    data.getBytes(&secondsSince1904, length: data.length)
    self.init(timeIntervalSinceReferenceDate:NSTimeInterval(secondsSince1904) - kCFAbsoluteTimeIntervalSince1904)
  }

}

If you need to adjust the time zone information (converting to AppleScript date does not preserve the time zone) add NSTimeZone.systemTimeZone().secondsFromGMT in Swift or time to GMT in AppleScript

vadian
  • 274,689
  • 30
  • 353
  • 361
0

I updated vadian's extension for Swift 3:

extension NSDate {

    func appleScriptDate() -> NSAppleEventDescriptor
    {
        var secondsSince1904 = Int64(self.timeIntervalSinceReferenceDate + kCFAbsoluteTimeIntervalSince1904)
        return NSAppleEventDescriptor(descriptorType: DescType(typeLongDateTime), bytes: &secondsSince1904, length: MemoryLayout.size(ofValue: secondsSince1904))!
    }

    convenience init(appleScriptDate : NSAppleEventDescriptor)
    {
        var secondsSince1904 : Int64 = 0
        withUnsafeMutablePointer(to: &secondsSince1904) {
            _ = appleScriptDate.data.copyBytes(
                to: UnsafeMutableBufferPointer(start: $0, count: 4),
                from: 0..<4)
        }
        self.init(timeIntervalSinceReferenceDate:TimeInterval(secondsSince1904) - kCFAbsoluteTimeIntervalSince1904)
    }
}
Alsatian
  • 3,086
  • 4
  • 25
  • 40