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.
This commit is contained in:
Stephen Heumann 2021-09-26 20:31:15 -05:00
parent 379f2f93ad
commit 5ad86f4a0b
2 changed files with 665 additions and 0 deletions

651
time.asm
View File

@ -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

View File

@ -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