├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── contents.xcworkspacedata
├── CHANGELOG.md
├── JSON_OUTPUT.md
├── LICENSE.txt
├── Package.resolved
├── Package.swift
├── README.md
├── install
├── now.xcodeproj
├── project.pbxproj
└── xcshareddata
│ └── xcschemes
│ └── now.xcscheme
└── now
├── Date+Offsets.swift
├── LocatedTime.swift
├── PlaceFinder.swift
├── Result+Utility.swift
├── RuntimeError.swift
└── main.swift
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ## V 1.2
4 |
5 | * Rolled back Swift tools from 5.2 to 5.1 to support Mojave
6 |
7 | ## V 1.1
8 |
9 | * Produce JSON output on demand.
10 |
11 | ## V 1.0
12 |
13 | * Look up time at remote location.
14 | * Look up specified local time at remote location.
15 | * Look up specified remote time at current location.
--------------------------------------------------------------------------------
/JSON_OUTPUT.md:
--------------------------------------------------------------------------------
1 | # JSON Output
2 |
3 | ## Version 1
4 |
5 | The `now` utility uses `CLGeocoder` to geocode place descriptions. Placemark information is limited to the scope and standards inherited from Core Location.
6 |
7 | These fields should not vary by the locale of utility's invocation:
8 |
9 | `version`: the JSON output format version, increased incrementally. The current version is 1.
10 | `time`: an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date.
11 |
12 | These fields *will* vary by the locale of invocation:
13 |
14 | `place`: a human-readable localized place name.
15 | `zone_name`: a human-readable localized time zone name.
16 | `time_zone`: a time zone designation. The time zone is not guaranteed to be uniform, for example either `MDT` or `GMT+6` may be produced for Mountain Daylight Time.
17 |
18 | ### Example
19 |
20 | ```
21 | % now -j uk
22 | {
23 | "place" : "United Kingdom",
24 | "zone_name" : "United Kingdom Time",
25 | "time_zone" : "GMT+1",
26 | "time" : "2020-06-04T20:53:58Z",
27 | "version" : "1"
28 | }
29 | ```
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Erica Sadun
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "swift-argument-parser",
6 | "repositoryURL": "https://github.com/apple/swift-argument-parser",
7 | "state": {
8 | "branch": null,
9 | "revision": "223d62adc52d51669ae2ee19bdb8b7d9fd6fcd9c",
10 | "version": "0.0.6"
11 | }
12 | }
13 | ]
14 | },
15 | "version": 1
16 | }
17 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.1
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "now",
8 | platforms: [
9 | .macOS(.v10_12)
10 | ],
11 | products: [
12 | .executable(
13 | name: "now",
14 | targets: ["now"]),
15 | ],
16 | dependencies: [
17 | .package(
18 | url:"https://github.com/apple/swift-argument-parser",
19 | .exact("0.0.6")),
20 | ],
21 | targets: [
22 | .target(
23 | name: "now",
24 | dependencies: [.product(name: "ArgumentParser", package: "swift-argument-parser")],
25 | path: "now/"
26 | ),
27 | ],
28 | swiftLanguageVersions: [
29 | .v5
30 | ]
31 | )
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Now
2 |
3 | I wrote this first for sample code, then for myself, then for friends.
4 |
5 | I didn't intend to push this out but darn if it isn't useful.
6 |
7 | ## Usage
8 |
9 | ```
10 | OVERVIEW:
11 |
12 | Check the time at a given location, "now Sao Paolo Brazil". Locations
13 | are diacritical and case insensitive. Use postcodes, cities, states,
14 | countries, even place names like "now Lincoln Memorial"
15 |
16 | When it's this time here: "now --local 5PM Bath UK"
17 | When it's that time there: "now --remote 5PM Bath UK"
18 |
19 | Valid time styles: 5PM, 5:30PM, 17:30, 1730. (No spaces.)
20 |
21 | USAGE: now [--local ] [--remote ] [ ...]
22 |
23 | ARGUMENTS:
24 |
25 |
26 | OPTIONS:
27 | -@, -l, --at, --here, --local
28 | When it's this local time
29 | -r, --when, --there, --remote
30 | When it's this remote time
31 | -h, --help Show help information.
32 |
33 | % now sao paolo brazil
34 | São Paulo 3:50:58 PM (GMT-3 Brasilia Standard Time)
35 | % now --local 4PM sao paolo brazil
36 | São Paulo 9:00:00 PM (GMT-3 Brasilia Standard Time)
37 | % now --remote 4PM sao paolo brazil
38 | Local 1:00:00 PM (GMT-3 Brasilia Standard Time)
39 | ```
40 |
41 | *Note: Bug filed because help is showing `when` and not `remote` as the value for the remote time.*
42 |
43 | ## Known issues
44 |
45 | * This can break at the edges of daylight time changes.
46 | * Casting times (local and remote) will break when VPNs change your "location"
47 |
48 | ## Installation
49 |
50 | * Install [homebrew](https://brew.sh).
51 | * Install [mint](https://github.com/yonaskolb/Mint) with homebrew (`brew install mint`).
52 | * From command line: `mint install erica/now`
53 |
54 | ## Dependencies
55 |
56 | * [Swift-Argument-Parser](https://github.com/apple/Swift-Argument-Parser)
57 |
58 | ## Building
59 |
60 | * Build from Xcode (there's a custom build phase that installs to /usr/local/bin, so make sure you have write access)
61 | * Build from SPM: `swift build` in the top level directory. The built utility can be found in `.build/debug/now`. Run with `swift run`
62 |
63 | ## Thanks
64 |
65 | *I just started this section so if you pitched in and I forgot to mention you, please let me know so I can update this!*
66 |
67 | Darren Ford (code review), Ryan Booker ([code review and improvements](https://github.com/ryanbooker/now/blob/master/Sources/now/main.swift), and not least Paul Hudson (for living in the wrong time zone)
68 |
69 | ## Help Request
70 |
71 | I want to add tests that will work regardless of where the utility is built and tested. (I do all tests outside of Xcode right now.)
72 |
73 | If you have suggestions or pointers, please let me know. Thanks!
74 |
--------------------------------------------------------------------------------
/install:
--------------------------------------------------------------------------------
1 | #! /bin/csh
2 | cp .build/debug/now /usr/local/bin/now
3 |
--------------------------------------------------------------------------------
/now.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 52;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 8E0CC35324734428008B5432 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E0CC35224734428008B5432 /* main.swift */; };
11 | 8E0CC3592473443C008B5432 /* now in CopyFiles */ = {isa = PBXBuildFile; fileRef = 8E0CC34F24734428008B5432 /* now */; };
12 | 8E0CC35E2473460C008B5432 /* RuntimeError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E0CC35D2473460C008B5432 /* RuntimeError.swift */; };
13 | 8E0CC36024734614008B5432 /* Date+Offsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E0CC35F24734614008B5432 /* Date+Offsets.swift */; };
14 | 8E0CC36224734697008B5432 /* PlaceFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E0CC36124734697008B5432 /* PlaceFinder.swift */; };
15 | 8E10B4A92478326700597641 /* Result+Utility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E10B4A82478326700597641 /* Result+Utility.swift */; };
16 | 8E3D88672479A4B700DB2C06 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = 8E3D88662479A4B700DB2C06 /* ArgumentParser */; };
17 | 8E8795962486A8DC0029766B /* LocatedTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E8795952486A8DC0029766B /* LocatedTime.swift */; };
18 | /* End PBXBuildFile section */
19 |
20 | /* Begin PBXCopyFilesBuildPhase section */
21 | 8E0CC34D24734428008B5432 /* CopyFiles */ = {
22 | isa = PBXCopyFilesBuildPhase;
23 | buildActionMask = 12;
24 | dstPath = /usr/local/bin;
25 | dstSubfolderSpec = 0;
26 | files = (
27 | 8E0CC3592473443C008B5432 /* now in CopyFiles */,
28 | );
29 | runOnlyForDeploymentPostprocessing = 0;
30 | };
31 | /* End PBXCopyFilesBuildPhase section */
32 |
33 | /* Begin PBXFileReference section */
34 | 8E0CC34F24734428008B5432 /* now */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = now; sourceTree = BUILT_PRODUCTS_DIR; };
35 | 8E0CC35224734428008B5432 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; };
36 | 8E0CC35D2473460C008B5432 /* RuntimeError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RuntimeError.swift; sourceTree = ""; };
37 | 8E0CC35F24734614008B5432 /* Date+Offsets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Date+Offsets.swift"; sourceTree = ""; };
38 | 8E0CC36124734697008B5432 /* PlaceFinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceFinder.swift; sourceTree = ""; };
39 | 8E10B4A82478326700597641 /* Result+Utility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+Utility.swift"; sourceTree = ""; };
40 | 8E8795952486A8DC0029766B /* LocatedTime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocatedTime.swift; sourceTree = ""; };
41 | /* End PBXFileReference section */
42 |
43 | /* Begin PBXFrameworksBuildPhase section */
44 | 8E0CC34C24734428008B5432 /* Frameworks */ = {
45 | isa = PBXFrameworksBuildPhase;
46 | buildActionMask = 2147483647;
47 | files = (
48 | 8E3D88672479A4B700DB2C06 /* ArgumentParser in Frameworks */,
49 | );
50 | runOnlyForDeploymentPostprocessing = 0;
51 | };
52 | /* End PBXFrameworksBuildPhase section */
53 |
54 | /* Begin PBXGroup section */
55 | 8E0CC34624734428008B5432 = {
56 | isa = PBXGroup;
57 | children = (
58 | 8E0CC35124734428008B5432 /* now */,
59 | 8E0CC35024734428008B5432 /* Products */,
60 | );
61 | sourceTree = "";
62 | };
63 | 8E0CC35024734428008B5432 /* Products */ = {
64 | isa = PBXGroup;
65 | children = (
66 | 8E0CC34F24734428008B5432 /* now */,
67 | );
68 | name = Products;
69 | sourceTree = "";
70 | };
71 | 8E0CC35124734428008B5432 /* now */ = {
72 | isa = PBXGroup;
73 | children = (
74 | 8E0CC35224734428008B5432 /* main.swift */,
75 | 8E0CC35F24734614008B5432 /* Date+Offsets.swift */,
76 | 8E8795952486A8DC0029766B /* LocatedTime.swift */,
77 | 8E0CC36124734697008B5432 /* PlaceFinder.swift */,
78 | 8E0CC35D2473460C008B5432 /* RuntimeError.swift */,
79 | 8E10B4A82478326700597641 /* Result+Utility.swift */,
80 | );
81 | path = now;
82 | sourceTree = "";
83 | };
84 | /* End PBXGroup section */
85 |
86 | /* Begin PBXNativeTarget section */
87 | 8E0CC34E24734428008B5432 /* now */ = {
88 | isa = PBXNativeTarget;
89 | buildConfigurationList = 8E0CC35624734428008B5432 /* Build configuration list for PBXNativeTarget "now" */;
90 | buildPhases = (
91 | 8E0CC34B24734428008B5432 /* Sources */,
92 | 8E0CC34C24734428008B5432 /* Frameworks */,
93 | 8E0CC34D24734428008B5432 /* CopyFiles */,
94 | );
95 | buildRules = (
96 | );
97 | dependencies = (
98 | );
99 | name = now;
100 | packageProductDependencies = (
101 | 8E3D88662479A4B700DB2C06 /* ArgumentParser */,
102 | );
103 | productName = now;
104 | productReference = 8E0CC34F24734428008B5432 /* now */;
105 | productType = "com.apple.product-type.tool";
106 | };
107 | /* End PBXNativeTarget section */
108 |
109 | /* Begin PBXProject section */
110 | 8E0CC34724734428008B5432 /* Project object */ = {
111 | isa = PBXProject;
112 | attributes = {
113 | LastSwiftUpdateCheck = 1150;
114 | LastUpgradeCheck = 1310;
115 | ORGANIZATIONNAME = "Erica Sadun";
116 | TargetAttributes = {
117 | 8E0CC34E24734428008B5432 = {
118 | CreatedOnToolsVersion = 11.4.1;
119 | };
120 | };
121 | };
122 | buildConfigurationList = 8E0CC34A24734428008B5432 /* Build configuration list for PBXProject "now" */;
123 | compatibilityVersion = "Xcode 9.3";
124 | developmentRegion = en;
125 | hasScannedForEncodings = 0;
126 | knownRegions = (
127 | en,
128 | Base,
129 | );
130 | mainGroup = 8E0CC34624734428008B5432;
131 | packageReferences = (
132 | 8E3D88652479A4B700DB2C06 /* XCRemoteSwiftPackageReference "swift-argument-parser" */,
133 | );
134 | productRefGroup = 8E0CC35024734428008B5432 /* Products */;
135 | projectDirPath = "";
136 | projectRoot = "";
137 | targets = (
138 | 8E0CC34E24734428008B5432 /* now */,
139 | );
140 | };
141 | /* End PBXProject section */
142 |
143 | /* Begin PBXSourcesBuildPhase section */
144 | 8E0CC34B24734428008B5432 /* Sources */ = {
145 | isa = PBXSourcesBuildPhase;
146 | buildActionMask = 2147483647;
147 | files = (
148 | 8E0CC35324734428008B5432 /* main.swift in Sources */,
149 | 8E8795962486A8DC0029766B /* LocatedTime.swift in Sources */,
150 | 8E0CC36024734614008B5432 /* Date+Offsets.swift in Sources */,
151 | 8E10B4A92478326700597641 /* Result+Utility.swift in Sources */,
152 | 8E0CC36224734697008B5432 /* PlaceFinder.swift in Sources */,
153 | 8E0CC35E2473460C008B5432 /* RuntimeError.swift in Sources */,
154 | );
155 | runOnlyForDeploymentPostprocessing = 0;
156 | };
157 | /* End PBXSourcesBuildPhase section */
158 |
159 | /* Begin XCBuildConfiguration section */
160 | 8E0CC35424734428008B5432 /* Debug */ = {
161 | isa = XCBuildConfiguration;
162 | buildSettings = {
163 | ALWAYS_SEARCH_USER_PATHS = NO;
164 | CLANG_ANALYZER_NONNULL = YES;
165 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
166 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
167 | CLANG_CXX_LIBRARY = "libc++";
168 | CLANG_ENABLE_MODULES = YES;
169 | CLANG_ENABLE_OBJC_ARC = YES;
170 | CLANG_ENABLE_OBJC_WEAK = YES;
171 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
172 | CLANG_WARN_BOOL_CONVERSION = YES;
173 | CLANG_WARN_COMMA = YES;
174 | CLANG_WARN_CONSTANT_CONVERSION = YES;
175 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
176 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
177 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
178 | CLANG_WARN_EMPTY_BODY = YES;
179 | CLANG_WARN_ENUM_CONVERSION = YES;
180 | CLANG_WARN_INFINITE_RECURSION = YES;
181 | CLANG_WARN_INT_CONVERSION = YES;
182 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
183 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
184 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
185 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
186 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
187 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
188 | CLANG_WARN_STRICT_PROTOTYPES = YES;
189 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
190 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
191 | CLANG_WARN_UNREACHABLE_CODE = YES;
192 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
193 | COPY_PHASE_STRIP = NO;
194 | DEBUG_INFORMATION_FORMAT = dwarf;
195 | ENABLE_STRICT_OBJC_MSGSEND = YES;
196 | ENABLE_TESTABILITY = YES;
197 | GCC_C_LANGUAGE_STANDARD = gnu11;
198 | GCC_DYNAMIC_NO_PIC = NO;
199 | GCC_NO_COMMON_BLOCKS = YES;
200 | GCC_OPTIMIZATION_LEVEL = 0;
201 | GCC_PREPROCESSOR_DEFINITIONS = (
202 | "DEBUG=1",
203 | "$(inherited)",
204 | );
205 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
206 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
207 | GCC_WARN_UNDECLARED_SELECTOR = YES;
208 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
209 | GCC_WARN_UNUSED_FUNCTION = YES;
210 | GCC_WARN_UNUSED_VARIABLE = YES;
211 | MACOSX_DEPLOYMENT_TARGET = 10.15;
212 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
213 | MTL_FAST_MATH = YES;
214 | ONLY_ACTIVE_ARCH = YES;
215 | SDKROOT = macosx;
216 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
217 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
218 | };
219 | name = Debug;
220 | };
221 | 8E0CC35524734428008B5432 /* Release */ = {
222 | isa = XCBuildConfiguration;
223 | buildSettings = {
224 | ALWAYS_SEARCH_USER_PATHS = NO;
225 | CLANG_ANALYZER_NONNULL = YES;
226 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
227 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
228 | CLANG_CXX_LIBRARY = "libc++";
229 | CLANG_ENABLE_MODULES = YES;
230 | CLANG_ENABLE_OBJC_ARC = YES;
231 | CLANG_ENABLE_OBJC_WEAK = YES;
232 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
233 | CLANG_WARN_BOOL_CONVERSION = YES;
234 | CLANG_WARN_COMMA = YES;
235 | CLANG_WARN_CONSTANT_CONVERSION = YES;
236 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
237 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
238 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
239 | CLANG_WARN_EMPTY_BODY = YES;
240 | CLANG_WARN_ENUM_CONVERSION = YES;
241 | CLANG_WARN_INFINITE_RECURSION = YES;
242 | CLANG_WARN_INT_CONVERSION = YES;
243 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
244 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
245 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
246 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
247 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
248 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
249 | CLANG_WARN_STRICT_PROTOTYPES = YES;
250 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
251 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
252 | CLANG_WARN_UNREACHABLE_CODE = YES;
253 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
254 | COPY_PHASE_STRIP = NO;
255 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
256 | ENABLE_NS_ASSERTIONS = NO;
257 | ENABLE_STRICT_OBJC_MSGSEND = YES;
258 | GCC_C_LANGUAGE_STANDARD = gnu11;
259 | GCC_NO_COMMON_BLOCKS = YES;
260 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
261 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
262 | GCC_WARN_UNDECLARED_SELECTOR = YES;
263 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
264 | GCC_WARN_UNUSED_FUNCTION = YES;
265 | GCC_WARN_UNUSED_VARIABLE = YES;
266 | MACOSX_DEPLOYMENT_TARGET = 10.15;
267 | MTL_ENABLE_DEBUG_INFO = NO;
268 | MTL_FAST_MATH = YES;
269 | SDKROOT = macosx;
270 | SWIFT_COMPILATION_MODE = wholemodule;
271 | SWIFT_OPTIMIZATION_LEVEL = "-O";
272 | };
273 | name = Release;
274 | };
275 | 8E0CC35724734428008B5432 /* Debug */ = {
276 | isa = XCBuildConfiguration;
277 | buildSettings = {
278 | CODE_SIGN_IDENTITY = "-";
279 | CODE_SIGN_STYLE = Automatic;
280 | DEVELOPMENT_TEAM = 2W4DVPEQ39;
281 | ENABLE_HARDENED_RUNTIME = YES;
282 | PRODUCT_NAME = "$(TARGET_NAME)";
283 | SWIFT_VERSION = 5.0;
284 | };
285 | name = Debug;
286 | };
287 | 8E0CC35824734428008B5432 /* Release */ = {
288 | isa = XCBuildConfiguration;
289 | buildSettings = {
290 | CODE_SIGN_IDENTITY = "-";
291 | CODE_SIGN_STYLE = Automatic;
292 | DEVELOPMENT_TEAM = 2W4DVPEQ39;
293 | ENABLE_HARDENED_RUNTIME = YES;
294 | PRODUCT_NAME = "$(TARGET_NAME)";
295 | SWIFT_VERSION = 5.0;
296 | };
297 | name = Release;
298 | };
299 | /* End XCBuildConfiguration section */
300 |
301 | /* Begin XCConfigurationList section */
302 | 8E0CC34A24734428008B5432 /* Build configuration list for PBXProject "now" */ = {
303 | isa = XCConfigurationList;
304 | buildConfigurations = (
305 | 8E0CC35424734428008B5432 /* Debug */,
306 | 8E0CC35524734428008B5432 /* Release */,
307 | );
308 | defaultConfigurationIsVisible = 0;
309 | defaultConfigurationName = Release;
310 | };
311 | 8E0CC35624734428008B5432 /* Build configuration list for PBXNativeTarget "now" */ = {
312 | isa = XCConfigurationList;
313 | buildConfigurations = (
314 | 8E0CC35724734428008B5432 /* Debug */,
315 | 8E0CC35824734428008B5432 /* Release */,
316 | );
317 | defaultConfigurationIsVisible = 0;
318 | defaultConfigurationName = Release;
319 | };
320 | /* End XCConfigurationList section */
321 |
322 | /* Begin XCRemoteSwiftPackageReference section */
323 | 8E3D88652479A4B700DB2C06 /* XCRemoteSwiftPackageReference "swift-argument-parser" */ = {
324 | isa = XCRemoteSwiftPackageReference;
325 | repositoryURL = "https://github.com/apple/swift-argument-parser";
326 | requirement = {
327 | kind = exactVersion;
328 | version = 0.0.6;
329 | };
330 | };
331 | /* End XCRemoteSwiftPackageReference section */
332 |
333 | /* Begin XCSwiftPackageProductDependency section */
334 | 8E3D88662479A4B700DB2C06 /* ArgumentParser */ = {
335 | isa = XCSwiftPackageProductDependency;
336 | package = 8E3D88652479A4B700DB2C06 /* XCRemoteSwiftPackageReference "swift-argument-parser" */;
337 | productName = ArgumentParser;
338 | };
339 | /* End XCSwiftPackageProductDependency section */
340 | };
341 | rootObject = 8E0CC34724734428008B5432 /* Project object */;
342 | }
343 |
--------------------------------------------------------------------------------
/now.xcodeproj/xcshareddata/xcschemes/now.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
60 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/now/Date+Offsets.swift:
--------------------------------------------------------------------------------
1 | // Copyright © 2020 Erica Sadun. All rights reserved.
2 |
3 | import Foundation
4 |
5 | extension Date {
6 |
7 | /// Returns a reminder date from a string formatted either hour-24:minute or hour:minute-meridian.
8 | ///
9 | /// This method constructs a date from an hour and minute representation.
10 | /// The date is calculated from "now", moving to midnight and adding the hour and minute components.
11 | /// If the new date is earlier than "now", it's pushed forward 24 hours, producing the first possible
12 | /// instance of that hour/minute time in the future.
13 | ///
14 | /// - Parameter string: a string formatted either as "h:ma" or "H:m"
15 | /// - Throws: `RuntimeError`s when unable to parse the input string.
16 | /// - Returns: A new date, initialized to the offset of the date either today or tomorrow.
17 | static func date(from string: String) throws -> Date {
18 | // Parse date in preferred order
19 | let dateFormatter = DateFormatter()
20 | var date: Date?
21 | for format in ["h:ma", "ha", "H:m", "HH", "Hm", "HHmm"] {
22 | dateFormatter.dateFormat = format
23 | if let parsed = dateFormatter.date(from: string) {
24 | date = parsed
25 | break
26 | }
27 | }
28 |
29 | // Ensure date was constructed
30 | guard
31 | let componentDate = date
32 | else { throw RuntimeError.timeParseFailure }
33 |
34 | // Construct YMD components from now
35 | let calendar = Calendar.autoupdatingCurrent
36 | let now = Date()
37 | let year = calendar.component(.year, from: now)
38 | let month = calendar.component(.month, from: now)
39 | let day = calendar.component(.day, from: now)
40 |
41 | // Construct HM components from constructed date
42 | let hour = calendar.component(.hour, from: componentDate)
43 | let minute = calendar.component(.minute, from: componentDate)
44 |
45 | // Combine
46 | guard let adjustedDate = calendar.date(from: DateComponents(year: year, month: month, day: day, hour: hour, minute: minute))
47 | else { throw RuntimeError.timeAdjustError }
48 | return adjustedDate
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/now/LocatedTime.swift:
--------------------------------------------------------------------------------
1 | // Copyright © 2020 Erica Sadun. All rights reserved.
2 |
3 | import Foundation
4 |
5 | struct LocatedTime: Codable {
6 | var version: String = "1"
7 | let place: String
8 | let time: String
9 | let timeZone: String
10 | let zoneName: String
11 | }
12 |
--------------------------------------------------------------------------------
/now/PlaceFinder.swift:
--------------------------------------------------------------------------------
1 | // Copyright © 2020 Erica Sadun. All rights reserved.
2 |
3 | import Foundation
4 | import CoreLocation
5 |
6 | typealias PlaceFindingResult = Result<[CLPlacemark], Error>
7 |
8 | protocol DateToStringFormatter {
9 | func string(from date: Date) -> String
10 | }
11 |
12 | extension DateFormatter: DateToStringFormatter {}
13 | extension ISO8601DateFormatter: DateToStringFormatter {}
14 |
15 | /// A utility type to locate a place marker and print out a time with respect to that point.
16 | enum PlaceFinder {
17 | /// Retrieve a placemark from a descriptive string.
18 | /// - Parameter hint: A free-form location indicator, such as a city name, zip code, place of interest.
19 | /// - Returns: A `PlaceFindingResult` of the geocoded place hint
20 | static func fetchPlaceMark(from hint: String) -> PlaceFindingResult {
21 | var result: Result<[CLPlacemark], Error> = Result(nil, RuntimeError.locationFetchFailure)
22 | CLGeocoder().geocodeAddressString(hint, in: nil) { placemarks, error in
23 | result = Result(placemarks, error)
24 | CFRunLoopStop(CFRunLoopGetCurrent())
25 | }
26 | CFRunLoopRun()
27 | return result
28 | }
29 |
30 | /// Display a user-localized time (medium style) for a timezone described by freeform text
31 | ///
32 | /// This uses the current time to fetch the time zone abbreviation so there will be errors at the very
33 | /// edges of daylight changes.
34 | ///
35 | /// - Parameters:
36 | /// - hint: A free-form location indicator, such as a city name, zip code, place of interest.
37 | /// - date: An absolute date that will be adjusted to a timezone, localized to the user, and printed.
38 | /// - localCast: Look up the remote time and cast it to the local zone
39 | /// - Throws: A `RuntimeError` if the target timezone cannot be interpreted.
40 | static func showTime(from hint: String, at timeSpecifier: String?, castingTimeToLocal localCast: Bool = false, outputJSON: Bool) throws {
41 |
42 | // Fetch placemark
43 | let placemarks = try fetchPlaceMark(from: hint).get()
44 | guard !placemarks.isEmpty
45 | else { throw RuntimeError.locationFetchFailure }
46 | let placemark = placemarks[0]
47 |
48 | // Extract place and zone information from placemark
49 | guard
50 | let place = placemark.name,
51 | let timeZone = placemark.timeZone,
52 | let timeZoneAbbr = placemark.timeZone?.abbreviation(for: Date()),
53 | let timeZoneName = placemark.timeZone?.localizedName(for: .generic, locale: .current)
54 | else { throw RuntimeError.timeConversionFailure }
55 |
56 | /// A pretty-printed JSON encoder
57 | let jsonEncoder = JSONEncoder()
58 | jsonEncoder.keyEncodingStrategy = .convertToSnakeCase
59 | jsonEncoder.outputFormatting = .prettyPrinted
60 |
61 | /// The local time zone
62 | let localTimeZone = Locale.autoupdatingCurrent.calendar.timeZone
63 |
64 | /// The default formatter
65 | let dateFormatter = DateFormatter()
66 | dateFormatter.dateStyle = .none
67 | dateFormatter.timeStyle = .medium
68 | dateFormatter.timeZone = localCast ? localTimeZone : timeZone
69 |
70 | /// Flexible formatter
71 | let formatter: DateToStringFormatter = outputJSON ? ISO8601DateFormatter() : dateFormatter
72 |
73 | func printOutput(time: String, zone: String, zoneName: String) throws {
74 | switch outputJSON {
75 | case true:
76 | let located = LocatedTime(place: localCast ? "Local" : place, time: time,
77 | timeZone: zone, zoneName: zoneName)
78 | let json = try jsonEncoder.encode(located)
79 | if let jsonString = String(data: json, encoding: .utf8) {
80 | print(jsonString)
81 | } else { throw RuntimeError.jsonError }
82 |
83 | case false:
84 | print(#"\#(localCast ? "Local" : "\(place)") \#(time) (\#(zone) \#(zoneName))"#)
85 | }
86 | }
87 |
88 | // Current time at remote location
89 | guard let timeSpecifier = timeSpecifier else {
90 | let time = formatter.string(from: Date())
91 | try printOutput(time: time, zone: timeZoneAbbr, zoneName: timeZoneName)
92 | return
93 | }
94 |
95 | // Specified time
96 | let date = try Date.date(from: timeSpecifier)
97 |
98 | // Specified time at remote location
99 | guard localCast == true else {
100 | let time = formatter.string(from: date)
101 | try printOutput(time: time, zone: timeZoneAbbr, zoneName: timeZoneName)
102 | return
103 | }
104 |
105 | // Specified remote time at local location
106 | guard
107 | let localAbbreviation = localTimeZone.abbreviation(),
108 | let localName = localTimeZone.localizedName(for: .generic, locale: Locale.autoupdatingCurrent)
109 | else { throw RuntimeError.timeConversionFailure }
110 | let localSeconds = localTimeZone.secondsFromGMT()
111 | let remoteSeconds = timeZone.secondsFromGMT()
112 | guard let newDate = Calendar.autoupdatingCurrent.date(byAdding: .second, value: localSeconds - remoteSeconds, to: date)
113 | else { throw RuntimeError.timeConversionFailure }
114 | let time = formatter.string(from: newDate)
115 | try printOutput(time: time, zone: localAbbreviation, zoneName: localName)
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/now/Result+Utility.swift:
--------------------------------------------------------------------------------
1 | // Copyright © 2020 Erica Sadun. All rights reserved.
2 |
3 | public extension Result {
4 | /// Initializes a `Result` from a completion handler's `(data?, error?)`.
5 | ///
6 | /// When both data and error are non-nil, `Result` first populates the
7 | /// `.failure` member over the `success`.
8 | ///
9 | /// - Parameters:
10 | /// - data: the optional data returned via a completion handler
11 | /// - error: the optional error returned via a completion handler
12 | init(_ data: Success?, _ error: Failure?) {
13 | precondition(!(data == nil && error == nil))
14 | switch (data, error) {
15 | case (_, let failure?): self = .failure(failure)
16 | case (let success?, _): self = .success(success)
17 | default:
18 | fatalError("Cannot initialize `Result` without success or failure")
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/now/RuntimeError.swift:
--------------------------------------------------------------------------------
1 | // Copyright © 2020 Erica Sadun. All rights reserved.
2 |
3 | import Foundation
4 |
5 | /// Errors encountered while running this command
6 | enum RuntimeError: String, Error, CustomStringConvertible {
7 | var description: String { rawValue }
8 |
9 | /// Mutual exclusion between `at` and `when`
10 | case localRemoteOverlap = "Cannot specify both remote and local times. Pick one."
11 |
12 | /// Time cannot be parsed
13 | case timeParseFailure = "Unable to parse hours and minutes from time string."
14 |
15 | /// Time components cannot be adjusted
16 | case timeAdjustError = "Failed to adjust time."
17 |
18 | /// Location cannot be fetched
19 | case locationFetchFailure = "Unable to fetch geocoded location information"
20 |
21 | /// Conversion fail
22 | case timeConversionFailure = "Time zone conversion failed."
23 |
24 | /// JSON fail
25 | case jsonError = "Unable to convert data to JSON"
26 | }
27 |
--------------------------------------------------------------------------------
/now/main.swift:
--------------------------------------------------------------------------------
1 | // Copyright © 2020 Erica Sadun. All rights reserved.
2 |
3 | import Foundation
4 | import ArgumentParser
5 |
6 | /// A command that checks the time at remote locations
7 | struct Now: ParsableCommand {
8 | static var configuration = CommandConfiguration(
9 | discussion: """
10 | Check the time at a given location, "now Sao Paolo Brazil". Locations
11 | are diacritical and case insensitive. Use postcodes, cities, states,
12 | countries, even place names like "now Lincoln Memorial"
13 |
14 | When it's this time here: "now --local 5PM Bath UK"
15 | When it's that time there: "now --remote 5PM Bath UK"
16 |
17 | Valid time styles: 5PM, 5:30PM, 17:30, 1730. (No spaces.)
18 | """,
19 | version: "1.1"
20 | )
21 |
22 | @Option(
23 | name: [.short, .customLong("local"), .customLong("here"), .customLong("at"), .customShort("@")],
24 | help: "When it's this local time.")
25 | var localTime: String?
26 |
27 | @Option(
28 | name: [.short, .customLong("remote"), .customLong("when"), .customLong("there")],
29 | help: "When it's this remote time.")
30 | var remoteTime: String?
31 |
32 | @Flag(
33 | name: .shortAndLong,
34 | help: "Output JSON results.")
35 | var json: Bool
36 |
37 | @Argument(parsing: .remaining)
38 | var locationInfo: [String]
39 |
40 | func run() throws {
41 | guard
42 | CommandLine.argc > 1
43 | else { throw CleanExit.helpRequest() }
44 |
45 | guard
46 | localTime == nil || remoteTime == nil
47 | else { throw RuntimeError.localRemoteOverlap }
48 |
49 | let hint = locationInfo.joined(separator: " ")
50 | try PlaceFinder.showTime(from: hint, at: localTime ?? remoteTime, castingTimeToLocal: remoteTime != nil, outputJSON: json)
51 | }
52 | }
53 |
54 | Now.main()
55 |
--------------------------------------------------------------------------------