时间和日期对于初级程序员可能是难以理解的,因为它们不是简单的变量。它们包含好几个成员。造成进一步混淆的是,一个C编译程序往往会提供多个处理时间的函数,而它们的处理方式却互不相同。这些函数分别应该在什么情况下使用呢?本章试图回答一些关于时间和日期的常见问题。
13.1 怎样把日期存储到单个数字中?有这方面的标准吗? 有好几个原因使你想把日期转换成单个数字,包括为了节省存储空间或进行简单的比较。此外,你也许想用转换所得的数字作为编程结构中的一部分。无论如何,如果你想用单个数字表示日期,你就需要问一下自己为什么要这样做,以及你想怎样处理转换所得的数字。回答这些问题将有助于你确定哪种转换方法最好。首先,请看一个简单的例子: # include <stdio. h> # include <stdlib. h> main ( ) { int month, day, year; unsigned long result; printf( "Enter Month, Day, Year : \n"); fflush( stdout ) ; scanf( "%d %d %d" , &month, &day, &year )) result = year; result | =month << 12; result | =day << 14; printf( "The result is: %ul. \n" , result ); } 这个程序通过位操作把三个变量转换为单个数字,以下是它的一种运行示例: Enter Month,Day,Year: 11 22 1972 The result is:470281. 尽管这个程序确实能工作(你可以把它输入计算机测试一下),但它还有一些缺陷。在进一步讨论之前,最好还是先指出其中的一些缺陷分别是什么。 你想到其中的某些缺陷了吗?以下是其中的几种缺陷: ·月份、日和年份是不受限制的,这意味着它们的存储区域必须比实际需要的大,而这将牺牲效率。此外,用户可以输入一个任意大的数值,以致超出位域的边界,从而导致一个完全错误的日期。 ·由日期转换而来的数字不是有序的,你不能根据这些数字对日期进行比较,而这种功能却能带来很大的方便。 ·各成员在转换所得的数字中的安置是简单的,甚至是随意的,然而把它们抽取出来却不那么简单了(你能想出一个简单的办法吗?)。你真正需要的可能是一种存储日期和抽取日期都比较简单的格式。 下面我们逐个分析这些问题。 月份的范围应该从1到12,日期的范围应该从1到31,然而年份却与众不同。你可以根据你的目的把程序中要使用的年份限制在一个范围内。这个范围是可以变化的,具体视程序的目的而定。有些程序需要使用的日期可能在遥远的过去,而另一些程序需要使用的日期可能在遥远的将来。然而,如果你的程序只需要使用1975到2020之间的年份,你就能节省一位存储空间。显然,在把日期转换成数字之前,你应该先检查日期的各成员,以确保它们都在正确的范围之内。
注意:一个考古学数据库就是一个需要使用远古日期的很好的例子。
在C语言中,通常从零开始计算(如数组等)。在这种情况下,强制使所有数字的范围都从零开始是有好处的。因此,如果你要存储的最早的年份是1975年,你应该从所有的年份中减去1975,以使年份的序列从零开始。请看改为以这种方式工作的程序: # include <stdio. h> # include <stdlib. h> main() { int month, day. year; unsigned long result; / * prompt the user for input * / printf( "Enter Month, Day, Year: \n" ) ; fflush( stdout); scanf( "%d %d %d" , &month, &day, &year) ; / * Make all of the ranges begin at zero * / --month; --day; year - = 1975; / * Make sure all of the date elements are in proper range * / if ( ( year <0 || year>127) || / * Keep the year in range * / ( month <0 || month>ll) || / * Keep the month in range * / ( day <0 || day>31) / * Keep the day in range * / ) { printf( "You entered an improper date! \n" ) ; exit(1); } result = year; result | = month <<7; result | =day<<11; printf ( "The result is : %ul. \n" , result) ; } 这个程序并没有考虑到有些月份不到31天的情况,但只需作一点小小的改进就可弥补这一缺陷。注意,当你限制了日期的范围后,在对月值和日值进行移位时,要少移几位。 上述程序所生成的数字仍然不能用来对日期进行排序。为了解决这个问题,你需要注意到这个数字最左边的几位是高于最右边的几位的有效位。因此,你应该把日期中最高的有效部分存入最左边的几位中。为此,上述程序中把二个变量安置到数字中的那部分代码应该改为如下形式: result = day; result | = month<<5; result | = year<<9;
以下是用几个示例日期测试上述修改的结果: Enter Month, Day, Year : 11 22 1980 The result is : 110771. Enter Month, Day, Year: 12 23 1980 The result is : 116211. Enter Month, Day, Year; 8 15 1998 The result is : 74151. 现在,你可以存储记录,而记录的日期可以存成上述格式,并且可以根据转换所得的数字对日期进行排序,而对排序结果的正确性可以充满信心。
最后还需要提到一点是这种存储方式在某种程度上的随机性和抽取日期的问题。这些问题都可以通过使用位域来解决。位域已经在第9章中介绍过了。请看下面这个已经比较完善的程序: / * These are the definitions to aid in the conversion of * dates to integers. */ typedef struct { unsigned int year : 7; unsigned int month : 4; unsigned int day : 5 ; } YearBitF; typedef union { YearBitF date; unsigned long number ; } YearConverter; / * Convert a date into an unsigned long integer. Return zero if * the date is invalid. This uses bit fields and a union. */ unsigned long DateToNumber(int month, int day, int year) { YearConverter yc; / * Make all of the ranges begin at zero * / --month; --day; year - = 1975; / * Make sure all of the date elements are in proper range * / if ( ( year<0 || year>127) || /* Keep the year in range */ ( month<0 || month>11)|| / * Keep the month in range * / ( day <0 || day>31) /* Keep the day in range */ ) return 0; yc. date. day = day; yc. date. month = month; yc. date. year = year; return yc. number+1; } / * Take a number and return the values for day, month, and year * stored therein. Very elegant due to use of bit fields. */ void NumberToDate(unsigned long number, int * month, int * day , int * year ) { YearConverter yc; yc. number = number-1; * month = yc. date. month+ 1; * day = yc. date. day+ 1; * year = yc. date. year+ 1975; }
/* * This tests the routine and makes sure it is OK. */ main() { unsigned long n; int m, d, y; n = DateToNumber( 11,22,1980); if (n == 0) { printf( "The date was invalid. \n" ) ; exit(1); } NumberToDate( n, &m, &d, &y); printf ( "The date after transformation is : %d/%d/%d. \n" , m, d, y) ; } 由于有些月份不足31天,因此上述程序的某些部分效率仍然不高。此外,每月天数的不同将给日期的增值运算和差值运算带来困难。好在C语言中有些现成的函数能非常出色地完成这些更为复杂的任务,它们可以使程序员少写很多代码。 请参见: 13.2怎样把时间存储到单个数字中?有这方面的标准吗? 13.2 怎样把时间存储到单个数字中?有这方面的标准吗? 把时间存储到单个数字中与把日期存储到单个数字中是类似的,因此,你可以先阅读一下13.1。另一方面,这两者之间也有差别。 首先,你应该注意到,一天中的时间比一年中的日期有更强的“确定性”。你知道一分钟内有多少秒,一小时内有多少分,一天内有多少小时。这种确定性使时间处理起来更容易,而且不容易出错。 在选择把时间转换为数字的方法时,你最好遵循下面这些原则: ·应该尽可能地节约存储空间; ·应该能存储不同种类的时间(标准的,军用的); ·应该使时间使用起来又快又高效。
如果你已经阅读了13.1,你可能会认为处理这个问题的一个好办法就是用位域来表示时间。这确实是一个不错的办法,有其巧妙之处。请看下面这个把时间表示为一个整数的程序: /* * The bit field structures for representing time */ typedef struct { unsigned int Hour : 5; unsigned int Minute :6; } TimeType; typedef union { TimeType time; int Number; } TimeConverter; /* * Convert time to a number, returning zero when the values are * out of range. */ int TimeToNumber( int Hour, int Minute) { TimeConverter convert; if (Hour<l || Hour>24 || Minute< 1 || Minute>60) return 0; convert, time. Hour = Hour; convert, time. Minute = Minute; return convert. Number+ 1; } /* * Convert a number back into the two time * elements that compose it. */ void NumberToTime( int Number, int *Hour, int * Minute) { TimeConverter convert; convert. Number = Number - 1; * Hour = convert. time. Hour; * Minute = convert. time. Minute; } /* * A main routine that tests everything to * ensure its proper functioning. */ main() { int Number, Hour, Minute; Hour=13; Minute = 13; Number = TimeToNumber( Hour, Minute); NumberToTime( Number, &Hour, &Minute) ; printf( "The time after conversion is %d:%d. \n" , Hour, Minute); } 在时间表示中加入秒是很容易的,你只需在TimeType结构中加入一个秒域,并在每个转换函数中多加一个参数。 但是,假设你想把转换所得的数字用作时钟,让它整天“滴答滴答”地走个不停,你应该怎样做呢?如果用位域来完成这项任务,你就先需要把数字转换为位域,然后增加秒值,并检测秒值是否达到了60;如果秒值达到了60,你还需要增加分值,并再次检测分值是否达到了60……。这个过程太繁琐了!
这里的问题是:TimeType结构中的各个成员并不是刚好能纳入一个位域——它们的上限临界值并不是2的幂。因此,用数学的方法表示时间更好。这做起来相当简单,你可以用从当天开始时到当天某一点为止所度过的秒(或分)数来表示该点的时间。如果你用这种方法表示时间,那么在相应的数字上加1就相当于在时间上加1秒(或分)。下面的程序就是用这种方法表示时间的: # include <stdio. h> # include <stdlib. h> /* * A subroutine to convert hours and minutes into an * integer number. This does not checking for the sake * of brevity (you’ve seen it done before!). */ int TimeToNumber(int Hours, int Minutes) { return Minutes+ Hours * 60; } /* * Convert an integer to hours and minutes. */ void NumberToTimeC int Number, int * Hours, int * Minutes) { * Minutes = Number % 60; * Hours = Number / 60; } /* * A quickie way to show time. */ void ShowTime(int Number) { int Hours, Minutes; NumberToTimeC Number, &Hours, &Minutes); printf("%02d:%02d\n", Hours, Minutes); }
/* * A main loop to test the salient features of the time class. */ main() { int Number, a; Number = TimeToNumber(9,32); printf("Time starts at :%d" , Number); ShowTime(Number); /* * Assure yourself that minutes are added correctly. * / for( a = 0; a<10; + +a) { printf( "After 32 minutes :"); Number + = 32; / * Add 32 minutes to the time. * / ShowTime(Number); } } 这个程序提供了一种更好的表示时间的方法。它容易操作,并且更为紧凑。它的代码还可以进一步压缩,加入秒的表示留给读者作为练习。 这种格式类似于C函数timelocal()和timegm()所使用的格式,这些函数都可以从任意给定的时间/日期开始计算秒数。只要对本章所提供的时间和日期处理程序稍作修改,你就可以使用这些程序了,而且还可以使用你自己定义的时间格式。 请参见: 13.1怎样把日期存储到单个数字中?有这方面的标准吗? 13.5存储时间的最好方法是哪一种? 13.3 为什么定义了这么多不同的时间标准? 由于所使用的计算机和编译程序不同,你可能会发现定义了许多时间标准。尽管有多种时间标准会带来一定的方便,但把它们都写出来显然要花很长的时间,并且把它们都存储起来也会多占硬盘空间。那么,为什么还要这样呢?其中有好几个原因。 首先,C是一种可移植的语言。因此在一台计算机上编写的C程序应该能在另一台计算机上运行。通常,当用C语言在一个新的系统上进行开发时,必须把专门在某个系统上使用的那些函数添加到C语言中。此后,当C程序需要从一个系统移植到另一个系统中时,将具体命令添加到目标系统中通常就很容易了。这样一来,同一函数的不同版本就都汇集在C语言中了。这种情况就曾多次发生在时间函数身上。 其次,时间(和日期)可能有多种不同的用法。你可能想按秒计时,可能想从一个具体的时间和日期开始计时。此外,你还可能想按最小的时间间隔计时,以确保计时尽可能精确。对于计时,没有一种最好的方法。当你开始编写一个涉及到时间的程序时,你必须先把可以使用的函数分析一遍,并确定哪一种函数最适合于你的目的。如果你要用多种方法处理时间,你就可能要使用多种不同的时间格式和函数。在这种情况下,你可能会庆幸有这么多时间格式可供选择,并且其中的一种能满足你的需要。 请参见: . 13.1怎样把日期存储到单个数字中?有这方面的标准吗? 13.2怎样把时间存储到单个数字中?有这方面的标准吗? 13.4存储日期的最好方法是哪一种? 13.5存储时间的最好方法是哪一种? 13.4 存储日期的最好方法是哪一种? 简而言之,并不存在一种存储日期的最好方法。对存储日期的方法的选择依赖于你究竟要做什么,你可能想把日期存成一个整数(可能是从历史上的某一天开始计算的天数),或者存成一个含月、日、年和其它信息的结构,或者存成一个文本字符串。文本字符串看起来并不实用,并且难以处理,但你应该看到它自有用处。 如果你只是记录一个数字型日期,这个问题就简单多了。你应该使用一种固有的格式,或者用一个整数来表示时间,等等。你还应该确定是否要存储当前日期,是否要更新日期,是否要检查两个日期之间的间隔,等等。完成这些任务的方法有许多种,并且它们大多使用c标准库中所含的格式和函数。但是,如果你在程序的设计中过早地定下一种格式,你就会受到限制。为了保持开阔的思路和程序的灵活性,你应该根据具体情况使用最合适的函数。 但是,你可能想用更复杂的方式表示日期。通常你会用多种不同的方法记忆日期。你不可能总是记住每一件事的准确日期,相反,你可能会把生活中一个重要的日期记成“我16岁生日聚会后的第三天”,或者把一个历史日期记成“奥特曼帝国覆灭后的第十年”。这样的日期不能用简单的数字或结构来表示,它们需要使用更复杂的方式。在存储这种相对日期的同时,你还可能想存储对一个计算机能够处理的已知日期的引用,或者存储一个固定日期。这种办法对日期的排序和操作是有帮助的。 请参见: 13.1 怎样把日期存储到单个数字中?有这方面的标准吗? 13.5 存储时间的最好方法是哪一种? 存储时间的最好方法完全依赖于存储时间的目的和将要施加到时间值上的操作。下面将举出一些时间的不同用途,分析一下它们将有助于你更好地选择时间的存储方法。 假设你只需要记录事件发生的时间,并且要以“实时”方式记录。换句话说,你想确定某一事件发生的真实时间。你要记录的事件可能包括一个文件的创建。一个长而复杂的程序的开始和结束,或者写完一本书的某一章的时间。在这种情况下,你需要从计算机的系统时钟中取出当前时间并存储起来。较好并且较简单的方法是用一个现成的时间函数取出时间,并且直接按原来的格式存储起来。这种方法基本上不需要你做什么工作。 由于种种原因,你可能不想用标准C函数提供的格式存储时间。你可能想用一种更简单的格式,以使操作更加容易,或者想用不同的方式表示时间。在这种情况下,用一个整数值表示时间是个好办法,这种方法在13.2中曾介绍过。这种方法使时间使用起来又快又简单,而且使你可以比较不同的时间,看看哪一个更早。 与处理日期一样,计算时间也可以使用相对的方法,但这些方法很难量化。虽然“午后半小时”并不难量化,但“我吃完午饭后”就很难量化了。尽管这并不是记录时间的最简单的方法,但在有些情况下,这却是唯一的方法。在这些情况下,除了存储描述时间的文本字符串,你别无选择,而此时这也是最好的存储方法。
|