From 5ad86f4a0b34262431e0a1621a3ad612fb2ff28e Mon Sep 17 00:00:00 2001 From: Stephen Heumann Date: Sun, 26 Sep 2021 20:31:15 -0500 Subject: [PATCH] Implement strftime. This is intended to be a complete implementation of strftime as specified in C17, although it lacks meaningful support for time zones or non-C locales. --- time.asm | 651 ++++++++++++++++++++++++++++++++++++++++++++++++++++ time.macros | 14 ++ 2 files changed, 665 insertions(+) diff --git a/time.asm b/time.asm index b2b0a8b..52e4dcc 100644 --- a/time.asm +++ b/time.asm @@ -543,3 +543,654 @@ lb1 lda count plb creturn 4:tptr end + +**************************************************************** +* +* size_t strftime( +* char * restrict s, +* size_t maxsize, +* const char * restrict format, +* const struct tm * restrict timeptr); +* +* Inputs: +* s - pointer to output buffer +* maxsize - max number of bytes to write +* format - format string +* timeptr - the time/date +* +* Outputs: +* s - formatted string representation of the time/date +* returns length of s (not including terminating null), +* or 0 if maxsize is too small +* +**************************************************************** +* +strftime start + + csubroutine (4:s,4:maxsize,4:format,4:timeptr),14 +substfmt equ 1 substitute format str (used if non-null) +s_orig equ substfmt+2 original s pointer (start of output str) +overflow equ s_orig+4 overflow flag +numstr equ overflow+2 string representation of a number + +numstr_len equ 6 length of numstr + +tm_sec equ 0 displacements into the time record +tm_min equ 2 +tm_hour equ 4 +tm_mday equ 6 +tm_mon equ 8 +tm_year equ 10 +tm_wday equ 12 +tm_yday equ 14 +tm_isdst equ 16 + + phb set data bank = program bank + phk + plb + +;initialization of local variables + stz substfmt substfmt = 0 + lda s s_orig = s + sta s_orig + lda s+2 + sta s_orig+2 + stz overflow overflow = false + +;main loop to process the format string +fmtloop jsr nextch get next character in format + cmp #'%' if it is not '%' + beq dosubst +nonfmt jsr writech write it to the output + bra fmtloop continue format loop +dosubst jsr nextch get next character in format + cmp #'E' if it is 'E' or 'O' + beq skipalt + cmp #'O' + bne dofmt +skipalt jsr nextch skip it +dofmt cmp #'%' if format character is '%' + beq nonfmt write it like an ordinary character + cmp #'@' if fmt chr is outside valid range + blt fmtloop skip it + cmp #'z'+1 + bge fmtloop + and #$003f if we are here, fmt chr is in ['@'..'z'] + asl a convert to jump table position + asl a + tax + lda fmttbl+2,x if there is a substitution + beq fmtcall + sta substfmt do the substitution + bra fmtloop +fmtcall jsr (fmttbl,x) otherwise, call the format routine + bra fmtloop continue format loop + + +;subroutine to get next character in format string (call only from main loop) +;returns with character in a, or exits via strftime_return if character is 0 +nextch lda substfmt if there is a substitute format string + beq nosubst + lda (substfmt) get next character from it + inc substfmt advance subst string pointer + and #$00FF + bne retchar if at end of substitute format string + stz substfmt go back to using main format string +nosubst lda [format] get next character from main fmt string + and #$00FF + beq strftime_return if char is '\0', return from strftime + inc4 format advance fmt string pointer +retchar rts return from nextch + +;code to return from strftime +strftime_return anop + jsr writech write '\0' to output + pla discard nextch return address + lda overflow if there was an overflow + beq ret_good + stz maxsize maxsize = 0 + stz maxsize+2 + bra ret +ret_good clc else + lda s maxsize = s - s_orig - 1 + sbc s_orig + sta maxsize + lda s+2 + sbc s_orig+2 + sta maxsize+2 +ret plb restore program bank + creturn 4:maxsize return maxsize + + +;subroutine to write a character to the output +;input: character in low-order byte of a (high-order byte is ignored) +;leaves x unchanged +writech ldy maxsize if remaining size is 0 + bne writeok + ldy maxsize+2 + bne writeok + lda #1 set overflow flag + sta overflow + rts return +writeok short M write the character to s + sta [s] + long M + inc4 s s++ + dec4 maxsize maxsize-- + rts return + + +;table of formatting routines or substitutions for the conversion specifiers +;first ptr is a routine, second is a subst string - only one should be non-zero +fmttbl anop + dc a2'fmt_invalid,0' @ + dc a2'fmt_A,0' A + dc a2'fmt_B,0' B + dc a2'fmt_C,0' C + dc a2'0,subst_D' D + dc a2'fmt_invalid,0' E + dc a2'0,subst_F' F + dc a2'fmt_G,0' G + dc a2'fmt_H,0' H + dc a2'fmt_I,0' I + dc a2'fmt_invalid,0' J + dc a2'fmt_invalid,0' K + dc a2'fmt_invalid,0' L + dc a2'fmt_M,0' M + dc a2'fmt_invalid,0' N + dc a2'fmt_invalid,0' O + dc a2'fmt_invalid,0' P + dc a2'fmt_invalid,0' Q + dc a2'0,subst_R' R + dc a2'fmt_S,0' S + dc a2'0,subst_T' T + dc a2'fmt_U,0' U + dc a2'fmt_V,0' V + dc a2'fmt_W,0' W + dc a2'0,subst_X' X + dc a2'fmt_Y,0' Y + dc a2'fmt_Z,0' Z + dc a2'fmt_invalid,0' [ + dc a2'fmt_invalid,0' \ + dc a2'fmt_invalid,0' ] + dc a2'fmt_invalid,0' ^ + dc a2'fmt_invalid,0' _ + dc a2'fmt_invalid,0' ` + dc a2'fmt_a,0' a + dc a2'fmt_b,0' b + dc a2'0,subst_c' c + dc a2'fmt_d,0' d + dc a2'fmt_e,0' e + dc a2'fmt_invalid,0' f + dc a2'fmt_g,0' g + dc a2'fmt_h,0' h + dc a2'fmt_invalid,0' i + dc a2'fmt_j,0' j + dc a2'fmt_invalid,0' k + dc a2'fmt_invalid,0' l + dc a2'fmt_m,0' m + dc a2'fmt_n,0' n + dc a2'fmt_invalid,0' o + dc a2'fmt_p,0' p + dc a2'fmt_invalid,0' q + dc a2'0,subst_r' r + dc a2'fmt_invalid,0' s + dc a2'fmt_t,0' t + dc a2'fmt_u,0' u + dc a2'fmt_invalid,0' v + dc a2'fmt_w,0' w + dc a2'0,subst_x' x + dc a2'fmt_y,0' y + dc a2'fmt_z,0' z + +;%a - abbreviated weekday name +fmt_a ldy #tm_wday + lda [timeptr],y + asl a + tay + ldx weekdays,y + lda |0,x + jsr writech + lda |1,x + jsr writech + lda |2,x + brl writech + +;%A - full weekday name +fmt_A ldy #tm_wday + lda [timeptr],y + asl a + tay + ldx weekdays,y +A_loop lda |0,x + and #$00FF + beq A_ret + jsr writech + inx + bra A_loop +A_ret rts + +;%b - abbreviated month name +fmt_b ldy #tm_mon + lda [timeptr],y + asl a + tay + ldx months,y + lda |0,x + jsr writech + lda |1,x + jsr writech + lda |2,x + brl writech + +;%B - full month name +fmt_B ldy #tm_mon + lda [timeptr],y + asl a + tay + ldx months,y +B_loop lda |0,x + and #$00FF + beq A_ret + jsr writech + inx + bra A_loop +B_ret rts + +;%c - date and time +subst_c dc c'%a %b %e %H:%M:%S %Y',i1'0' + +;%C - century +fmt_C jsr format_year + ldx #0 +C_loop lda numstr,x + and #$00FF + cmp #' ' + beq C_skip + jsr writech +C_skip inx + cpx #numstr_len-2 + blt C_loop + rts + +;%d - day of the month (01-31) +fmt_d ldy #tm_mday + brl print2digits_of_field + +;%D - equivalent to %m/%d/%y +subst_D dc c'%m/%d/%y',i1'0' + +;%e - day of the month (1-31, padded with space if a single digit) +fmt_e ldy #tm_mday + lda [timeptr],y + ldy #2 + cmp #10 + bge e_print + tax + lda #' ' + jsr writech + txa + ldy #1 +e_print brl printdigits + +;%F - equivalent to %Y-%m-%d +subst_F dc c'%Y-%m-%d',i1'0' + +;%g - last two digits of week-based year +fmt_g jsr week_number_V + jsr format_year_altbase + brl write_year_2digit + +;%G - week-based year +fmt_G jsr week_number_V + jsr format_year_altbase + brl write_year + +;%h - equivalent to %b +fmt_h brl fmt_b + +;%H - hour (24-hour clock, 00-23) +fmt_H ldy #tm_hour + brl print2digits_of_field + +;%I - hour (12-hour clock, 01-12) +fmt_I ldy #tm_hour + lda [timeptr],y + bne I_adjust + lda #12 +I_adjust cmp #12+1 + blt I_print + sbc #12 +I_print brl print2digits + +;%j - day of the year (001-366) +fmt_j ldy #tm_yday + lda [timeptr],y + inc a + ldy #3 + brl printdigits + +;%m - month number +fmt_m ldy #tm_mon + lda [timeptr],y + inc a + brl print2digits + +;%M - minute +fmt_M ldy #tm_min + brl print2digits_of_field + +;%n - new-line character +fmt_n lda #$0A + brl writech + +;%p - AM/PM +fmt_p ldy #tm_hour + lda [timeptr],y + cmp #12 + bge p_pm + lda #'A' + bra p_write +p_pm lda #'P' +p_write jsr writech + lda #'M' + brl writech + +;%r - time (using 12-hour clock) +subst_r dc c'%I:%M:%S %p',i1'0' + +;%R - equivalent to %H:%M +subst_R dc c'%H:%M',i1'0' + +;%S - seconds +fmt_S ldy #tm_sec + brl print2digits_of_field + +;%t - horizontal tab character +fmt_t lda #$09 + brl writech + +;%T - equivalent to %H:%M:%S +subst_T dc c'%H:%M:%S',i1'0' + +;%u - weekday number (1-7, Monday=1) +fmt_u ldy #tm_wday + lda [timeptr],y + bne u_print + lda #7 +u_print ldy #1 + brl printdigits + +;%U - week number of the year (first Sunday starts week 01) +fmt_U ldy #tm_yday + lda [timeptr],y + clc + adc #7 + sec + ldy #tm_wday + sbc [timeptr],y + jsr div7 + tya + brl print2digits + +;%V - ISO 8601 week number +fmt_V jsr week_number_V + txa + brl print2digits + +;%w - weekday number (0-6, 0=Sunday) +fmt_w ldy #tm_wday + lda [timeptr],y + ldy #1 + brl printdigits + +;%W - week number of the year (first Monday starts week 01) +fmt_W jsr week_number_W + tya + brl print2digits + +;%x - date +subst_x dc c'%m/%d/%y',i1'0' + +;%X - time +subst_X dc c'%T',i1'0' + +;%y - last two digits of year +fmt_y jsr format_year +write_year_2digit anop + lda numstr+4 + jsr writech + lda numstr+5 + brl writech + +;%Y - year +fmt_Y jsr format_year +write_year anop + ldx #0 +Y_loop lda numstr,x + and #$00FF + cmp #' ' + beq Y_skip + jsr writech +Y_skip inx + cpx #numstr_len + blt Y_loop + rts + +;%z - offset from UTC, if available +;we print nothing, because time zone info is not available +fmt_z rts + +;%Z - time zone name or abbreviation, if available +;we print nothing, because time zone info is not available +fmt_Z rts + +fmt_invalid rts + + +;get decimal representation of the year in numstr +;the string is adjusted to have at least four digits +format_year anop + lda #1900 +format_year_altbase anop alt entry point using year base in a + ldx #1 default to signed + clc + ldy #tm_year + adc [timeptr],y + bvc year_ok + ldx #0 use unsigned if signed value overflows +year_ok jsr int2dec + short M,I + ldx #4 +yr_adjlp lda numstr,x adjust year to have >= 4 digits + cmp #'-' + bne yr_adj1 + sta numstr-1,x + bra yr_adj2 +yr_adj1 cmp #' ' + bne yr_adj3 +yr_adj2 lda #'0' + sta numstr,x +yr_adj3 dex + cpx #2 + bge yr_adjlp + long M,I + rts + + +;get the week number as for %W (first Monday starts week 1) +;output: week number in y +week_number_W anop + ldy #tm_wday + lda [timeptr],y + beq W_yday + sec + lda #7 + sbc [timeptr],y +W_yday sec + ldy #tm_yday + adc [timeptr],y + brl div7 + + +;get the ISO 8601 week number (as for %V) and corresponding year adjustment +;output: week number in x, adjusted year base in a (1900-1, 1900, or 1900+1) +week_number_V anop + jsr week_number_W get %W-style week number (kept in x) + tyx + ldy #tm_wday calculate wday for Jan 1 (kept in a) + lda [timeptr],y + sec + ldy #tm_yday + sbc [timeptr],y + clc + adc #53*7 + jsr div7 + cmp #2 if Jan 1 was Tue/Wed/Thu + blt V_adjust + cmp #4+1 + bge V_adjust + inx inc week (week 1 started in last year) +V_adjust txy + bne V_not0 week 0 is really 52 or 53 of last year: + ldx #52 assume 52 + cmp #5 if Jan 1 is Fri + bne V_0notfr + inx last year had week 53 + bra V_0done +V_0notfr cmp #6 else if Jan 1 is Sat + bne V_0done + ldy #tm_year + lda [timeptr],y + dec a + jsr leapyear if last year was a leap year + bne V_0done + inx last year had week 53 +V_0done lda #-1+1900 year adjustment is -1 + bra V_done +V_not0 cpx #53 week 53 might be week 1 of next year: + bne V_noadj + cmp #4 if Jan 1 was Thu + beq V_noadj it is week 53 + cmp #3 else if Jan 1 was Wed + bne V_53is1 + ldy #tm_year + lda [timeptr],y + jsr leapyear and this is a leap year + beq V_noadj it is week 53 +V_53is1 ldx #1 otherwise, it is really week 1 + lda #1+1900 and year adjustment is +1 + rts +V_noadj lda #0+1900 if we get here, year adjustment is 0 +V_done rts + + +;check if a year is a leap year +;input: tm_year value in a +;output: z flag set if a leap year, clear if not; x,y unmodified +leapyear and #$0003 not multiple of 4 => not leap year + bne ly_done + clc calculate year mod 400 + adc #1900-1600 + bpl ly_lp400 + clc + adc #32800 + sec +ly_lp400 sbc #400 + bcs ly_lp400 + adc #400 + beq ly_done multiple of 400 => leap year + sec +ly_lp100 sbc #100 + bcs ly_lp100 + cmp #-100 + bne ly_leap + dec a other multiple of 100 => not leap year + rts +ly_leap lda #0 other multiple of 4 => leap year +ly_done rts + + +;divide a number (treated as unsigned) by 7 +;input: dividend in a +;output: quotient in y, remainder in a, x unmodified +div7 ldy #-1 + sec +sublp iny + sbc #7 + bcs sublp + adc #7 + rts + + +;print the low-order two digits of a field of struct tm +;(with leading zeros, if any) +;input: offset of field in y +print2digits_of_field anop + lda [timeptr],y load the field + +;print the low-order two digits of a number (with leading zeros, if any) +;input: number in a +print2digits anop + ldy #2 print two digits + +;print the low-order digits of a number (with leading zeros, if any) +;input: number in a, how many digits to print in y +printdigits anop +pd1 phy save number of digits to print + ldx #0 treat as signed + jsr int2dec convert to decimal string + sec calculate where to print from + lda #numstr_len + sbc 1,s + ply + tax +pd_loop lda numstr,x print the digits + and #$00FF + cmp #' ' change padding spaces to zeros + bne pd_write + lda #'0' +pd_write jsr writech + inx + cpx #numstr_len + blt pd_loop + rts + + +;get decimal representation of a number, placed in numstr +;input: number in a, signed flag in y +int2dec pha number to convert + pea 0000 pointer to string buffer + tdc + clc + adc #numstr + pha + pea numstr_len length of string buffer + phx signed flag + _Int2Dec + rts + + +weekdays dc a2'sun,mon,tue,wed,thu,fri,sat' +sun dc c'Sunday',i1'0' +mon dc c'Monday',i1'0' +tue dc c'Tuesday',i1'0' +wed dc c'Wednesday',i1'0' +thu dc c'Thursday',i1'0' +fri dc c'Friday',i1'0' +sat dc c'Saturday',i1'0' + +months dc a2'jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,dec' +jan dc c'January',i1'0' +feb dc c'February',i1'0' +mar dc c'March',i1'0' +apr dc c'April',i1'0' +may dc c'May',i1'0' +jun dc c'June',i1'0' +jul dc c'July',i1'0' +aug dc c'August',i1'0' +sep dc c'September',i1'0' +oct dc c'October',i1'0' +nov dc c'November',i1'0' +dec dc c'December',i1'0' + end diff --git a/time.macros b/time.macros index c50a85f..d5b39e5 100644 --- a/time.macros +++ b/time.macros @@ -600,3 +600,17 @@ longi off .c mend + macro +&l dec4 &a +&l ~setm + lda &a + bne ~&SYSCNT + dec 2+&a +~&SYSCNT dec &a + ~restm + mend + MACRO +&LAB _INT2DEC +&LAB LDX #$260B + JSL $E10000 + MEND