@@ -394,6 +394,323 @@ export class PGOid extends WrappedNumber {
394394 }
395395}
396396
397+ /**
398+ * @typedef Interval
399+ * @see Spanner.interval
400+ */
401+ export class Interval {
402+ private months : number ;
403+ private days : number ;
404+ private nanoseconds : bigint ;
405+
406+ // Regex to parse ISO8601 duration format: P[n]Y[n]M[n]DT[n]H[n]M[n][.fffffffff]S
407+ // Only seconds can be fractional, and can have at most 9 digits after decimal point.
408+ // Both '.' and ',' are considered valid decimal point.
409+ private static readonly ISO8601_PATTERN : RegExp =
410+ / ^ P (? ! $ ) ( - ? \d + Y ) ? ( - ? \d + M ) ? ( - ? \d + D ) ? ( T (? = - ? [ . , ] ? \d ) ( - ? \d + H ) ? ( - ? \d + M ) ? ( - ? ( ( ( \d + ) ( [ . , ] \d { 1 , 9 } ) ? ) | ( [ . , ] \d { 1 , 9 } ) ) S ) ? ) ? $ / ;
411+
412+ static readonly MONTHS_PER_YEAR : number = 12 ;
413+ static readonly DAYS_PER_MONTH : number = 30 ;
414+ static readonly HOURS_PER_DAY : number = 24 ;
415+ static readonly MINUTES_PER_HOUR : number = 60 ;
416+ static readonly SECONDS_PER_MINUTE : number = 60 ;
417+ static readonly SECONDS_PER_HOUR : number =
418+ Interval . MINUTES_PER_HOUR * Interval . SECONDS_PER_MINUTE ;
419+ static readonly MILLISECONDS_PER_SECOND : number = 1000 ;
420+ static readonly MICROSECONDS_PER_MILLISECOND : number = 1000 ;
421+ static readonly NANOSECONDS_PER_MICROSECOND : number = 1000 ;
422+ static readonly NANOSECONDS_PER_MILLISECOND : number =
423+ Interval . MICROSECONDS_PER_MILLISECOND *
424+ Interval . NANOSECONDS_PER_MICROSECOND ;
425+ static readonly NANOSECONDS_PER_SECOND : number =
426+ Interval . MILLISECONDS_PER_SECOND *
427+ Interval . MICROSECONDS_PER_MILLISECOND *
428+ Interval . NANOSECONDS_PER_MICROSECOND ;
429+ static readonly NANOSECONDS_PER_DAY : bigint =
430+ BigInt ( Interval . HOURS_PER_DAY ) *
431+ BigInt ( Interval . SECONDS_PER_HOUR ) *
432+ BigInt ( Interval . NANOSECONDS_PER_SECOND ) ;
433+ static readonly NANOSECONDS_PER_MONTH : bigint =
434+ BigInt ( Interval . DAYS_PER_MONTH ) * Interval . NANOSECONDS_PER_DAY ;
435+ static readonly ZERO : Interval = new Interval ( 0 , 0 , BigInt ( 0 ) ) ;
436+
437+ /**
438+ * @param months months part of the `Interval`
439+ * @param days days part of the `Interval`
440+ * @param nanoseconds nanoseconds part of the `Interval`
441+ */
442+ constructor ( months : number , days : number , nanoseconds : bigint ) {
443+ if ( ! is . integer ( months ) ) {
444+ throw new GoogleError (
445+ `Invalid months: ${ months } , months should be an integral value`
446+ ) ;
447+ }
448+
449+ if ( ! is . integer ( days ) ) {
450+ throw new GoogleError (
451+ `Invalid days: ${ days } , days should be an integral value`
452+ ) ;
453+ }
454+
455+ if ( is . null ( nanoseconds ) || is . undefined ( nanoseconds ) ) {
456+ throw new GoogleError (
457+ `Invalid nanoseconds: ${ nanoseconds } , nanoseconds should be a valid bigint value`
458+ ) ;
459+ }
460+
461+ this . months = months ;
462+ this . days = days ;
463+ this . nanoseconds = nanoseconds ;
464+ }
465+
466+ /**
467+ * @returns months part of the `Interval`.
468+ */
469+ getMonths ( ) : number {
470+ return this . months ;
471+ }
472+
473+ /**
474+ * @returns days part of the `Interval`.
475+ */
476+ getDays ( ) : number {
477+ return this . days ;
478+ }
479+
480+ /**
481+ * @returns nanoseconds part of the `Interval`.
482+ */
483+ getNanoseconds ( ) : bigint {
484+ return this . nanoseconds ;
485+ }
486+
487+ /**
488+ * Constructs an `Interval` with specified months.
489+ */
490+ static fromMonths ( months : number ) : Interval {
491+ return new Interval ( months , 0 , BigInt ( 0 ) ) ;
492+ }
493+
494+ /**
495+ * Constructs an `Interval` with specified days.
496+ */
497+ static fromDays ( days : number ) : Interval {
498+ return new Interval ( 0 , days , BigInt ( 0 ) ) ;
499+ }
500+
501+ /**
502+ * Constructs an `Interval` with specified seconds.
503+ */
504+ static fromSeconds ( seconds : number ) : Interval {
505+ if ( ! is . integer ( seconds ) ) {
506+ throw new GoogleError (
507+ `Invalid seconds: ${ seconds } , seconds should be an integral value`
508+ ) ;
509+ }
510+ return new Interval (
511+ 0 ,
512+ 0 ,
513+ BigInt ( Interval . NANOSECONDS_PER_SECOND ) * BigInt ( seconds )
514+ ) ;
515+ }
516+
517+ /**
518+ * Constructs an `Interval` with specified milliseconds.
519+ */
520+ static fromMilliseconds ( milliseconds : number ) : Interval {
521+ if ( ! is . integer ( milliseconds ) ) {
522+ throw new GoogleError (
523+ `Invalid milliseconds: ${ milliseconds } , milliseconds should be an integral value`
524+ ) ;
525+ }
526+ return new Interval (
527+ 0 ,
528+ 0 ,
529+ BigInt ( Interval . NANOSECONDS_PER_MILLISECOND ) * BigInt ( milliseconds )
530+ ) ;
531+ }
532+
533+ /**
534+ * Constructs an `Interval` with specified microseconds.
535+ */
536+ static fromMicroseconds ( microseconds : number ) : Interval {
537+ if ( ! is . integer ( microseconds ) ) {
538+ throw new GoogleError (
539+ `Invalid microseconds: ${ microseconds } , microseconds should be an integral value`
540+ ) ;
541+ }
542+ return new Interval (
543+ 0 ,
544+ 0 ,
545+ BigInt ( Interval . NANOSECONDS_PER_MICROSECOND ) * BigInt ( microseconds )
546+ ) ;
547+ }
548+
549+ /**
550+ * Constructs an `Interval` with specified nanoseconds.
551+ */
552+ static fromNanoseconds ( nanoseconds : bigint ) : Interval {
553+ return new Interval ( 0 , 0 , nanoseconds ) ;
554+ }
555+
556+ /**
557+ * Constructs an Interval from ISO8601 duration format: `P[n]Y[n]M[n]DT[n]H[n]M[n][.fffffffff]S`.
558+ * Only seconds can be fractional, and can have at most 9 digits after decimal point.
559+ * Both '.' and ',' are considered valid decimal point.
560+ */
561+ static fromISO8601 ( isoString : string ) : Interval {
562+ const matcher = Interval . ISO8601_PATTERN . exec ( isoString ) ;
563+ if ( ! matcher ) {
564+ throw new GoogleError ( `Invalid ISO8601 duration string: ${ isoString } ` ) ;
565+ }
566+
567+ const getNullOrDefault = ( groupIdx : number ) : string =>
568+ matcher [ groupIdx ] === undefined ? '0' : matcher [ groupIdx ] ;
569+ const years : number = parseInt ( getNullOrDefault ( 1 ) . replace ( 'Y' , '' ) ) ;
570+ const months : number = parseInt ( getNullOrDefault ( 2 ) . replace ( 'M' , '' ) ) ;
571+ const days : number = parseInt ( getNullOrDefault ( 3 ) . replace ( 'D' , '' ) ) ;
572+ const hours : number = parseInt ( getNullOrDefault ( 5 ) . replace ( 'H' , '' ) ) ;
573+ const minutes : number = parseInt ( getNullOrDefault ( 6 ) . replace ( 'M' , '' ) ) ;
574+ const seconds : Big = Big (
575+ getNullOrDefault ( 7 ) . replace ( 'S' , '' ) . replace ( ',' , '.' )
576+ ) ;
577+
578+ const totalMonths : number = Big ( years )
579+ . mul ( Big ( Interval . MONTHS_PER_YEAR ) )
580+ . add ( Big ( months ) )
581+ . toNumber ( ) ;
582+ if ( ! Number . isSafeInteger ( totalMonths ) ) {
583+ throw new GoogleError (
584+ 'Total months is outside of the range of safe integer'
585+ ) ;
586+ }
587+
588+ const totalNanoseconds = BigInt (
589+ seconds
590+ . add (
591+ Big ( ( BigInt ( hours ) * BigInt ( Interval . SECONDS_PER_HOUR ) ) . toString ( ) )
592+ )
593+ . add (
594+ Big (
595+ ( BigInt ( minutes ) * BigInt ( Interval . SECONDS_PER_MINUTE ) ) . toString ( )
596+ )
597+ )
598+ . mul ( Big ( this . NANOSECONDS_PER_SECOND ) )
599+ . toString ( )
600+ ) ;
601+
602+ return new Interval ( totalMonths , days , totalNanoseconds ) ;
603+ }
604+
605+ /**
606+ * @returns string representation of Interval in ISO8601 duration format: `P[n]Y[n]M[n]DT[n]H[n]M[n][.fffffffff]S`
607+ */
608+ toISO8601 ( ) : string {
609+ if ( this . equals ( Interval . ZERO ) ) {
610+ return 'P0Y' ;
611+ }
612+
613+ // months part is normalized to years and months.
614+ let result = 'P' ;
615+ if ( this . months !== 0 ) {
616+ const years_part : number = Math . trunc (
617+ this . months / Interval . MONTHS_PER_YEAR
618+ ) ;
619+ const months_part : number =
620+ this . months - years_part * Interval . MONTHS_PER_YEAR ;
621+ if ( years_part !== 0 ) {
622+ result += `${ years_part } Y` ;
623+ }
624+ if ( months_part !== 0 ) {
625+ result += `${ months_part } M` ;
626+ }
627+ }
628+
629+ if ( this . days !== 0 ) {
630+ result += `${ this . days } D` ;
631+ }
632+
633+ // Nanoseconds part is normalized to hours, minutes and nanoseconds.
634+ if ( this . nanoseconds !== BigInt ( 0 ) ) {
635+ result += 'T' ;
636+ let nanoseconds : bigint = this . nanoseconds ;
637+ const hours_part : bigint =
638+ nanoseconds /
639+ BigInt ( Interval . NANOSECONDS_PER_SECOND * Interval . SECONDS_PER_HOUR ) ;
640+ nanoseconds =
641+ nanoseconds -
642+ hours_part *
643+ BigInt ( Interval . NANOSECONDS_PER_SECOND * Interval . SECONDS_PER_HOUR ) ;
644+
645+ const minutes_part : bigint =
646+ nanoseconds /
647+ BigInt ( Interval . NANOSECONDS_PER_SECOND * Interval . SECONDS_PER_MINUTE ) ;
648+ nanoseconds =
649+ nanoseconds -
650+ minutes_part *
651+ BigInt ( Interval . NANOSECONDS_PER_SECOND * Interval . SECONDS_PER_MINUTE ) ;
652+ const zero_bigint = BigInt ( 0 ) ;
653+ if ( hours_part !== zero_bigint ) {
654+ result += `${ hours_part } H` ;
655+ }
656+
657+ if ( minutes_part !== zero_bigint ) {
658+ result += `${ minutes_part } M` ;
659+ }
660+
661+ let sign = '' ;
662+ if ( nanoseconds < zero_bigint ) {
663+ sign = '-' ;
664+ nanoseconds = - nanoseconds ;
665+ }
666+
667+ // Nanoseconds are converted to seconds and fractional part.
668+ const seconds_part : bigint =
669+ nanoseconds / BigInt ( Interval . NANOSECONDS_PER_SECOND ) ;
670+ nanoseconds =
671+ nanoseconds - seconds_part * BigInt ( Interval . NANOSECONDS_PER_SECOND ) ;
672+ if ( seconds_part !== zero_bigint || nanoseconds !== zero_bigint ) {
673+ result += `${ sign } ${ seconds_part } ` ;
674+ if ( nanoseconds !== zero_bigint ) {
675+ // Fractional part is kept in a group of 3
676+ // For e.g.: PT0.5S will be normalized to PT0.500S
677+ result += `.${ nanoseconds
678+ . toString ( )
679+ . padStart ( 9 , '0' )
680+ . replace ( / ( 0 { 3 } ) + $ / , '' ) } `;
681+ }
682+ result += 'S' ;
683+ }
684+ }
685+
686+ return result ;
687+ }
688+
689+ equals ( other : Interval ) : boolean {
690+ if ( ! other ) {
691+ return false ;
692+ }
693+
694+ return (
695+ this . months === other . months &&
696+ this . days === other . days &&
697+ this . nanoseconds === other . nanoseconds
698+ ) ;
699+ }
700+
701+ valueOf ( ) : Interval {
702+ return this ;
703+ }
704+
705+ /**
706+ * @returns JSON representation for Interval.
707+ * Interval is represented in ISO8601 duration format string in JSON.
708+ */
709+ toJSON ( ) : string {
710+ return this . toISO8601 ( ) . toString ( ) ;
711+ }
712+ }
713+
397714/**
398715 * @typedef JSONOptions
399716 * @property {boolean } [wrapNumbers=false] Indicates if the numbers should be
@@ -581,6 +898,10 @@ function decode(
581898 }
582899 decoded = JSON . parse ( decoded ) ;
583900 break ;
901+ case spannerClient . spanner . v1 . TypeCode . INTERVAL :
902+ case 'INTERVAL' :
903+ decoded = Interval . fromISO8601 ( decoded ) ;
904+ break ;
584905 case spannerClient . spanner . v1 . TypeCode . ARRAY :
585906 case 'ARRAY' :
586907 decoded = decoded . map ( value => {
@@ -677,6 +998,10 @@ function encodeValue(value: Value): Value {
677998 return value . toString ( ) ;
678999 }
6791000
1001+ if ( value instanceof Interval ) {
1002+ return value . toISO8601 ( ) ;
1003+ }
1004+
6801005 if ( is . object ( value ) ) {
6811006 return JSON . stringify ( value ) ;
6821007 }
@@ -707,6 +1032,7 @@ const TypeCode: {
7071032 bytes : 'BYTES' ,
7081033 json : 'JSON' ,
7091034 jsonb : 'JSON' ,
1035+ interval : 'INTERVAL' ,
7101036 proto : 'PROTO' ,
7111037 enum : 'ENUM' ,
7121038 array : 'ARRAY' ,
@@ -745,6 +1071,7 @@ interface FieldType extends Type {
7451071 * - string
7461072 * - bytes
7471073 * - json
1074+ * - interval
7481075 * - proto
7491076 * - enum
7501077 * - timestamp
@@ -802,6 +1129,10 @@ function getType(value: Value): Type {
8021129 return { type : 'pgOid' } ;
8031130 }
8041131
1132+ if ( value instanceof Interval ) {
1133+ return { type : 'interval' } ;
1134+ }
1135+
8051136 if ( value instanceof ProtoMessage ) {
8061137 return { type : 'proto' , fullName : value . fullName } ;
8071138 }
@@ -978,6 +1309,7 @@ export const codec = {
9781309 ProtoMessage,
9791310 ProtoEnum,
9801311 PGOid,
1312+ Interval,
9811313 convertFieldsToJson,
9821314 decode,
9831315 encode,
0 commit comments