leapsecond.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. #!/usr/bin/env python
  2. """
  3. Usage: leapsecond.py [-v] { [-h] | [-f filename] | [-g filename]
  4. | [-H filename] | [-I isodate] | [-O unixdate]
  5. | [-i rfcdate] | [-o unixdate] | [-n MMMYYYY] }
  6. Options:
  7. -I take a date in ISO8601 format and convert to Unix-UTC time
  8. -O take a date in Unix-UTC time and convert to ISO8601.
  9. -i take a date in RFC822 format and convert to Unix-UTC time
  10. -o take a date in Unix-UTC time and convert to RFC822.
  11. -f fetch leap-second offset data and save to local cache file
  12. -H make leapsecond include
  13. -h print this help
  14. -v be verbose
  15. -g generate a plot of leap-second dates over time. The command you
  16. probably want is something like (depending on if your gnuplot install
  17. does or does not support X11.
  18. leapsecond.py -g leapseconds.cache | gnuplot --persist
  19. leapsecond.py -g leapseconds.cache | gnuplot -e 'set terminal svg' - \\
  20. | display
  21. -n compute Unix gmt time for an IERS leap-second event given as a
  22. three-letter English Gregorian month abbreviation followed by a
  23. 4-digit year.
  24. Public urls and local cache file used:
  25. http://hpiers.obspm.fr/iers/bul/bulc/bulletinc.dat
  26. http://hpiers.obspm.fr/iers/bul/bulc/UTC-TAI.history
  27. ftp://maia.usno.navy.mil/ser7/tai-utc.dat
  28. leapseconds.cache
  29. This file is Copyright (c) 2013 by the GPSD project
  30. SPDX-License-Identifier: BSD-2-clause
  31. """
  32. # This code runs compatibly under Python 2 and 3.x for x >= 2.
  33. # Preserve this property!
  34. from __future__ import absolute_import, print_function, division
  35. import calendar
  36. import math
  37. import os
  38. import random
  39. import re
  40. import signal
  41. import sys
  42. import time
  43. try:
  44. import urllib.request as urlrequest # Python 3
  45. except ImportError:
  46. import urllib as urlrequest # Python 2
  47. # Set a socket timeout for slow servers
  48. import socket
  49. socket.setdefaulttimeout(30)
  50. del socket
  51. # *** Duplicate some code from gps.misc to avoid a dependency ***
  52. # Determine a single class for testing "stringness"
  53. try:
  54. STR_CLASS = basestring # Base class for 'str' and 'unicode' in Python 2
  55. except NameError:
  56. STR_CLASS = str # In Python 3, 'str' is the base class
  57. # Polymorphic str/bytes handling
  58. BINARY_ENCODING = 'latin-1'
  59. if bytes is str: # In Python 2 these functions can be null transformations
  60. polystr = str
  61. else: # Otherwise we do something real
  62. def polystr(o):
  63. "Convert bytes or str to str with proper encoding."
  64. if isinstance(o, str):
  65. return o
  66. if isinstance(o, bytes):
  67. return str(o, encoding=BINARY_ENCODING)
  68. raise ValueError
  69. def isotime(s):
  70. """Convert timestamps in ISO8601 format to and from Unix time including
  71. optional fractional seconds.
  72. """
  73. if isinstance(s, int):
  74. return time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(s))
  75. if isinstance(s, float):
  76. date = int(s)
  77. msec = s - date
  78. date = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(s))
  79. return date + "." + repr(msec)[3:]
  80. if isinstance(s, STR_CLASS):
  81. if s[-1] == "Z":
  82. s = s[:-1]
  83. if "." in s:
  84. (date, msec) = s.split(".")
  85. else:
  86. date = s
  87. msec = "0"
  88. # Note: no leap-second correction!
  89. return calendar.timegm(time.strptime(date, "%Y-%m-%dT%H:%M:%S")) \
  90. + float("0." + msec)
  91. # else:
  92. raise TypeError
  93. # *** End of duplicated code ***
  94. verbose = 0
  95. __locations = [
  96. (
  97. # U.S. Navy's offset-history file
  98. "ftp://maia.usno.navy.mil/ser7/tai-utc.dat",
  99. r" TAI-UTC= +([0-9-]+)[^\n]*\n$",
  100. 1,
  101. 19, # Magic TAI-GPS offset -> (leapseconds 1980)
  102. "ftp://maia.usno.navy.mil/ser7/tai-utc.dat",
  103. ),
  104. (
  105. # International Earth Rotation Service Bulletin C
  106. "http://hpiers.obspm.fr/iers/bul/bulc/bulletinc.dat",
  107. r" UTC-TAI = ([0-9-]+)",
  108. -1,
  109. 19, # Magic TAI-GPS offset -> (leapseconds 1980)
  110. "http://hpiers.obspm.fr/iers/bul/bulc/UTC-TAI.history",
  111. ),
  112. ]
  113. GPS_EPOCH = 315964800 # 6 Jan 1980 00:00:00
  114. SECS_PER_WEEK = 60 * 60 * 24 * 7 # Seconds per GPS week
  115. ROLLOVER = 1024 # 10-bit week rollover
  116. def gps_week(t):
  117. return (t - GPS_EPOCH) // SECS_PER_WEEK % ROLLOVER
  118. def gps_rollovers(t):
  119. return (t - GPS_EPOCH) // SECS_PER_WEEK // ROLLOVER
  120. def retrieve():
  121. "Retrieve current leap-second from Web sources."
  122. random.shuffle(__locations) # To spread the load
  123. for (url, regexp, sign, offset, _) in __locations:
  124. try:
  125. if os.path.exists(url):
  126. ifp = open(url)
  127. else:
  128. ifp = urlrequest.urlopen(url)
  129. txt = polystr(ifp.read())
  130. ifp.close()
  131. if verbose:
  132. sys.stderr.write("%s\n" % txt)
  133. m = re.search(regexp, txt)
  134. if m:
  135. return int(m.group(1)) * sign - offset
  136. except IOError:
  137. if verbose:
  138. sys.stderr.write("IOError: %s\n" % url)
  139. return None
  140. def last_insertion_time():
  141. "Give last potential insertion time for a leap second."
  142. # We need the Unix times for midnights Jan 1 and Jul 1 this year.
  143. when = time.gmtime()
  144. (tm_year, tm_mon, tm_mday, tm_hour, tm_min,
  145. tm_sec, tm_wday, tm_yday, tm_isdst) = when
  146. tm_mday = 1
  147. tm_hour = tm_min = tm_sec = 0
  148. tm_mon = 1
  149. jan_t = (tm_year, tm_mon, tm_mday, tm_hour, tm_min,
  150. tm_sec, tm_wday, tm_yday, tm_isdst)
  151. jan = int(calendar.timegm(jan_t))
  152. tm_mon = 7
  153. jul_t = (tm_year, tm_mon, tm_mday, tm_hour, tm_min,
  154. tm_sec, tm_wday, tm_yday, tm_isdst)
  155. jul = int(calendar.timegm(jul_t))
  156. # We have the UTC times of the potential insertion points this year.
  157. now = time.time()
  158. if now > jul:
  159. return jul
  160. return jan
  161. def save_leapseconds(outfile):
  162. """Fetch the leap-second history data and make a leap-second list since
  163. Unix epoch GMT (1970-01-01T00:00:00).
  164. """
  165. random.shuffle(__locations) # To spread the load
  166. for (_, _, _, _, url) in __locations:
  167. skip = True
  168. try:
  169. fetchobj = urlrequest.urlopen(url)
  170. except IOError:
  171. sys.stderr.write("Fetch from %s failed.\n" % url)
  172. continue
  173. # This code assumes that after 1980, leap-second increments are
  174. # always integrally one second and every increment is listed here
  175. fp = open(outfile, "w")
  176. for line in fetchobj:
  177. line = polystr(line)
  178. if verbose:
  179. sys.stderr.write("%s\n" % line[:-1])
  180. if line.startswith(" 1980"):
  181. skip = False
  182. if skip:
  183. continue
  184. fields = line.strip().split()
  185. if len(fields) < 2:
  186. continue
  187. md = leapbound(fields[0], fields[1])
  188. if verbose:
  189. sys.stderr.write("# %s\n" % md)
  190. fp.write(repr(iso_to_unix(md)) + "\t# " + str(md) + "\n")
  191. fp.close()
  192. return
  193. sys.stderr.write("%s not updated.\n" % outfile)
  194. def fetch_leapsecs(filename):
  195. "Get a list of leap seconds from the local cache of the USNO history"
  196. leapsecs = []
  197. for line in open(str(filename)):
  198. leapsecs.append(float(line.strip().split()[0]))
  199. return leapsecs
  200. def make_leapsecond_include(infile):
  201. """Get the current leap second count and century from the local cache
  202. usable as C preprocessor #define
  203. """
  204. # Underscore prefixes avoids warning W0612 from pylint,
  205. # which doesn't count substitution through locals() as use.
  206. leapjumps = fetch_leapsecs(infile)
  207. now = int(time.time())
  208. _century = time.strftime("%Y", time.gmtime(now))[:2] + "00"
  209. _week = gps_week(now)
  210. _rollovers = gps_rollovers(now)
  211. _isodate = isotime(now - now % SECS_PER_WEEK)
  212. _leapsecs = 0
  213. for leapjump in leapjumps:
  214. if leapjump < time.time():
  215. _leapsecs += 1
  216. return """\
  217. /*
  218. * Constants used for GPS time detection and rollover correction.
  219. *
  220. * Correct for week beginning %(_isodate)s
  221. *
  222. * Autogenerated, do not edit. All changes will be undone.
  223. */
  224. #define BUILD_CENTURY\t%(_century)s
  225. #define BUILD_WEEK\t%(_week)d # Assumes 10-bit week counter
  226. #define BUILD_LEAPSECONDS\t%(_leapsecs)d
  227. #define BUILD_ROLLOVERS\t%(_rollovers)d # Assumes 10-bit week counter
  228. /* Autogenerated, do not edit. All changes will be undone. */
  229. """ % locals()
  230. def conditional_leapsecond_fetch(outfile, timeout):
  231. """Conditionally fetch leapsecond data,
  232. w. timeout in case of evil firewalls.
  233. """
  234. if not os.path.exists(outfile):
  235. stale = True
  236. else:
  237. # If there can't have been a leapsecond insertion since the
  238. # last time the cache was updated, we don't need to refresh.
  239. # This test cuts way down on the frequency with which we fetch.
  240. stale = last_insertion_time() > os.path.getmtime(outfile)
  241. if not stale:
  242. return True
  243. def handler(_signum, _frame):
  244. raise IOError
  245. try:
  246. signal.signal(signal.SIGALRM, handler)
  247. except ValueError:
  248. # Parallel builds trigger this - signal only works in main thread
  249. sys.stdout.write("Signal set failed; ")
  250. return False
  251. signal.alarm(timeout)
  252. sys.stdout.write("Attempting leap-second fetch...")
  253. try:
  254. save_leapseconds(outfile)
  255. sys.stdout.write("succeeded.\n")
  256. except IOError:
  257. sys.stdout.write("failed; ")
  258. return False
  259. signal.alarm(0)
  260. return True
  261. def leastsquares(tuples):
  262. "Generate coefficients for a least-squares fit to the specified data."
  263. sum_x = 0
  264. sum_y = 0
  265. sum_xx = 0
  266. sum_xy = 0
  267. for (x, y) in tuples:
  268. sum_x = sum_x + x
  269. sum_y = sum_y + y
  270. xx = math.pow(x, 2)
  271. sum_xx = sum_xx + xx
  272. xy = x * y
  273. sum_xy = sum_xy + xy
  274. n = len(tuples)
  275. c = (-sum_x * sum_xy + sum_xx * sum_y) / (n * sum_xx - sum_x * sum_x)
  276. b = (-sum_x * sum_y + n * sum_xy) / (n * sum_xx - sum_x * sum_x)
  277. # y = b * x + c
  278. maxerr = 0
  279. for (x, y) in tuples:
  280. err = y - (x * b + c)
  281. if err > maxerr:
  282. maxerr = err
  283. return (b, c, maxerr)
  284. def iso_to_unix(tv):
  285. "Local Unix time to iso date."
  286. return calendar.timegm(time.strptime(tv, "%Y-%m-%dT%H:%M:%S"))
  287. def graph_history(filename):
  288. "Generate a GNUPLOT plot of the leap-second history."
  289. raw = fetch_leapsecs(filename)
  290. (b, c, e) = leastsquares(list(zip(list(range(len(raw))), raw)))
  291. e /= (60 * 60 * 24 * 7)
  292. dates = [time.strftime("%Y-%m-%d", time.localtime(t)) for t in raw]
  293. # Adding 190 days to scale
  294. enddate = time.strftime("%Y-%m-%d", time.localtime(raw[-1] + 16416000))
  295. fmt = ''
  296. fmt += '# Least-squares approximation of Unix time from leapsecond is:\n'
  297. fmt += 'lsq(x) = %s * x + %s\n' % (b, c)
  298. fmt += '# Maximum residual error is %.2f weeks\n' % e
  299. fmt += 'set autoscale\n'
  300. fmt += 'set ylabel "GPS-UTC (s)"\n'
  301. fmt += 'set yrange [-1:%d]\n' % (len(dates))
  302. fmt += 'set xlabel "Leap second date"\n'
  303. fmt += 'set xtics rotate by 300\n'
  304. fmt += 'set timefmt "%Y-%m-%d"\n'
  305. fmt += 'set xdata time\n'
  306. fmt += 'set format x "%Y-%m-%d"\n'
  307. fmt += 'set xrange ["%s":"%s"]\n' % ("1979-09-01", enddate)
  308. fmt += 'set key left top box\n'
  309. fmt += 'plot "-" using 3:1 title "Leap second inserted" with points ;\n'
  310. for (i, (r, d)) in enumerate(zip(raw, dates)):
  311. fmt += "%d\t%s\t%s\n" % (i, r, d)
  312. fmt += 'e\n'
  313. print(fmt)
  314. def rfc822_to_unix(tv):
  315. "Local Unix time to RFC822 date."
  316. return calendar.timegm(time.strptime(tv, "%d %b %Y %H:%M:%S"))
  317. def unix_to_rfc822(tv):
  318. "RFC822 date to gmt Unix time."
  319. return time.strftime("%d %b %Y %H:%M:%S", time.gmtime(tv))
  320. def printnext(val):
  321. "Compute Unix time correponsing to a scheduled leap second."
  322. if val[:3].lower() not in ("jun", "dec"):
  323. sys.stderr.write("leapsecond.py: -n argument must begin with "
  324. "'Jun' or 'Dec'\n")
  325. raise SystemExit(1)
  326. # else:
  327. month = val[:3].lower()
  328. if len(val) != 7:
  329. sys.stderr.wrrite("leapsecond.py: -n argument must be of "
  330. "the form {jun|dec}nnnn.\n")
  331. raise SystemExit(1)
  332. try:
  333. year = int(val[3:])
  334. except ValueError:
  335. sys.stderr.write("leapsecond.py: -n argument must end "
  336. "with a 4-digit year.\n")
  337. raise SystemExit(1)
  338. # Date looks valid
  339. tv = leapbound(year, month)
  340. print("%d /* %s */" % (iso_to_unix(tv), tv))
  341. def leapbound(year, month):
  342. "Return a leap-second date in RFC822 form."
  343. # USNO lists JAN and JUL (month following the leap second).
  344. # IERS lists DEC. and JUN. (month preceding the leap second).
  345. # Note: It is also possible for leap seconds to occur in end-Mar and
  346. # end-Sep although none have occurred yet
  347. if month.upper()[:3] == "JAN":
  348. tv = "%s-12-31T23:59:60" % (int(year) - 1)
  349. elif month.upper()[:3] in ("JUN", "JUL"):
  350. tv = "%s-06-30T23:59:59" % year
  351. elif month.upper()[:3] == "DEC":
  352. tv = "%s-12-31T23:59:59" % year
  353. return tv
  354. # Main part
  355. def usage():
  356. print(__doc__)
  357. raise SystemExit(0)
  358. if __name__ == '__main__':
  359. import getopt
  360. (options, arguments) = getopt.getopt(sys.argv[1:], "hvf:g:H:i:n:o:I:O:")
  361. for (switch, val) in options:
  362. if switch == '-h': # help, get usage only
  363. usage()
  364. elif switch == '-v': # be verbose
  365. verbose = 1
  366. elif switch == '-f': # Fetch USNO data to cache locally
  367. save_leapseconds(val)
  368. raise SystemExit(0)
  369. elif switch == '-g': # Graph the leap_second history
  370. graph_history(val)
  371. raise SystemExit(0)
  372. elif switch == '-H': # make leapsecond include
  373. sys.stdout.write(make_leapsecond_include(val))
  374. raise SystemExit(0)
  375. elif switch == '-i': # Compute Unix time from RFC822 date
  376. print(rfc822_to_unix(val))
  377. raise SystemExit(0)
  378. elif switch == '-n': # Compute possible next leapsecond
  379. printnext(val)
  380. raise SystemExit(0)
  381. elif switch == '-o': # Compute RFC822 date from Unix time
  382. print(unix_to_rfc822(float(val)))
  383. raise SystemExit(0)
  384. elif switch == '-I': # Compute Unix time from ISO8601 date
  385. print(isotime(val))
  386. raise SystemExit(0)
  387. elif switch == '-O': # Compute ISO8601 date from Unix time
  388. print(isotime(float(val)))
  389. raise SystemExit(0)
  390. # End