Last active
February 3, 2025 11:13
-
-
Save RhetTbull/db70c113efd03029c6ff619f4699ce34 to your computer and use it in GitHub Desktop.
Get the named timezone for a given location in python using native macOS CoreLocation APIs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""Get named timezone for a location on macOS using CoreLocation | |
To use this script, you need to have pyobjc-core and pyobjc-framework-CoreLocation installed: | |
pip install pyobjc-core pyobjc-framework-CoreLocation | |
This script uses CoreLocation to get the timezone for a given latitude and longitude | |
as well as the timezone offset for a given date. | |
It effectively does the same thing as python packages like [tzfpy](https://pypi.org/project/tzfpy/) | |
or [timezonefinder](https://pypi.org/project/timezonefinder/) but uses macOS native APIs. | |
""" | |
# /// script | |
# dependencies = [ | |
# "pyobjc-core", | |
# "pyobjc-framework-CoreLocation" | |
# ] | |
# /// | |
import datetime | |
import objc | |
from CoreLocation import CLGeocoder, CLLocation | |
from Foundation import NSDate, NSRunLoop, NSTimeZone | |
WAIT_FOR_COMPLETION = 0.01 # wait time for async completion in seconds | |
COMPLETION_TIMEOUT = 5.0 # timeout for async completion in seconds | |
def timezone_for_location(latitude: float, longitude: float) -> NSTimeZone: | |
with objc.autorelease_pool(): | |
location = CLLocation.alloc().initWithLatitude_longitude_(latitude, longitude) | |
geocoder = CLGeocoder.alloc().init() | |
result = {"timezone": None, "error": None} | |
completed = False | |
def completion(placemarks, error): | |
nonlocal completed | |
if error: | |
result["error"] = error.localizedDescription() | |
else: | |
placemark = placemarks[0] if placemarks else None | |
if placemark and placemark.timeZone(): | |
result["timezone"] = placemark.timeZone() | |
else: | |
result["error"] = "Unable to determine timezone" | |
completed = True | |
geocoder.reverseGeocodeLocation_completionHandler_(location, completion) | |
# reverseGeocodeLocation_completionHandler_ is async so run the event loop until completion | |
# I usuall use threading.Event for this type of thing in pyobjc but the the thread blocked forever | |
waiting = 0 | |
while not completed: | |
NSRunLoop.currentRunLoop().runMode_beforeDate_( | |
"NSDefaultRunLoopMode", | |
NSDate.dateWithTimeIntervalSinceNow_(WAIT_FOR_COMPLETION), | |
) | |
waiting += WAIT_FOR_COMPLETION | |
if waiting >= COMPLETION_TIMEOUT: | |
raise TimeoutError( | |
f"Timeout waiting for completion of reverseGeocodeLocation_completionHandler_: {waiting} seconds" | |
) | |
if result["error"]: | |
raise Exception(f"Error: {result['error']}") | |
return result["timezone"] | |
if __name__ == "__main__": | |
import argparse | |
import time | |
def parse_date(date_str): | |
try: | |
return datetime.datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S") | |
except ValueError: | |
return datetime.datetime.strptime(date_str, "%Y-%m-%d") | |
parser = argparse.ArgumentParser(description="Get timezone for a location") | |
parser.add_argument("latitude", type=float, help="Latitude of location") | |
parser.add_argument("longitude", type=float, help="Longitude of location") | |
parser.add_argument( | |
"--date", | |
type=lambda d: parse_date(d), | |
default=datetime.datetime.now(), | |
help="Date/time for timezone offset in format 'YYYY-MM-DD HH:MM:SS' or 'YYYY-MM-DD'", | |
) | |
args = parser.parse_args() | |
try: | |
start_t = time.time_ns() | |
timezone = timezone_for_location(args.latitude, args.longitude) | |
offset = timezone.secondsFromGMTForDate_( | |
args.date | |
) # takes an NSDate but pybojc will convert | |
end_t = time.time_ns() | |
print( | |
f"Timezone: {timezone.name()}, offset: {offset}, took: {(end_t - start_t) / 1e6:.2f} ms" | |
) | |
except Exception as e: | |
print(f"Error: {e}") |
Run like this:
uv run "https://gist.githubusercontent.com/RhetTbull/db70c113efd03029c6ff619f4699ce34/raw/06f34e6d64f054faa88c1caea4a4fa3968e14848/tzname.py" 34 -118
Which ouputs:
Timezone: America/Los_Angeles, offset: -28800, took: 317.20 ms
Or
uv run "https://gist.githubusercontent.com/RhetTbull/db70c113efd03029c6ff619f4699ce34/raw/06f34e6d64f054faa88c1caea4a4fa3968e14848/tzname.py" 34 -118 --date "2024-06-01"
which outputs
Timezone: America/Los_Angeles, offset: -25200, took: 264.47 ms
Thanks to @simonw for the tip on including uv script
block.
Amazing. Offset accounts for DST based on sample date.
Tries to extract DST status via timezone.isDaylightSavingTime()
but it comes as False in both cases. Sorry don't master Apple Foundation framework.
$ python tzname.py 43 -9 --date "2024-06-01 10:00:00"
Timezone: Europe/Madrid, offset: 7200, took: 254.66 ms
$ python tzname.py 43 -9 --date "2024-01-01 10:00:00"
Timezone: Europe/Madrid, offset: 3600, took: 304.73 ms
Added:
# Get DST information
is_dst = timezone.isDaylightSavingTimeForDate_(args.date)
dst_offset = timezone.daylightSavingTimeOffsetForDate_(args.date)
dst_timezone_name = timezone.abbreviationForDate_(args.date)
print(
f"Timezone: {timezone.name()}, offset: {offset}, "
f"DST: {is_dst}, DST offset: {dst_offset}, "
f"DST timezone name: {dst_timezone_name}, "
f"took: {(end_t - start_t) / 1e6:.2f} ms"
)
Weird. timezone.isDaylightSavingTimeForDate_(args.date)
works correctly for me.
❯ python tz.py 34 -118 --date "2021-06-01"
Timezone: America/Los_Angeles, offset: -25200, DST: True, took: 281.25 ms
❯ python tz.py 34 -118 --date "2021-01-01"
Timezone: America/Los_Angeles, offset: -28800, DST: False, took: 292.69 ms
I added:
is_dst = timezone.isDaylightSavingTimeForDate_(args.date)
print(
f"Timezone: {timezone.name()}, offset: {offset}, DST: {is_dst}, took: {(end_t - start_t) / 1e6:.2f} ms"
)
Very nice, thank you
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
MIT License