From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from tb-mx1.topicbox.com (localhost.local [127.0.0.1]) by tb-mx1.topicbox.com (Postfix) with ESMTP id 8091C2140012 for <9fans@9fans.net>; Sat, 25 Apr 2020 20:54:33 -0400 (EDT) (envelope-from ori@eigenstate.org) Received: from tb-mx1.topicbox.com (localhost [127.0.0.1]) by tb-mx1.topicbox.com (Authentication Milter) with ESMTP id D966EEC2956; Sat, 25 Apr 2020 20:54:33 -0400 ARC-Seal: i=1; a=rsa-sha256; cv=none; d=topicbox.com; s=arcseal; t= 1587862473; b=aWGPDNovhUseTJWCG6SneHoJ62fzi11x9yCrccd4bfGt/a8IRT 2zSpRujlQndPfp1hT/0FEH+N18Cbc/B2hRIm0XidGM75UOo9FcDtsHdzWgxZ+sNI B36k7S8SKV+sFmbHAyq8LTy+KFuzlyB2LtYs9usX006NwYAojcPj2We69ybPAnW8 PwQtXIfdRQk6Z8X7AvRYCJS6syyBZt0ko7WCFpC4kkCDLTBsNqutF5rHMLQFflTj Ax8uenpIPoCJYm9V7EiOnWpBzgbA2we4UVVjoEBdaygn9RCPG7sbmMY/ajOjgLby p0f20AQr6i1ZF5ZDUARKW45tzRTN+WmTQfxw== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d= topicbox.com; h=message-id:to:subject:date:from:mime-version :content-type:content-transfer-encoding; s=arcseal; t= 1587862473; bh=IGW3wRIDNnbD4V6GCDyDHcWmKzVnIknjlFT6vaY8QRY=; b=s R6Qom6TB4yNvRPnS2e33ETJjHQlr6j7X/JolDgVC49DK5R6lhiwVvCpo71iH7CV2 kGKwZNUtkXvBwv5P5hBrZ3ZKEoLMEvFlrapoz31qLn7gzTFNU4qaGVwcqNwoXCHH aqHLcxkfxgTQ5bPkicS/sSSJbOG0ql1pVWy85qX/SYwb0L9zmbdawdqu4UEPV36N eA9YEmgAEnnLSL1QXyc1a8JCYVqr5JwyBuYunnS6fz8mA/p1hKH34tQZ3P/HTynE GltUzBukDbmbZPILvbW4uMH6TZVWLIWq52FiLWRRzNrkELXdAUYOfPKxZFMFGs55 s7pwMAAs45blRC8cgpXEw== ARC-Authentication-Results: i=1; tb-mx1.topicbox.com; arc=none (no signatures found); bimi=none (Domain is not BIMI enabled); dkim=none (no signatures found); dmarc=pass policy.published-domain-policy=none policy.applied-disposition=none policy.evaluated-disposition=none (p=none,d=none,d.eval=none) policy.policy-from=p header.from=eigenstate.org; iprev=pass smtp.remote-ip=206.124.132.107 (mimir.eigenstate.org); spf=pass smtp.mailfrom=ori@eigenstate.org smtp.helo=mimir.eigenstate.org; x-aligned-from=pass (Address match); x-ptr=pass smtp.helo=mimir.eigenstate.org policy.ptr=mimir.eigenstate.org; x-return-mx=pass header.domain=eigenstate.org policy.is_org=yes (MX Records found: eigenstate.org,kusuri.pikopiko.org,mail.pikopiko.org,nokogiri.pikopiko.org); x-return-mx=pass smtp.domain=eigenstate.org policy.is_org=yes (MX Records found: eigenstate.org,kusuri.pikopiko.org,mail.pikopiko.org,nokogiri.pikopiko.org); x-tls=pass smtp.version=TLSv1.2 smtp.cipher=ECDHE-RSA-AES256-GCM-SHA384 smtp.bits=256/256; x-vs=clean score=0 state=0 Authentication-Results: tb-mx1.topicbox.com; arc=none (no signatures found); bimi=none (Domain is not BIMI enabled); dkim=none (no signatures found); dmarc=pass policy.published-domain-policy=none policy.applied-disposition=none policy.evaluated-disposition=none (p=none,d=none,d.eval=none) policy.policy-from=p header.from=eigenstate.org; iprev=pass smtp.remote-ip=206.124.132.107 (mimir.eigenstate.org); spf=pass smtp.mailfrom=ori@eigenstate.org smtp.helo=mimir.eigenstate.org; x-aligned-from=pass (Address match); x-ptr=pass smtp.helo=mimir.eigenstate.org policy.ptr=mimir.eigenstate.org; x-return-mx=pass header.domain=eigenstate.org policy.is_org=yes (MX Records found: eigenstate.org,kusuri.pikopiko.org,mail.pikopiko.org,nokogiri.pikopiko.org); x-return-mx=pass smtp.domain=eigenstate.org policy.is_org=yes (MX Records found: eigenstate.org,kusuri.pikopiko.org,mail.pikopiko.org,nokogiri.pikopiko.org); x-tls=pass smtp.version=TLSv1.2 smtp.cipher=ECDHE-RSA-AES256-GCM-SHA384 smtp.bits=256/256; x-vs=clean score=0 state=0 X-ME-VSCause: gggruggvucftvghtrhhoucdtuddrgeduhedrheehgdefkecutefuodetggdotefrodftvf curfhrohhfihhlvgemucfhrghsthforghilhdpggftfghnshhusghstghrihgsvgdpuffr tefokffrpgfnqfghnecuuegrihhlohhuthemuceftddtnecunecujfgurhepkffvufffhf ggtgfgsehtkeejtddttdejnecuhfhrohhmpehorhhisegvihhgvghnshhtrghtvgdrohhr ghenucffohhmrghinhepvghighgvnhhsthgrthgvrdhorhhgnecukfhppedvtdeirdduvd egrddufedvrddutdejpdduiedvrdekfedrudefvddrvdegheenucevlhhushhtvghrufhi iigvpedtnecurfgrrhgrmhepihhnvghtpedvtdeirdduvdegrddufedvrddutdejpdhhvg hlohepmhhimhhirhdrvghighgvnhhsthgrthgvrdhorhhgpdhmrghilhhfrhhomhepoeho rhhisegvihhgvghnshhtrghtvgdrohhrgheq X-ME-VSScore: 0 X-ME-VSCategory: clean Received-SPF: pass (eigenstate.org: 206.124.132.107 is authorized to use 'ori@eigenstate.org' in 'mfrom' identity (mechanism 'mx' matched)) receiver=tb-mx1.topicbox.com; identity=mailfrom; envelope-from="ori@eigenstate.org"; helo=mimir.eigenstate.org; client-ip=206.124.132.107 Received: from mimir.eigenstate.org (mimir.eigenstate.org [206.124.132.107]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by tb-mx1.topicbox.com (Postfix) with ESMTPS for <9fans@9fans.net>; Sat, 25 Apr 2020 20:54:32 -0400 (EDT) (envelope-from ori@eigenstate.org) Received: from abbatoir.fios-router.home (pool-162-83-132-245.nycmny.fios.verizon.net [162.83.132.245]) by mimir.eigenstate.org (OpenSMTPD) with ESMTPSA id 39848f47 (TLSv1.2:ECDHE-RSA-AES256-SHA:256:NO) for <9fans@9fans.net>; Sat, 25 Apr 2020 17:54:31 -0700 (PDT) Message-ID: <756B454F0747C231D31E8E3D3AFA3113@eigenstate.org> To: 9fans@9fans.net Subject: libdate Date: Sat, 25 Apr 2020 17:54:30 -0700 From: ori@eigenstate.org MIME-Version: 1.0 Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: 8bit Topicbox-Policy-Reasoning: allow: sender is a member Topicbox-Message-UUID: 8108173e-8758-11ea-8101-a994d5c52e11 Date handling on plan 9 is almost adequate today if you don't have to parse dates or deal with timezones, and don't do multithreading. Otherwise, it's difficult to get right, and we often don't. We've got a crappy home-rolled date parser in seconds(1), a few in the upas source tree to deal with mail formats, and git9 has a few hacks around this as well. Out of tree, joe9 has been trying to write code that takes stock information in one timezone and moves them to another, and our APIs there are completely inadequate. So, I tried to write a library that is adequate, without being complicated. The code lives here: https://git.eigenstate.org/ori/date.git I'll probably be merging in the changes between Tmd and Tm soon, and committing to 9front, possibly even as part of libc. Some additional work is probably going to be needed to convert from IANA zoneinfo to actually bring our timezone data up to date. We may also need some timezone info format changes to handle political (and leap second) changes. The manpage is attached below for review: TMDATE(2) TMDATE(2) NAME tmnow, tmgetzone, tmtime, tmstime, tmparse, tmfmt, tmnorm, - convert date and time SYNOPSIS #include #include typedef struct Tmd Tmd; struct Tmd { vlong abs; /* seconds since Jan 1 1970, GMT */ int sec; /* seconds (range 0..59) */ int min; /* minutes (0..59) */ int hour; /* hours (0..23) */ int mday; /* day of the month (1..31) */ int mon; /* month of the year (0..11) */ int year; /* year A.D. - 1900 */ int wday; /* day of week (0..6, Sunday = 0) */ int yday; /* day of year (0..365) */ char zone[]; /* time zone name */ int tzoff; /* time zone delta from GMT */ }; Tzone *tmgetzone(char *name); Tmd *tmnow(Tmd *tm, char *tz); Tmd *tmtime(Tmd *tm, vlong abs, Tzone *tz); Tmd *tmstime(Tmd *tm, vlong sec, Tzone *tz); Tmd *tmparse(char *fmt, char *tm, Tzone *zone, Tmd *dst); int tmfmt(char *buf, usize nbuf, char *fmt, Tmd *tm); void tmnorm(Tmd *tm); void tmfmtinstall(char *fmt); DESCRIPTION This family of functions handles simple date and time manpu- lation. Times are represented as an absolute instant in time, combined with a time zone. Time zones are loaded by as name. They can be specified as the abbreviated timezone name, the full timezone name, the path to a timezone file, or an absolute offset in the HHMM form. When given as a timezone, any instant-dependent adjustments such as leap seconds and daylight savings time will be applied to the derived fields of struct tm, but will not affect the absolute time. The time zone name local always refers to the time in /env/timezone. The nil timezone always refers to GMT. Tmgetzone loads a timezone by name. The returned timezone is cached for the lifetime of the program, and should not be freed. Loading a timezone repeatedly by name loads from the cache, and does not leak. Tmnow gets the current time of day in the requested time zone. Tmtime converts the millisecond-resolution timestamp 'abs' into a Tm struct in the requested timezone. Tmstime is identical to tmtime, but accepts the time in sec- onds. Tmparse parses a time from a string according to the format argument. The result is returned in the timezone requested. If there is a timezone in the date, then we tzshift to the local timezone. The format argument takes contains zero or more of the fol- lowing components: Y, YY, YYYY Represents the year. YY prints the year in 2 digit form. M, MM, MMM, MMMM The month of the year, in unpadded numeric, padded numeric, short name, or long name, respectively. D, DD The day of month in unpadded or padded numeric form, respectively. W, WW The day of week in short or long name form, respec- tively. h, hh The hour in unpadded or padded form, respectively m, mm The minute in unpadded or padded form, respectively s, ss The second in unpadded or padded form, respectively z, Z, ZZ The timezone in named, [+-]HHMM and [+-]HH:MM form, respectively a, A Lower and uppercase 'am' and 'pm' specifiers, respec- tively. [...] Quoted text, copied directly to the output. Any characters not specified above are copied directly to output, without modification. If the format argument is nil, it makes an attempt to parse common human readable date formats. These formats include ISO-8601,RFC-3339 and RFC-2822 dates. Tmfmt formats a Tm struct according to the format fmt. If fmt is nil, we format as in ctime(2). At most characters are written into buf, including the terminator. The format is identical to tmparse. When parsing, any amount of whitespace is treated as a sin- gle token. All string matches are case insensitive, and zero padding is optional. Tmrecalc takes a manually adjusted Tm structure, and recal- culates the absolute time from the year, mon, mday, hr, min and sec fields. Other fields are ignored. This recalcula- tion respects the time zone stored in struct tm. Out of range values are wrapped. For example, December 32nd becomes January 1st. Tmfmtinstall installs a time format specifier %τ. The time format behaves as in tmfmt Examples All examples assume tmfmtinstall has been called. Get the current date in the local timezone, GMT, and US_Pacific time. Print it using the default format. Tm t; Tzone *zl, *zp; if((zl = tmgetzone("local") == nil) sysfatal("load zone: %r"); if((zp = tmgetzone("US_Pacific") == nil) sysfatal("load zone: %r"); print("local: %τ\n", tmnow(&t, zl)); print("gmt: %τ\n", tmnow(&t, nil)); print("eastern: %τ\n", tmnow(&t, zp)); Compare if two times are the same, regardless of timezone. Tm a, b; tmparse(&a, nil, "Tue Dec 10 12:36:00 PST 2019"); tmparse(&b, nil, "Tue Dec 10 15:36:00 EST 2019"); if(a.abs == b.abs) print("same\n"); else print("different\n"); Convert from one timezone to another. Tm here, there; Tzone *zl, *zp; if((zl = tmgetzone("local") == nil) sysfatal("load zone: %r"); if((zp = tmgetzone("US_Pacific") == nil) sysfatal("load zone: %r"); if(tmnow(&here, zl) == nil) sysfatal("get time: %r"); if(tmtime(&there, here.abs, zp) == nil) sysfatal("shift time: %r"); Add a day to two times. Because we picked daylight savings time to adjust over, only 23 hours are added. Tm t; tmparse(&t, "W MMM D hh:mm:ss z YYYY, "Sun Nov 2 13:11:11 PST 2019"); tm.day++; tmrecalc(&t); print("%τ", &t); /* Mon Nov 3 13:11:11 PST 2019 */