├── .babelrc ├── .flowconfig ├── examples ├── index.js ├── date.js ├── time.js └── dateTime.js ├── src ├── index.js ├── utils │ ├── index.js │ ├── formatter.js │ ├── validator.js │ └── __tests__ │ │ ├── validatorTest.js │ │ └── formatterTests.js ├── date │ ├── index.js │ ├── __snapshots__ │ │ └── index.test.js.snap │ ├── index.test.js │ └── integration.test.js ├── time │ ├── index.js │ ├── __snapshots__ │ │ └── index.test.js.snap │ ├── index.test.js │ └── integration.test.js └── dateTime │ ├── index.js │ ├── __snapshots__ │ └── index.test.js.snap │ ├── integration.test.js │ └── index.test.js ├── resources ├── clean-package-json.js └── prepublish.sh ├── CONTRIBUTING.md ├── .gitignore ├── .travis.yml ├── LICENSE ├── package.json ├── README.md ├── flow-typed └── npm │ └── jest_v22.x.x.js └── rfc3339.txt /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2"], 3 | "plugins": ["transform-flow-strip-types"] 4 | } -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [options] 8 | include_warnings=true 9 | 10 | [lints] 11 | all=warn -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * Copyright (c) 2017, Dirk-Jan Rutten 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | * 9 | */ 10 | 11 | import './date' 12 | import './time' 13 | import './dateTime' 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * Copyright (c) 2017, Dirk-Jan Rutten 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | * 9 | */ 10 | 11 | export { default as GraphQLDate } from './date' 12 | export { default as GraphQLTime } from './time' 13 | export { default as GraphQLDateTime } from './dateTime' 14 | -------------------------------------------------------------------------------- /resources/clean-package-json.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Ensure a clean package.json before deploying so other tools do not 3 | * interpret the built output as requiring any further transformation. 4 | */ 5 | 6 | const fs = require('fs') 7 | 8 | const packageJson = require('../package.json') 9 | 10 | delete packageJson.scripts 11 | delete packageJson.options 12 | delete packageJson.devDependencies 13 | delete packageJson.standard 14 | delete packageJson.jest 15 | 16 | fs.writeFileSync('./package.json', JSON.stringify(packageJson, null, 2)) 17 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * Copyright (c) 2017, Dirk-Jan Rutten 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | * 9 | */ 10 | 11 | export { 12 | serializeTime, 13 | serializeTimeString, 14 | serializeDate, 15 | serializeDateTime, 16 | serializeDateTimeString, 17 | serializeUnixTimestamp, 18 | parseTime, 19 | parseDate, 20 | parseDateTime 21 | } from './formatter' 22 | 23 | export { 24 | validateTime, 25 | validateDate, 26 | validateDateTime, 27 | validateUnixTimestamp, 28 | validateJSDate 29 | } from './validator' 30 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Release on NPM 2 | 3 | *Only core contributors may release to NPM.* 4 | 5 | To release a new version on NPM, first ensure all tests pass with `npm test`, 6 | then use `npm version patch|minor|major` in order to increment the version in 7 | package.json and tag and commit a release. Then `git push && git push --tags` 8 | this change so Travis CI can deploy to NPM. *Do not run `npm publish` directly.* 9 | Once published, add [release notes](https://github.com/excitement-engineer/graphql-iso-date/tags). 10 | Use [semver](http://semver.org/) to determine which version part to increment. 11 | 12 | Example for a patch release: 13 | 14 | ```sh 15 | npm test 16 | npm version patch 17 | git push --follow-tags 18 | ``` 19 | -------------------------------------------------------------------------------- /resources/prepublish.sh: -------------------------------------------------------------------------------- 1 | # This script is adapted from https://github.com/graphql/graphql-js. 2 | 3 | # Publishing to NPM is currently supported by Travis CI, which ensures that all 4 | # tests pass first and the deployed module contains the correct file structure. 5 | # In order to prevent inadvertently circumventing this, we ensure that a CI 6 | # environment exists before continuing. 7 | if [ "$CI" != true ]; then 8 | echo "\n\n\n \033[101;30m Only Travis CI can publish to NPM. \033[0m" 1>&2; 9 | echo " Ensure git is left is a good state by backing out any commits and deleting any tags." 1>&2; 10 | echo " Then read CONTRIBUTING.md to learn how to publish to NPM.\n\n\n" 1>&2; 11 | exit 1; 12 | fi; 13 | 14 | # When Travis CI publishes to NPM, we need to make 15 | # sure that the files are built into dist. 16 | npm run build; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Project ### 2 | 3 | dist/ 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # node-waf configuration 29 | .lock-wscript 30 | 31 | # Compiled binary addons (http://nodejs.org/api/addons.html) 32 | build/Release 33 | 34 | # Dependency directories 35 | node_modules 36 | jspm_packages 37 | 38 | # Optional npm cache directory 39 | .npm 40 | 41 | # Optional REPL history 42 | .node_repl_history 43 | 44 | ### WebStorm ### 45 | 46 | .idea/ 47 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: yarn 3 | node_js: 4 | - '8' 5 | script: 6 | - npm test && npm run coverage 7 | deploy: 8 | provider: npm 9 | skip_cleanup: true 10 | email: dirkjanrutten@gmail.com 11 | api_key: 12 | secure: Tj9s+BqnSFCSyHydsJgnZ1lpxE5LTxUvYwo7U3e10wyVcDGNrQkkCBAEuVYERkysprLg0yAqbTskUsPnfFG4DMc8LYHluk+RYRqd1AgVcUc0wLGmjV8ZU1dIvp9ijQygc6ftjGU8OKABo8rRN8HF09QTAav9Pj3ubC8+Ryq6Z1IFOI8mI5s06SBYgK95m4b88F684jib5G69EN9VILOV5txKfPuP/K+GmRIpmJkc/aoolu4NIKgaBpMG44lBpdSJs5w5nMypsNf9uMqLGj/AMUtXdEEMXcHZOmMeYrPb8DFc7AAXyFNh0lyeCFP9/R3RiM9ACImpgCY7k1yDDBQRWealkiuap1g0YZ0LC8SZ2bfQPauP8FU+PWXkpWvv2inEVXXBIKLlw98B0mZwT1xfMMjo3mFLjjDzQI9CHasAqvDcnc7yK8DRPPetzwfvtwQo+8ieetl3hxvUiBcxNyVNKB5MmrIEOKwiV1Pur6qm4eeBiplx/AqFmuXqbZdguzhEbImwYueToQT03hfme0nIrudRw6hZhbnvzngmw+NCIO/aY4YNfBxD+pVBv35WTos6rZsGd/v8qAtJ8JDobqlXlR/EPtpyo6FpoDd178KS89lgrU4Rfh9MlavxiU9EnJJmhbTD2gBKk5/+kR4uvw3XRSS77jI/dxem9HRu0OM30PM= 13 | on: 14 | tags: true 15 | branch: master 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Dirk-Jan Rutten 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/date.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * Copyright (c) 2017, Dirk-Jan Rutten 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | * 9 | */ 10 | 11 | import { graphql, GraphQLObjectType, GraphQLSchema } from 'graphql' 12 | import { GraphQLDate } from '../dist' 13 | 14 | /** 15 | * Example of the GraphQLDate scalar. 16 | */ 17 | const schema = new GraphQLSchema({ 18 | query: new GraphQLObjectType({ 19 | name: 'Query', 20 | fields: { 21 | today: { 22 | type: GraphQLDate, 23 | // Resolve can take a javascript Date 24 | resolve: (): Date => new Date() 25 | }, 26 | birthdate: { 27 | type: GraphQLDate, 28 | // Resolve can take a date string. 29 | resolve: (): string => '1991-12-24' 30 | }, 31 | input: { 32 | type: GraphQLDate, 33 | args: { 34 | date: { 35 | type: GraphQLDate 36 | } 37 | }, 38 | // When passed as argument the date string is parsed to a javascript Date. 39 | resolve: (_, input: { date: Date }): Date => input.date 40 | } 41 | } 42 | }) 43 | }) 44 | 45 | const query = ` 46 | query DateTest($date: Date!) { 47 | today 48 | birthdate 49 | input(date: $date) 50 | } 51 | ` 52 | 53 | const variables = { date: '2017-10-01' } 54 | 55 | graphql(schema, query, null, null, variables).then(data => { 56 | console.log('\n\nDate Scalar Example:\n') 57 | console.log(JSON.stringify(data, null, 2)) 58 | }) 59 | -------------------------------------------------------------------------------- /examples/time.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * Copyright (c) 2017, Dirk-Jan Rutten 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | * 9 | */ 10 | 11 | import { graphql, GraphQLObjectType, GraphQLSchema } from 'graphql' 12 | import { GraphQLTime } from '../dist' 13 | 14 | /** 15 | * Example of the GraphQLTime scalar. 16 | */ 17 | const schema = new GraphQLSchema({ 18 | query: new GraphQLObjectType({ 19 | name: 'Query', 20 | fields: { 21 | time: { 22 | type: GraphQLTime, 23 | // Resolve can take a javascript Date. 24 | resolve: (): Date => new Date() 25 | }, 26 | openingNYSE: { 27 | type: GraphQLTime, 28 | // Resolve can take a time string. 29 | resolve: (): string => '14:30:00Z' 30 | }, 31 | timezone: { 32 | type: GraphQLTime, 33 | // Resolve takes a time string with a timezone and shifts it to UTC. 34 | resolve: (): string => '14:30:00+01:00' 35 | }, 36 | input: { 37 | type: GraphQLTime, 38 | args: { 39 | time: { 40 | type: GraphQLTime 41 | } 42 | }, 43 | // When passed as argument the time string is parsed to a javascript Date. 44 | resolve: (_, input: { time: Date }): Date => input.time 45 | } 46 | } 47 | }) 48 | }) 49 | 50 | const query = ` 51 | query TimeTest($time: Time) { 52 | time 53 | openingNYSE 54 | timezone 55 | input(time: $time) 56 | } 57 | ` 58 | 59 | const variables = { time: '11:34:21.345Z' } 60 | 61 | graphql(schema, query, null, null, variables).then(data => { 62 | console.log('\n\nTime Scalar Example:\n') 63 | console.log(JSON.stringify(data, null, 2)) 64 | }) 65 | -------------------------------------------------------------------------------- /examples/dateTime.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * Copyright (c) 2017, Dirk-Jan Rutten 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | * 9 | */ 10 | 11 | import { graphql, GraphQLObjectType, GraphQLSchema } from 'graphql' 12 | import { GraphQLDateTime } from '../dist' 13 | 14 | /** 15 | * Example of the GraphQLDateTime scalar. 16 | */ 17 | const schema = new GraphQLSchema({ 18 | query: new GraphQLObjectType({ 19 | name: 'Query', 20 | fields: { 21 | now: { 22 | type: GraphQLDateTime, 23 | // Resolve can take a javascript Date. 24 | resolve: (): Date => new Date() 25 | }, 26 | instant: { 27 | type: GraphQLDateTime, 28 | // Resolve can take a date-time string. 29 | resolve: (): string => '2017-01-27T21:46:33.6756Z' 30 | }, 31 | timezone: { 32 | type: GraphQLDateTime, 33 | // Resolve takes a date-time string with a timezone and shifts it to UTC. 34 | resolve: (): string => '2017-01-07T00:00:00.1+01:20' 35 | }, 36 | unix: { 37 | type: GraphQLDateTime, 38 | // Resolve can take a timestamp. 39 | resolve: (): number => 344555632.543 40 | }, 41 | input: { 42 | type: GraphQLDateTime, 43 | args: { 44 | dateTime: { 45 | type: GraphQLDateTime 46 | } 47 | }, 48 | // When passed as argument the date-time string is parsed to a javascript Date. 49 | resolve: (_, input: { dateTime: Date }): Date => input.dateTime 50 | } 51 | } 52 | }) 53 | }) 54 | 55 | const query = ` 56 | query DateTimeTest($dateTime: DateTime) { 57 | now 58 | unix 59 | instant 60 | timezone 61 | input(dateTime: $dateTime) 62 | } 63 | ` 64 | 65 | const variables = { dateTime: '2010-01-11T11:34:21Z' } 66 | 67 | graphql(schema, query, null, null, variables).then(data => { 68 | console.log('\n\nDateTime Scalar Example:\n') 69 | console.log(JSON.stringify(data, null, 2)) 70 | }) 71 | -------------------------------------------------------------------------------- /src/date/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * Copyright (c) 2017, Dirk-Jan Rutten 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | * 9 | */ 10 | 11 | import { GraphQLScalarType, Kind } from 'graphql' 12 | import type {GraphQLScalarTypeConfig} from "graphql";// eslint-disable-line 13 | import { validateDate, validateJSDate, serializeDate, parseDate } from '../utils' 14 | 15 | /** 16 | * An RFC 3339 compliant date scalar. 17 | * 18 | * Input: 19 | * This scalar takes an RFC 3339 date string as input and 20 | * parses it to a javascript Date. 21 | * 22 | * Output: 23 | * This scalar serializes javascript Dates and 24 | * RFC 3339 date strings to RFC 3339 date strings. 25 | */ 26 | const config: GraphQLScalarTypeConfig = { 27 | name: 'Date', 28 | description: 'A date string, such as 2007-12-03, compliant with the `full-date` ' + 29 | 'format outlined in section 5.6 of the RFC 3339 profile of the ' + 30 | 'ISO 8601 standard for representation of dates and times using ' + 31 | 'the Gregorian calendar.', 32 | serialize (value) { 33 | if (value instanceof Date) { 34 | if (validateJSDate(value)) { 35 | return serializeDate(value) 36 | } 37 | throw new TypeError('Date cannot represent an invalid Date instance') 38 | } else if (typeof value === 'string' || value instanceof String) { 39 | if (validateDate(value)) { 40 | return value 41 | } 42 | throw new TypeError( 43 | `Date cannot represent an invalid date-string ${value}.` 44 | ) 45 | } else { 46 | throw new TypeError( 47 | 'Date cannot represent a non string, or non Date type ' + 48 | JSON.stringify(value) 49 | ) 50 | } 51 | }, 52 | parseValue (value) { 53 | if (!(typeof value === 'string' || value instanceof String)) { 54 | throw new TypeError( 55 | `Date cannot represent non string type ${JSON.stringify(value)}` 56 | ) 57 | } 58 | 59 | if (validateDate(value)) { 60 | return parseDate(value) 61 | } 62 | throw new TypeError( 63 | `Date cannot represent an invalid date-string ${value}.` 64 | ) 65 | }, 66 | parseLiteral (ast) { 67 | if (ast.kind !== Kind.STRING) { 68 | throw new TypeError( 69 | `Date cannot represent non string type ${String(ast.value != null ? ast.value : null)}` 70 | ) 71 | } 72 | const { value } = ast 73 | if (validateDate(value)) { 74 | return parseDate(value) 75 | } 76 | throw new TypeError( 77 | `Date cannot represent an invalid date-string ${String(value)}.` 78 | ) 79 | } 80 | } 81 | 82 | export default new GraphQLScalarType(config) 83 | -------------------------------------------------------------------------------- /src/time/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * Copyright (c) 2017, Dirk-Jan Rutten 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | * 9 | */ 10 | 11 | import { GraphQLScalarType, Kind } from 'graphql' 12 | import type {GraphQLScalarTypeConfig} from 'graphql' // eslint-disable-line 13 | import { 14 | validateTime, 15 | validateJSDate, 16 | serializeTime, 17 | serializeTimeString, 18 | parseTime 19 | } from '../utils' 20 | 21 | /** 22 | * An RFC 3339 compliant time scalar. 23 | * 24 | * Input: 25 | * This scalar takes an RFC 3339 time string as input and 26 | * parses it to a javascript Date (with a year-month-day relative 27 | * to the current day). 28 | * 29 | * Output: 30 | * This scalar serializes javascript Dates and 31 | * RFC 3339 time strings to RFC 3339 UTC time strings. 32 | */ 33 | const config: GraphQLScalarTypeConfig = { 34 | name: 'Time', 35 | description: 'A time string at UTC, such as 10:15:30Z, compliant with ' + 36 | 'the `full-time` format outlined in section 5.6 of the RFC 3339' + 37 | 'profile of the ISO 8601 standard for representation of dates and ' + 38 | 'times using the Gregorian calendar.', 39 | serialize (value: mixed): string { 40 | if (value instanceof Date) { 41 | if (validateJSDate(value)) { 42 | return serializeTime(value) 43 | } 44 | throw new TypeError('Time cannot represent an invalid Date instance') 45 | } else if (typeof value === 'string' || value instanceof String) { 46 | if (validateTime(value)) { 47 | return serializeTimeString(value) 48 | } 49 | throw new TypeError( 50 | `Time cannot represent an invalid time-string ${value}.` 51 | ) 52 | } else { 53 | throw new TypeError( 54 | 'Time cannot be serialized from a non string, ' + 55 | 'or non Date type ' + JSON.stringify(value) 56 | ) 57 | } 58 | }, 59 | parseValue (value: mixed): Date { 60 | if (!(typeof value === 'string' || value instanceof String)) { 61 | throw new TypeError( 62 | `Time cannot represent non string type ${JSON.stringify(value)}` 63 | ) 64 | } 65 | 66 | if (validateTime(value)) { 67 | return parseTime(value) 68 | } 69 | throw new TypeError( 70 | `Time cannot represent an invalid time-string ${value}.` 71 | ) 72 | }, 73 | parseLiteral (ast): ?Date { 74 | if (ast.kind !== Kind.STRING) { 75 | throw new TypeError( 76 | `Time cannot represent non string type ${String(ast.value != null ? ast.value : null)}` 77 | ) 78 | } 79 | const value = ast.value 80 | if (validateTime(value)) { 81 | return parseTime(value) 82 | } 83 | throw new TypeError( 84 | `Time cannot represent an invalid time-string ${String(value)}.` 85 | ) 86 | } 87 | } 88 | 89 | export default new GraphQLScalarType(config) 90 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-iso-date", 3 | "version": "3.6.1", 4 | "description": "A set of RFC 3339 compliant date/time GraphQL scalar types.", 5 | "main": "dist/index.js", 6 | "homepage": "https://github.com/excitement-engineer/graphql-iso-date", 7 | "bugs": { 8 | "url": "https://github.com/excitement-engineer/graphql-iso-date/issues" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/excitement-engineer/graphql-iso-date.git" 13 | }, 14 | "scripts": { 15 | "examples": "npm run build:babel && npm run build:flow && babel-node examples/index.js", 16 | "build": "npm run build:clean && npm run build:babel && npm run build:flow && npm run build:package-json", 17 | "build:clean": "rm -rf ./dist", 18 | "build:package-json": "node ./resources/clean-package-json.js", 19 | "build:babel": "babel src --ignore __tests__,*.test.js --out-dir dist/ ", 20 | "build:flow": "find ./src -name '*.js' -not -path '*/__tests__*' -not -name '*.test.js' | while read filepath; do cp $filepath `echo $filepath | sed 's/\\/src\\//\\/dist\\//g'`.flow; done", 21 | "test": "npm run examples && npm run testonly && npm run check && npm run lint", 22 | "testonly": "jest", 23 | "coverage": "codecov", 24 | "watch": "jest --watch", 25 | "check": "flow check", 26 | "prepublishOnly": ". ./resources/prepublish.sh", 27 | "lint": "standard" 28 | }, 29 | "keywords": [ 30 | "GraphQL", 31 | "Scalar", 32 | "Date", 33 | "Node", 34 | "ISO" 35 | ], 36 | "author": "Dirk-Jan Rutten ", 37 | "license": "MIT", 38 | "files": [ 39 | "dist", 40 | "README.md" 41 | ], 42 | "peerDependencies": { 43 | "graphql": "^0.5.0 || ^0.6.0 || ^0.7.0 || ^0.8.0-b || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0" 44 | }, 45 | "devDependencies": { 46 | "babel-cli": "^6.26.0", 47 | "babel-eslint": "^9.0.0", 48 | "babel-jest": "^23.6.0", 49 | "babel-plugin-transform-flow-strip-types": "^6.22.0", 50 | "babel-preset-es2015": "^6.24.1", 51 | "babel-preset-stage-2": "^6.24.1", 52 | "codecov": "^3.0.0", 53 | "eslint-plugin-flowtype": "^2.50.0", 54 | "flow-bin": "^0.81.0", 55 | "graphql": "^14.0.2", 56 | "jest": "^23.6.0", 57 | "mockdate": "^2.0.2", 58 | "standard": "^12.0.1" 59 | }, 60 | "standard": { 61 | "parser": "babel-eslint", 62 | "ignore": [ 63 | "flow-typed/" 64 | ], 65 | "plugins": [ 66 | "flowtype" 67 | ], 68 | "globals": [ 69 | "describe", 70 | "before", 71 | "beforeEach", 72 | "beforeAll", 73 | "after", 74 | "afterEach", 75 | "afterAll", 76 | "it", 77 | "expect" 78 | ] 79 | }, 80 | "jest": { 81 | "testEnvironment": "node", 82 | "coverageDirectory": "./coverage/", 83 | "collectCoverage": true, 84 | "coverageThreshold": { 85 | "global": { 86 | "branches": 100, 87 | "functions": 100, 88 | "lines": 100, 89 | "statements": 100 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/date/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`GraphQLDate has a description 1`] = `"A date string, such as 2007-12-03, compliant with the \`full-date\` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar."`; 4 | 5 | exports[`GraphQLDate literial parsing errors when parsing invalid literal {"kind": "Document"} 1`] = `"Date cannot represent non string type null"`; 6 | 7 | exports[`GraphQLDate literial parsing errors when parsing invalid literal {"kind": "FloatValue", "value": "5"} 1`] = `"Date cannot represent non string type 5"`; 8 | 9 | exports[`GraphQLDate literial parsing errors when parsing invalid literal {"kind": "StringValue", "value": "2015-02-29"} 1`] = `"Date cannot represent an invalid date-string 2015-02-29."`; 10 | 11 | exports[`GraphQLDate literial parsing errors when parsing invalid literal {"kind": "StringValue", "value": "invalid date"} 1`] = `"Date cannot represent an invalid date-string invalid date."`; 12 | 13 | exports[`GraphQLDate serialization throws an error when serializing an invalid date-string "2015-02-29" 1`] = `"Date cannot represent an invalid date-string 2015-02-29."`; 14 | 15 | exports[`GraphQLDate serialization throws an error when serializing an invalid date-string "invalid date" 1`] = `"Date cannot represent an invalid date-string invalid date."`; 16 | 17 | exports[`GraphQLDate serialization throws error when serializing [] 1`] = `"Date cannot represent a non string, or non Date type []"`; 18 | 19 | exports[`GraphQLDate serialization throws error when serializing {} 1`] = `"Date cannot represent a non string, or non Date type {}"`; 20 | 21 | exports[`GraphQLDate serialization throws error when serializing invalid javascript Date 1`] = `"Date cannot represent an invalid Date instance"`; 22 | 23 | exports[`GraphQLDate serialization throws error when serializing null 1`] = `"Date cannot represent a non string, or non Date type null"`; 24 | 25 | exports[`GraphQLDate serialization throws error when serializing true 1`] = `"Date cannot represent a non string, or non Date type true"`; 26 | 27 | exports[`GraphQLDate serialization throws error when serializing undefined 1`] = `"Date cannot represent a non string, or non Date type undefined"`; 28 | 29 | exports[`GraphQLDate value parsing throws an error parsing an invalid datetime-string "2015-02-29" 1`] = `"Date cannot represent an invalid date-string 2015-02-29."`; 30 | 31 | exports[`GraphQLDate value parsing throws an error parsing an invalid datetime-string "invalid date" 1`] = `"Date cannot represent an invalid date-string invalid date."`; 32 | 33 | exports[`GraphQLDate value parsing throws an error when parsing [] 1`] = `"Date cannot represent non string type []"`; 34 | 35 | exports[`GraphQLDate value parsing throws an error when parsing {} 1`] = `"Date cannot represent non string type {}"`; 36 | 37 | exports[`GraphQLDate value parsing throws an error when parsing 4566 1`] = `"Date cannot represent non string type 4566"`; 38 | 39 | exports[`GraphQLDate value parsing throws an error when parsing null 1`] = `"Date cannot represent non string type null"`; 40 | 41 | exports[`GraphQLDate value parsing throws an error when parsing true 1`] = `"Date cannot represent non string type true"`; 42 | -------------------------------------------------------------------------------- /src/dateTime/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * Copyright (c) 2017, Dirk-Jan Rutten 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | * 9 | */ 10 | 11 | import { GraphQLScalarType, Kind } from 'graphql' 12 | import type {GraphQLScalarTypeConfig} from 'graphql' // eslint-disable-line 13 | import { 14 | validateDateTime, 15 | validateUnixTimestamp, 16 | validateJSDate, 17 | serializeDateTime, 18 | serializeDateTimeString, 19 | serializeUnixTimestamp, 20 | parseDateTime 21 | } from '../utils' 22 | 23 | /** 24 | * An RFC 3339 compliant date-time scalar. 25 | * 26 | * Input: 27 | * This scalar takes an RFC 3339 date-time string as input and 28 | * parses it to a javascript Date. 29 | * 30 | * Output: 31 | * This scalar serializes javascript Dates, 32 | * RFC 3339 date-time strings and unix timestamps 33 | * to RFC 3339 UTC date-time strings. 34 | */ 35 | const config: GraphQLScalarTypeConfig = { 36 | name: 'DateTime', 37 | description: 'A date-time string at UTC, such as 2007-12-03T10:15:30Z, ' + 38 | 'compliant with the `date-time` format outlined in section 5.6 of ' + 39 | 'the RFC 3339 profile of the ISO 8601 standard for representation ' + 40 | 'of dates and times using the Gregorian calendar.', 41 | serialize (value) { 42 | if (value instanceof Date) { 43 | if (validateJSDate(value)) { 44 | return serializeDateTime(value) 45 | } 46 | throw new TypeError('DateTime cannot represent an invalid Date instance') 47 | } else if (typeof value === 'string' || value instanceof String) { 48 | if (validateDateTime(value)) { 49 | return serializeDateTimeString(value) 50 | } 51 | throw new TypeError( 52 | `DateTime cannot represent an invalid date-time-string ${value}.` 53 | ) 54 | } else if (typeof value === 'number' || value instanceof Number) { 55 | if (validateUnixTimestamp(value)) { 56 | return serializeUnixTimestamp(value) 57 | } 58 | throw new TypeError( 59 | 'DateTime cannot represent an invalid Unix timestamp ' + value 60 | ) 61 | } else { 62 | throw new TypeError( 63 | 'DateTime cannot be serialized from a non string, ' + 64 | 'non numeric or non Date type ' + JSON.stringify(value) 65 | ) 66 | } 67 | }, 68 | parseValue (value) { 69 | if (!(typeof value === 'string' || value instanceof String)) { 70 | throw new TypeError( 71 | `DateTime cannot represent non string type ${JSON.stringify(value)}` 72 | ) 73 | } 74 | 75 | if (validateDateTime(value)) { 76 | return parseDateTime(value) 77 | } 78 | throw new TypeError( 79 | `DateTime cannot represent an invalid date-time-string ${value}.` 80 | ) 81 | }, 82 | parseLiteral (ast) { 83 | if (ast.kind !== Kind.STRING) { 84 | throw new TypeError( 85 | `DateTime cannot represent non string type ${String(ast.value != null ? ast.value : null)}` 86 | ) 87 | } 88 | const { value } = ast 89 | if (validateDateTime(value)) { 90 | return parseDateTime(value) 91 | } 92 | throw new TypeError( 93 | `DateTime cannot represent an invalid date-time-string ${String(value)}.` 94 | ) 95 | } 96 | } 97 | 98 | export default new GraphQLScalarType(config) 99 | -------------------------------------------------------------------------------- /src/time/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`GraphQLTime has a description 1`] = `"A time string at UTC, such as 10:15:30Z, compliant with the \`full-time\` format outlined in section 5.6 of the RFC 3339profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar."`; 4 | 5 | exports[`GraphQLTime literial parsing errors when parsing invalid literal {"kind": "StringValue", "value": "00:00:00.45+01"} 1`] = `"Time cannot represent an invalid time-string 00:00:00.45+01."`; 6 | 7 | exports[`GraphQLTime literial parsing errors when parsing invalid literal {"kind": "StringValue", "value": "00:00:00.45+0130"} 1`] = `"Time cannot represent an invalid time-string 00:00:00.45+0130."`; 8 | 9 | exports[`GraphQLTime literial parsing errors when parsing invalid literal {"kind": "StringValue", "value": "10:30:02.Z"} 1`] = `"Time cannot represent an invalid time-string 10:30:02.Z."`; 10 | 11 | exports[`GraphQLTime literial parsing errors when parsing invalid literal {"kind": "StringValue", "value": "2016-01-01T00:00:00.223Z"} 1`] = `"Time cannot represent an invalid time-string 2016-01-01T00:00:00.223Z."`; 12 | 13 | exports[`GraphQLTime literial parsing errors when parsing invalid literal {"kind": "StringValue", "value": "Invalid date"} 1`] = `"Time cannot represent an invalid time-string Invalid date."`; 14 | 15 | exports[`GraphQLTime serialization throws an error when serializing an invalid date-string "00:00:00.45+01" 1`] = `"Time cannot represent an invalid time-string 00:00:00.45+01."`; 16 | 17 | exports[`GraphQLTime serialization throws an error when serializing an invalid date-string "00:00:00.45+0130" 1`] = `"Time cannot represent an invalid time-string 00:00:00.45+0130."`; 18 | 19 | exports[`GraphQLTime serialization throws an error when serializing an invalid date-string "10:30:02.Z" 1`] = `"Time cannot represent an invalid time-string 10:30:02.Z."`; 20 | 21 | exports[`GraphQLTime serialization throws an error when serializing an invalid date-string "2016-01-01T00:00:00.223Z" 1`] = `"Time cannot represent an invalid time-string 2016-01-01T00:00:00.223Z."`; 22 | 23 | exports[`GraphQLTime serialization throws an error when serializing an invalid date-string "Invalid date" 1`] = `"Time cannot represent an invalid time-string Invalid date."`; 24 | 25 | exports[`GraphQLTime serialization throws error when serializing [] 1`] = `"Time cannot be serialized from a non string, or non Date type []"`; 26 | 27 | exports[`GraphQLTime serialization throws error when serializing {} 1`] = `"Time cannot be serialized from a non string, or non Date type {}"`; 28 | 29 | exports[`GraphQLTime serialization throws error when serializing invalid date 1`] = `"Time cannot represent an invalid Date instance"`; 30 | 31 | exports[`GraphQLTime serialization throws error when serializing null 1`] = `"Time cannot be serialized from a non string, or non Date type null"`; 32 | 33 | exports[`GraphQLTime serialization throws error when serializing true 1`] = `"Time cannot be serialized from a non string, or non Date type true"`; 34 | 35 | exports[`GraphQLTime serialization throws error when serializing undefined 1`] = `"Time cannot be serialized from a non string, or non Date type undefined"`; 36 | 37 | exports[`GraphQLTime value parsing throws an error parsing an invalid time-string "00:00:00.45+01" 1`] = `"Time cannot represent an invalid time-string 00:00:00.45+01."`; 38 | 39 | exports[`GraphQLTime value parsing throws an error parsing an invalid time-string "00:00:00.45+0130" 1`] = `"Time cannot represent an invalid time-string 00:00:00.45+0130."`; 40 | 41 | exports[`GraphQLTime value parsing throws an error parsing an invalid time-string "10:30:02.Z" 1`] = `"Time cannot represent an invalid time-string 10:30:02.Z."`; 42 | 43 | exports[`GraphQLTime value parsing throws an error parsing an invalid time-string "2016-01-01T00:00:00.223Z" 1`] = `"Time cannot represent an invalid time-string 2016-01-01T00:00:00.223Z."`; 44 | 45 | exports[`GraphQLTime value parsing throws an error parsing an invalid time-string "Invalid date" 1`] = `"Time cannot represent an invalid time-string Invalid date."`; 46 | 47 | exports[`GraphQLTime value parsing throws an error when parsing [] 1`] = `"Time cannot represent non string type []"`; 48 | 49 | exports[`GraphQLTime value parsing throws an error when parsing {} 1`] = `"Time cannot represent non string type {}"`; 50 | 51 | exports[`GraphQLTime value parsing throws an error when parsing 4566 1`] = `"Time cannot represent non string type 4566"`; 52 | 53 | exports[`GraphQLTime value parsing throws an error when parsing null 1`] = `"Time cannot represent non string type null"`; 54 | 55 | exports[`GraphQLTime value parsing throws an error when parsing true 1`] = `"Time cannot represent non string type true"`; 56 | -------------------------------------------------------------------------------- /src/date/index.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * Copyright (c) 2017, Dirk-Jan Rutten 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | * 9 | */ 10 | 11 | import GraphQLDate from './' 12 | import { Kind } from 'graphql' 13 | // flowlint-next-line untyped-import:off 14 | import { stringify } from 'jest-matcher-utils' 15 | 16 | const invalidDates = [ 17 | 'invalid date', 18 | '2015-02-29' 19 | ] 20 | 21 | const validDates = [ 22 | [ '2016-12-17', new Date(Date.UTC(2016, 11, 17)) ], 23 | [ '2016-02-01', new Date(Date.UTC(2016, 1, 1)) ] 24 | ] 25 | 26 | describe('GraphQLDate', () => { 27 | it('has a description', () => { 28 | expect(GraphQLDate.description).toMatchSnapshot() 29 | }) 30 | 31 | describe('serialization', () => { 32 | [ 33 | {}, 34 | [], 35 | null, 36 | undefined, 37 | true 38 | ].forEach(invalidInput => { 39 | it(`throws error when serializing ${stringify(invalidInput)}`, () => { 40 | expect(() => 41 | GraphQLDate.serialize(invalidInput) 42 | ).toThrowErrorMatchingSnapshot() 43 | }) 44 | }); 45 | 46 | [ 47 | [ new Date(Date.UTC(2016, 11, 17, 14)), '2016-12-17' ], 48 | [ new Date(Date.UTC(2016, 0, 1, 14, 48, 10, 3)), '2016-01-01' ], 49 | [ new Date(Date.UTC(2016, 0, 1)), '2016-01-01' ] 50 | ].forEach(([ value, expected ]) => { 51 | it(`serializes javascript Date ${stringify(value)} into ${stringify(expected)}`, () => { 52 | expect( 53 | GraphQLDate.serialize(value) 54 | ).toEqual(expected) 55 | }) 56 | }) 57 | 58 | it(`throws error when serializing invalid javascript Date`, () => { 59 | expect(() => 60 | GraphQLDate.serialize(new Date('invalid date')) 61 | ).toThrowErrorMatchingSnapshot() 62 | }) 63 | 64 | // Serializes from date string 65 | validDates.forEach(([value]) => { 66 | it(`serializes date-string ${value}`, () => { 67 | expect( 68 | GraphQLDate.serialize(value) 69 | ).toEqual(value) 70 | }) 71 | }) 72 | 73 | invalidDates.forEach(dateString => { 74 | it(`throws an error when serializing an invalid date-string ${stringify(dateString)}`, () => { 75 | expect(() => 76 | GraphQLDate.serialize(dateString) 77 | ).toThrowErrorMatchingSnapshot() 78 | }) 79 | }) 80 | }) 81 | 82 | describe('value parsing', () => { 83 | validDates.forEach(([ value, expected ]) => { 84 | it(`parses date-string ${stringify(value)} into javascript Date ${stringify(expected)}`, () => { 85 | expect( 86 | GraphQLDate.parseValue(value) 87 | ).toEqual(expected) 88 | }) 89 | }); 90 | 91 | [ 92 | 4566, 93 | {}, 94 | [], 95 | true, 96 | null 97 | ].forEach(invalidInput => { 98 | it(`throws an error when parsing ${stringify(invalidInput)}`, () => { 99 | expect(() => 100 | GraphQLDate.parseValue(invalidInput) 101 | ).toThrowErrorMatchingSnapshot() 102 | }) 103 | }) 104 | 105 | invalidDates.forEach(dateString => { 106 | it(`throws an error parsing an invalid datetime-string ${stringify(dateString)}`, () => { 107 | expect(() => 108 | GraphQLDate.parseValue(dateString) 109 | ).toThrowErrorMatchingSnapshot() 110 | }) 111 | }) 112 | }) 113 | 114 | describe('literial parsing', () => { 115 | validDates.forEach(([ value, expected ]) => { 116 | const literal = { 117 | kind: Kind.STRING, value 118 | } 119 | 120 | it(`parses literal ${stringify(literal)} into javascript Date ${stringify(expected)}`, () => { 121 | expect( 122 | GraphQLDate.parseLiteral(literal) 123 | ).toEqual(expected) 124 | }) 125 | }) 126 | 127 | invalidDates.forEach(value => { 128 | const invalidLiteral = { 129 | kind: Kind.STRING, value 130 | } 131 | it(`errors when parsing invalid literal ${stringify(invalidLiteral)}`, () => { 132 | expect(() => 133 | GraphQLDate.parseLiteral(invalidLiteral) 134 | ).toThrowErrorMatchingSnapshot() 135 | }) 136 | }); 137 | 138 | [ 139 | { 140 | kind: Kind.FLOAT, value: '5' 141 | }, 142 | ({ 143 | kind: Kind.DOCUMENT 144 | // flowlint-next-line unclear-type:off 145 | }: any) 146 | ].forEach(literal => { 147 | it(`errors when parsing invalid literal ${stringify(literal)}`, () => { 148 | expect(() => 149 | GraphQLDate.parseLiteral(literal) 150 | ).toThrowErrorMatchingSnapshot() 151 | }) 152 | }) 153 | }) 154 | }) 155 | -------------------------------------------------------------------------------- /src/utils/formatter.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * Copyright (c) 2017, Dirk-Jan Rutten 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | * 9 | */ 10 | 11 | // Parses an RFC 3339 compliant time-string into a Date. 12 | // It does this by combining the current date with the time-string 13 | // to create a new Date instance. 14 | // 15 | // Example: 16 | // Suppose the current date is 2016-01-01, then 17 | // parseTime('11:00:12Z') parses to a Date corresponding to 18 | // 2016-01-01T11:00:12Z. 19 | export const parseTime = (time: string): Date => { 20 | const currentDateString = new Date().toISOString() 21 | return new Date(currentDateString.substr(0, currentDateString.indexOf('T') + 1) + time) 22 | } 23 | 24 | // Serializes a Date into an RFC 3339 compliant time-string in the 25 | // format hh:mm:ss.sssZ. 26 | export const serializeTime = (date: Date): string => { 27 | const dateTimeString = date.toISOString() 28 | return dateTimeString.substr(dateTimeString.indexOf('T') + 1) 29 | } 30 | 31 | // Serializes an RFC 3339 compliant time-string by shifting 32 | // it to UTC. 33 | export const serializeTimeString = (time: string): string => { 34 | // If already formatted to UTC then return the time string 35 | if (time.indexOf('Z') !== -1) { 36 | return time 37 | } else { 38 | // These are time-strings with timezone information, 39 | // these need to be shifted to UTC. 40 | 41 | // Convert to UTC time string in 42 | // format hh:mm:ss.sssZ. 43 | const date = parseTime(time) 44 | let timeUTC = serializeTime(date) 45 | 46 | // Regex to look for fractional second part in time string 47 | // such as 00:00:00.345+01:00 48 | const regexFracSec = /\.\d{1,}/ 49 | 50 | // Retrieve the fractional second part of the time 51 | // string if it exists. 52 | const fractionalPart = time.match(regexFracSec) 53 | if (fractionalPart == null) { 54 | // These are time-strings without the fractional 55 | // seconds. So we remove them from the UTC time-string. 56 | timeUTC = timeUTC.replace(regexFracSec, '') 57 | return timeUTC 58 | } else { 59 | // These are time-string with fractional seconds. 60 | // Make sure that we inject the fractional 61 | // second part back in. The `timeUTC` variable 62 | // has millisecond precision, we may want more or less 63 | // depending on the string that was passed. 64 | timeUTC = timeUTC.replace(regexFracSec, fractionalPart[0]) 65 | return timeUTC 66 | } 67 | } 68 | } 69 | 70 | // Parses an RFC 3339 compliant date-string into a Date. 71 | // 72 | // Example: 73 | // parseDate('2016-01-01') parses to a Date corresponding to 74 | // 2016-01-01T00:00:00.000Z. 75 | export const parseDate = (date: string): Date => { 76 | return new Date(date) 77 | } 78 | 79 | // Serializes a Date into a RFC 3339 compliant date-string 80 | // in the format YYYY-MM-DD. 81 | export const serializeDate = (date: Date): string => { 82 | return date.toISOString().split('T')[0] 83 | } 84 | 85 | // Parses an RFC 3339 compliant date-time-string into a Date. 86 | export const parseDateTime = (dateTime: string): Date => { 87 | return new Date(dateTime) 88 | } 89 | 90 | // Serializes a Date into an RFC 3339 compliant date-time-string 91 | // in the format YYYY-MM-DDThh:mm:ss.sssZ. 92 | export const serializeDateTime = (dateTime: Date): string => { 93 | return dateTime.toISOString() 94 | } 95 | 96 | // Serializes an RFC 3339 compliant date-time-string by shifting 97 | // it to UTC. 98 | export const serializeDateTimeString = (dateTime: string): string => { 99 | // If already formatted to UTC then return the time string 100 | if (dateTime.indexOf('Z') !== -1) { 101 | return dateTime 102 | } else { 103 | // These are time-strings with timezone information, 104 | // these need to be shifted to UTC. 105 | 106 | // Convert to UTC time string in 107 | // format YYYY-MM-DDThh:mm:ss.sssZ. 108 | let dateTimeUTC = (new Date(dateTime)).toISOString() 109 | 110 | // Regex to look for fractional second part in date-time string 111 | const regexFracSec = /\.\d{1,}/ 112 | 113 | // Retrieve the fractional second part of the time 114 | // string if it exists. 115 | const fractionalPart = dateTime.match(regexFracSec) 116 | if (fractionalPart == null) { 117 | // The date-time-string has no fractional part, 118 | // so we remove it from the dateTimeUTC variable. 119 | dateTimeUTC = dateTimeUTC.replace(regexFracSec, '') 120 | return dateTimeUTC 121 | } else { 122 | // These are datetime-string with fractional seconds. 123 | // Make sure that we inject the fractional 124 | // second part back in. The `dateTimeUTC` variable 125 | // has millisecond precision, we may want more or less 126 | // depending on the string that was passed. 127 | dateTimeUTC = dateTimeUTC.replace(regexFracSec, fractionalPart[0]) 128 | return dateTimeUTC 129 | } 130 | } 131 | } 132 | 133 | // Serializes a Unix timestamp to an RFC 3339 compliant date-time-string 134 | // in the format YYYY-MM-DDThh:mm:ss.sssZ 135 | export const serializeUnixTimestamp = (timestamp: number): string => { 136 | return new Date(timestamp * 1000).toISOString() 137 | } 138 | -------------------------------------------------------------------------------- /src/date/integration.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * Copyright (c) 2018, Dirk-Jan Rutten 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | * 9 | */ 10 | 11 | import { graphql, GraphQLObjectType, GraphQLSchema, GraphQLError } from 'graphql' 12 | import GraphQLDate from './' 13 | 14 | const schema = new GraphQLSchema({ 15 | query: new GraphQLObjectType({ 16 | name: 'Query', 17 | fields: { 18 | validDate: { 19 | type: GraphQLDate, 20 | resolve: () => new Date('2016-05-02') 21 | }, 22 | validDateString: { 23 | type: GraphQLDate, 24 | resolve: () => '1991-12-24' 25 | }, 26 | invalidDateString: { 27 | type: GraphQLDate, 28 | resolve: () => '2017-01-001' 29 | }, 30 | invalidDate: { 31 | type: GraphQLDate, 32 | resolve: () => new Date('wrong') 33 | }, 34 | invalidType: { 35 | type: GraphQLDate, 36 | resolve: () => 5 37 | }, 38 | input: { 39 | type: GraphQLDate, 40 | args: { 41 | date: { 42 | type: GraphQLDate 43 | } 44 | }, 45 | resolve: (_, input: { date: Date }) => input.date 46 | } 47 | } 48 | }) 49 | }) 50 | 51 | it('executes a query that includes a date', async () => { 52 | const query = ` 53 | query DateTest($date: Date!) { 54 | validDate 55 | validDateString 56 | input(date: $date) 57 | inputNull: input 58 | } 59 | ` 60 | 61 | const variables = { date: '2017-10-01' } 62 | 63 | const response = await graphql(schema, query, null, null, variables) 64 | 65 | expect(response).toEqual({ 66 | data: { 67 | validDate: '2016-05-02', 68 | input: '2017-10-01', 69 | validDateString: '1991-12-24', 70 | inputNull: null 71 | } 72 | }) 73 | }) 74 | 75 | it('parses input to a JS Date', done => { 76 | const schema = new GraphQLSchema({ 77 | query: new GraphQLObjectType({ 78 | name: 'Query', 79 | fields: { 80 | input: { 81 | type: GraphQLDate, 82 | args: { 83 | date: { 84 | type: GraphQLDate 85 | } 86 | }, 87 | resolve: (_, input) => { 88 | try { 89 | expect(input.date).toEqual(new Date(Date.UTC(2016, 11, 17))) 90 | done() 91 | } catch (e) { 92 | done.fail(e) 93 | } 94 | } 95 | } 96 | } 97 | }) 98 | }) 99 | 100 | const query = ` 101 | query DateTest($date: Date!) { 102 | input(date: $date) 103 | } 104 | ` 105 | const variables = { date: '2016-12-17' } 106 | 107 | graphql(schema, query, null, null, variables) 108 | }) 109 | 110 | it('errors if there is an invalid date returned from the resolver', async () => { 111 | const query = ` 112 | { 113 | invalidDateString 114 | invalidDate 115 | invalidType 116 | } 117 | ` 118 | 119 | const response = await graphql(schema, query) 120 | 121 | expect(response).toEqual({ 122 | data: { 123 | invalidDateString: null, 124 | invalidDate: null, 125 | invalidType: null 126 | }, 127 | errors: [ 128 | new GraphQLError('Date cannot represent an invalid date-string 2017-01-001.'), 129 | new GraphQLError('Date cannot represent an invalid Date instance'), 130 | new GraphQLError('Date cannot represent a non string, or non Date type 5') 131 | ] 132 | }) 133 | }) 134 | 135 | it('errors if the variable value is not a valid date', async () => { 136 | const query = ` 137 | query DateTest($date: Date!) { 138 | input(date: $date) 139 | } 140 | ` 141 | 142 | const variables = { date: '2017-10-001' } 143 | 144 | const response = await graphql(schema, query, null, null, variables) 145 | 146 | expect(response).toEqual({ 147 | errors: [ 148 | new GraphQLError('Variable "$date" got invalid value "2017-10-001"; Expected type Date; Date cannot represent an invalid date-string 2017-10-001.') 149 | ] 150 | }) 151 | }) 152 | 153 | it('errors if the variable value is not of type string', async () => { 154 | const query = ` 155 | query DateTest($date: Date!) { 156 | input(date: $date) 157 | } 158 | ` 159 | 160 | const variables = { date: 4 } 161 | 162 | const response = await graphql(schema, query, null, null, variables) 163 | 164 | expect(response).toEqual({ 165 | errors: [ 166 | new GraphQLError('Variable "$date" got invalid value 4; Expected type Date; Date cannot represent non string type 4') 167 | ] 168 | }) 169 | }) 170 | 171 | it('errors if the literal input value is not a valid date', async () => { 172 | const query = ` 173 | { 174 | input(date: "2017-10-001") 175 | } 176 | ` 177 | 178 | const response = await graphql(schema, query) 179 | 180 | expect(response).toEqual({ 181 | errors: [ 182 | new GraphQLError('Expected type Date, found "2017-10-001"; Date cannot represent an invalid date-string 2017-10-001.') 183 | ] 184 | }) 185 | }) 186 | 187 | it('errors if the literal input value in a query is not a string', async () => { 188 | const query = ` 189 | { 190 | input(date: 4) 191 | } 192 | ` 193 | 194 | const response = await graphql(schema, query) 195 | 196 | expect(response).toEqual({ 197 | errors: [ 198 | new GraphQLError('Expected type Date, found 4; Date cannot represent non string type 4') 199 | ] 200 | }) 201 | }) 202 | -------------------------------------------------------------------------------- /src/time/index.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * Copyright (c) 2017, Dirk-Jan Rutten 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | * 9 | */ 10 | 11 | import GraphQLTime from './' 12 | import { Kind } from 'graphql' 13 | // flowlint-next-line untyped-import:off 14 | import MockDate from 'mockdate' 15 | // flowlint-next-line untyped-import:off 16 | import { stringify } from 'jest-matcher-utils' 17 | 18 | // Mock the new Date() call so it always returns 2017-01-01T00:00:00.000Z 19 | MockDate.set(new Date(Date.UTC(2017, 0, 1))) 20 | 21 | const invalidDates = [ 22 | 'Invalid date', 23 | '2016-01-01T00:00:00.223Z', 24 | '10:30:02.Z', 25 | '00:00:00.45+0130', 26 | '00:00:00.45+01' 27 | ] 28 | 29 | const validDates = [ 30 | [ '00:00:00Z', new Date(Date.UTC(2017, 0, 1)) ], 31 | [ '00:00:59Z', new Date(Date.UTC(2017, 0, 1, 0, 0, 59)) ], 32 | [ '10:30:02.1Z', new Date(Date.UTC(2017, 0, 1, 10, 30, 2, 100)) ], 33 | [ '09:09:06.13Z', new Date(Date.UTC(2017, 0, 1, 9, 9, 6, 130)) ], 34 | [ '10:00:11.003Z', new Date(Date.UTC(2017, 0, 1, 10, 0, 11, 3)) ], 35 | [ '16:10:20.1359945Z', new Date(Date.UTC(2017, 0, 1, 16, 10, 20, 135)) ], 36 | [ '00:00:00+01:30', new Date(Date.UTC(2016, 11, 31, 22, 30)) ], 37 | [ '00:00:30.3-01:30', new Date(Date.UTC(2017, 0, 1, 1, 30, 30, 300)) ] 38 | ] 39 | 40 | describe('GraphQLTime', () => { 41 | it('has a description', () => { 42 | expect(GraphQLTime.description).toMatchSnapshot() 43 | }) 44 | 45 | describe('serialization', () => { 46 | [ 47 | {}, 48 | [], 49 | null, 50 | undefined, 51 | true 52 | ].forEach(invalidInput => { 53 | it(`throws error when serializing ${stringify(invalidInput)}`, () => { 54 | expect(() => 55 | GraphQLTime.serialize(invalidInput) 56 | ).toThrowErrorMatchingSnapshot() 57 | }) 58 | }); 59 | 60 | // Serialize from Date 61 | [ 62 | [ new Date(Date.UTC(2016, 0, 1)), '00:00:00.000Z' ], 63 | [ new Date(Date.UTC(2016, 0, 1, 14, 48, 10, 3)), '14:48:10.003Z' ] 64 | ].forEach(([ value, expected ]) => { 65 | it(`serializes javascript Date ${stringify(value)} into ${stringify(expected)}`, () => { 66 | expect( 67 | GraphQLTime.serialize(value) 68 | ).toEqual(expected) 69 | }) 70 | }) 71 | 72 | it(`throws error when serializing invalid date`, () => { 73 | expect(() => 74 | GraphQLTime.serialize(new Date('invalid date')) 75 | ).toThrowErrorMatchingSnapshot() 76 | }); 77 | 78 | [ 79 | [ '00:00:00Z', '00:00:00Z' ], 80 | [ '10:30:02.1Z', '10:30:02.1Z' ], 81 | [ '16:10:20.1359945Z', '16:10:20.1359945Z' ], 82 | [ '00:00:00+01:30', '22:30:00Z' ], 83 | [ '00:00:30.3-01:30', '01:30:30.3Z' ] 84 | ].forEach(([input, output]) => { 85 | it(`serializes time-string ${input} into UTC time-string ${output}`, () => { 86 | expect( 87 | GraphQLTime.serialize(input) 88 | ).toEqual(output) 89 | }) 90 | }) 91 | 92 | invalidDates.forEach(dateString => { 93 | it(`throws an error when serializing an invalid date-string ${stringify(dateString)}`, () => { 94 | expect(() => 95 | GraphQLTime.serialize(dateString) 96 | ).toThrowErrorMatchingSnapshot() 97 | }) 98 | }) 99 | }) 100 | 101 | describe('value parsing', () => { 102 | validDates.forEach(([ value, expected ]) => { 103 | it(`parses date-string ${stringify(value)} into javascript Date ${stringify(expected)}`, () => { 104 | expect( 105 | GraphQLTime.parseValue(value) 106 | ).toEqual(expected) 107 | }) 108 | }); 109 | 110 | [ 111 | 4566, 112 | {}, 113 | [], 114 | true, 115 | null 116 | ].forEach(invalidInput => { 117 | it(`throws an error when parsing ${stringify(invalidInput)}`, () => { 118 | expect(() => 119 | GraphQLTime.parseValue(invalidInput) 120 | ).toThrowErrorMatchingSnapshot() 121 | }) 122 | }) 123 | 124 | invalidDates.forEach(dateString => { 125 | it(`throws an error parsing an invalid time-string ${stringify(dateString)}`, () => { 126 | expect(() => 127 | GraphQLTime.parseValue(dateString) 128 | ).toThrowErrorMatchingSnapshot() 129 | }) 130 | }) 131 | }) 132 | 133 | describe('literial parsing', () => { 134 | validDates.forEach(([ value, expected ]) => { 135 | const literal = { 136 | kind: Kind.STRING, value 137 | } 138 | 139 | it(`parses literal ${stringify(literal)} into javascript Date ${stringify(expected)}`, () => { 140 | expect( 141 | GraphQLTime.parseLiteral(literal) 142 | ).toEqual(expected) 143 | }) 144 | }) 145 | 146 | invalidDates.forEach(value => { 147 | const invalidLiteral = { 148 | kind: Kind.STRING, value 149 | } 150 | it(`errors when parsing invalid literal ${stringify(invalidLiteral)}`, () => { 151 | expect(() => 152 | GraphQLTime.parseLiteral(invalidLiteral) 153 | ).toThrowErrorMatchingSnapshot() 154 | }) 155 | }); 156 | 157 | [ 158 | { 159 | kind: Kind.FLOAT, value: '5' 160 | }, 161 | ({ 162 | kind: Kind.DOCUMENT 163 | // flowlint-next-line unclear-type:off 164 | }: any) 165 | ].forEach(literal => { 166 | it(`errors when parsing invalid literal ${stringify(literal)}`, () => { 167 | expect(() => 168 | GraphQLTime.parseLiteral(literal) 169 | ).toThrowError() 170 | }) 171 | }) 172 | }) 173 | }) 174 | -------------------------------------------------------------------------------- /src/utils/validator.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * Copyright (c) 2017, Dirk-Jan Rutten 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | * 9 | */ 10 | 11 | // Check whether a certain year is a leap year. 12 | // 13 | // Every year that is exactly divisible by four 14 | // is a leap year, except for years that are exactly 15 | // divisible by 100, but these centurial years are 16 | // leap years if they are exactly divisible by 400. 17 | // For example, the years 1700, 1800, and 1900 are not leap years, 18 | // but the years 1600 and 2000 are. 19 | // 20 | const leapYear = (year: number): boolean => { 21 | return ((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0) 22 | } 23 | 24 | // Function that checks whether a time-string is RFC 3339 compliant. 25 | // 26 | // It checks whether the time-string is structured in one of the 27 | // following formats: 28 | // 29 | // - hh:mm:ssZ 30 | // - hh:mm:ss±hh:mm 31 | // - hh:mm:ss.*sZ 32 | // - hh:mm:ss.*s±hh:mm 33 | // 34 | // Where *s is a fraction of seconds with at least 1 digit. 35 | // 36 | // Note, this validator assumes that all minutes have 37 | // 59 seconds. This assumption does not follow RFC 3339 38 | // which includes leap seconds (in which case it is possible that 39 | // there are 60 seconds in a minute). 40 | // 41 | // Leap seconds are ignored because it adds complexity in 42 | // the following areas: 43 | // - The native Javascript Date ignores them; i.e. Date.parse('1972-12-31T23:59:60Z') 44 | // equals NaN. 45 | // - Leap seconds cannot be known in advance. 46 | // 47 | export const validateTime = (time: string): boolean => { 48 | const TIME_REGEX = /^([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(\.\d{1,})?(([Z])|([+|-]([01][0-9]|2[0-3]):[0-5][0-9]))$/ 49 | return TIME_REGEX.test(time) 50 | } 51 | 52 | // Function that checks whether a date-string is RFC 3339 compliant. 53 | // 54 | // It checks whether the date-string is a valid date in the YYYY-MM-DD. 55 | // 56 | // Note, the number of days in each date are determined according to the 57 | // following lookup table: 58 | // 59 | // Month Number Month/Year Maximum value of date-mday 60 | // ------------ ---------- -------------------------- 61 | // 01 January 31 62 | // 02 February, normal 28 63 | // 02 February, leap year 29 64 | // 03 March 31 65 | // 04 April 30 66 | // 05 May 31 67 | // 06 June 30 68 | // 07 July 31 69 | // 08 August 31 70 | // 09 September 30 71 | // 10 October 31 72 | // 11 November 30 73 | // 12 December 31 74 | // 75 | export const validateDate = (datestring: string): boolean => { 76 | const RFC_3339_REGEX = /^(\d{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01]))$/ 77 | 78 | if (!RFC_3339_REGEX.test(datestring)) { 79 | return false 80 | } 81 | 82 | // Verify the correct number of days for 83 | // the month contained in the date-string. 84 | const year = Number(datestring.substr(0, 4)) 85 | const month = Number(datestring.substr(5, 2)) 86 | const day = Number(datestring.substr(8, 2)) 87 | 88 | switch (month) { 89 | case 2: // February 90 | if (leapYear(year) && day > 29) { 91 | return false 92 | } else if (!leapYear(year) && day > 28) { 93 | return false 94 | } 95 | return true 96 | case 4: // April 97 | case 6: // June 98 | case 9: // September 99 | case 11: // November 100 | if (day > 30) { 101 | return false 102 | } 103 | break 104 | } 105 | 106 | return true 107 | } 108 | 109 | // Function that checks whether a date-time-string is RFC 3339 compliant. 110 | // 111 | // It checks whether the time-string is structured in one of the 112 | // 113 | // - YYYY-MM-DDThh:mm:ssZ 114 | // - YYYY-MM-DDThh:mm:ss±hh:mm 115 | // - YYYY-MM-DDThh:mm:ss.*sZ 116 | // - YYYY-MM-DDThh:mm:ss.*s±hh:mm 117 | // 118 | // Where *s is a fraction of seconds with at least 1 digit. 119 | // 120 | export const validateDateTime = (dateTimeString: string): boolean => { 121 | const RFC_3339_REGEX = /^(\d{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60))(\.\d{1,})?(([Z])|([+|-]([01][0-9]|2[0-3]):[0-5][0-9]))$/ 122 | 123 | // Validate the structure of the date-string 124 | if (!RFC_3339_REGEX.test(dateTimeString)) { 125 | return false 126 | } 127 | 128 | // Check if it is a correct date using the javascript Date parse() method. 129 | const time = Date.parse(dateTimeString) 130 | if (time !== time) { // eslint-disable-line 131 | return false 132 | } 133 | // Split the date-time-string up into the string-date and time-string part. 134 | // and check whether these parts are RFC 3339 compliant. 135 | const index = dateTimeString.indexOf('T') 136 | const dateString = dateTimeString.substr(0, index) 137 | const timeString = dateTimeString.substr(index + 1) 138 | return (validateDate(dateString) && validateTime(timeString)) 139 | } 140 | 141 | // Function that checks whether a given number is a valid 142 | // Unix timestamp. 143 | // 144 | // Unix timestamps are signed 32-bit integers. They are interpreted 145 | // as the number of seconds since 00:00:00 UTC on 1 January 1970. 146 | // 147 | export const validateUnixTimestamp = (timestamp: number): boolean => { 148 | const MAX_INT = 2147483647 149 | const MIN_INT = -2147483648 150 | return (timestamp === timestamp && timestamp <= MAX_INT && timestamp >= MIN_INT) // eslint-disable-line 151 | } 152 | 153 | // Function that checks whether a javascript Date instance 154 | // is valid. 155 | // 156 | export const validateJSDate = (date: Date): boolean => { 157 | const time = date.getTime() 158 | return time === time // eslint-disable-line 159 | } 160 | -------------------------------------------------------------------------------- /src/time/integration.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * Copyright (c) 2018, Dirk-Jan Rutten 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | * 9 | */ 10 | 11 | import { graphql, GraphQLObjectType, GraphQLSchema, GraphQLError } from 'graphql' 12 | import GraphQLTime from './' 13 | // flowlint-next-line untyped-import:off 14 | import MockDate from 'mockdate' 15 | 16 | // Mock the new Date() call so it always returns 2017-01-01T00:00:00.000Z 17 | MockDate.set(new Date(Date.UTC(2017, 0, 1))) 18 | 19 | const schema = new GraphQLSchema({ 20 | query: new GraphQLObjectType({ 21 | name: 'Query', 22 | fields: { 23 | validJSDate: { 24 | type: GraphQLTime, 25 | resolve: () => new Date(Date.UTC(2016, 0, 1, 14, 48, 10, 3)) 26 | }, 27 | validUTCTimeString: { 28 | type: GraphQLTime, 29 | resolve: () => '14:30:00Z' 30 | }, 31 | validTimeString: { 32 | type: GraphQLTime, 33 | resolve: () => '00:00:00+01:30' 34 | }, 35 | invalidTimeString: { 36 | type: GraphQLTime, 37 | resolve: () => '2222' 38 | }, 39 | invalidJSDate: { 40 | type: GraphQLTime, 41 | resolve: () => new Date('wrong') 42 | }, 43 | invalidType: { 44 | type: GraphQLTime, 45 | resolve: () => 5 46 | }, 47 | input: { 48 | type: GraphQLTime, 49 | args: { 50 | time: { 51 | type: GraphQLTime 52 | } 53 | }, 54 | resolve: (_, input) => input.time 55 | } 56 | } 57 | }) 58 | }) 59 | 60 | it('executes a query that includes a time', async () => { 61 | const query = ` 62 | query TimeTest($time: Time!) { 63 | validJSDate 64 | validUTCTimeString 65 | validTimeString 66 | input(time: $time) 67 | inputNull: input 68 | } 69 | ` 70 | 71 | const variables = { time: '14:30:00Z' } 72 | 73 | const response = await graphql(schema, query, null, null, variables) 74 | 75 | expect(response).toEqual({ 76 | data: { 77 | validJSDate: '14:48:10.003Z', 78 | validUTCTimeString: '14:30:00Z', 79 | validTimeString: '22:30:00Z', 80 | input: '14:30:00.000Z', 81 | inputNull: null 82 | } 83 | }) 84 | }) 85 | 86 | it('shifts an input time to UTC', async () => { 87 | const query = ` 88 | query TimeTest($time: Time!) { 89 | input(time: $time) 90 | } 91 | ` 92 | 93 | const variables = { time: '00:00:00+01:30' } 94 | 95 | const response = await graphql(schema, query, null, null, variables) 96 | 97 | expect(response).toEqual({ 98 | data: { 99 | input: '22:30:00.000Z' 100 | } 101 | }) 102 | }) 103 | 104 | it('parses input to a JS Date', done => { 105 | const schema = new GraphQLSchema({ 106 | query: new GraphQLObjectType({ 107 | name: 'Query', 108 | fields: { 109 | input: { 110 | type: GraphQLTime, 111 | args: { 112 | time: { 113 | type: GraphQLTime 114 | } 115 | }, 116 | resolve: (_, input) => { 117 | try { 118 | expect(input.time).toEqual(new Date(Date.UTC(2016, 11, 31, 22, 30))) 119 | done() 120 | } catch (e) { 121 | done.fail(e) 122 | } 123 | } 124 | } 125 | } 126 | }) 127 | }) 128 | 129 | const query = ` 130 | query TimeTest($time: Time!) { 131 | input(time: $time) 132 | } 133 | ` 134 | 135 | const variables = { time: '00:00:00+01:30' } 136 | 137 | graphql(schema, query, null, null, variables) 138 | }) 139 | 140 | it('errors if there is an invalid time returned from the resolver', async () => { 141 | const query = ` 142 | { 143 | invalidTimeString 144 | invalidJSDate 145 | invalidType 146 | } 147 | ` 148 | 149 | const response = await graphql(schema, query) 150 | 151 | expect(response).toEqual({ 152 | data: { 153 | invalidTimeString: null, 154 | invalidJSDate: null, 155 | invalidType: null 156 | }, 157 | errors: [ 158 | new GraphQLError('Time cannot represent an invalid time-string 2222.'), 159 | new GraphQLError('Time cannot represent an invalid Date instance'), 160 | new GraphQLError('Time cannot be serialized from a non string, or non Date type 5') 161 | ] 162 | }) 163 | }) 164 | 165 | it('errors if the variable value is not a valid time', async () => { 166 | const query = ` 167 | query TimeTest($time: Time!) { 168 | input(time: $time) 169 | } 170 | ` 171 | 172 | const variables = { time: '__2222' } 173 | 174 | const response = await graphql(schema, query, null, null, variables) 175 | 176 | expect(response).toEqual({ 177 | errors: [ 178 | new GraphQLError('Variable "$time" got invalid value "__2222"; Expected type Time; Time cannot represent an invalid time-string __2222.') 179 | ] 180 | }) 181 | }) 182 | 183 | it('errors if the variable value is not of type string', async () => { 184 | const query = ` 185 | query DateTest($time: Time!) { 186 | input(time: $time) 187 | } 188 | ` 189 | 190 | const variables = { time: 4 } 191 | 192 | const response = await graphql(schema, query, null, null, variables) 193 | 194 | expect(response).toEqual({ 195 | errors: [ 196 | new GraphQLError('Variable "$time" got invalid value 4; Expected type Time; Time cannot represent non string type 4') 197 | ] 198 | }) 199 | }) 200 | 201 | it('errors if the literal input value is not a valid time', async () => { 202 | const query = ` 203 | { 204 | input(time: "__invalid__") 205 | } 206 | ` 207 | 208 | const response = await graphql(schema, query) 209 | 210 | expect(response).toEqual({ 211 | errors: [ 212 | new GraphQLError('Expected type Time, found "__invalid__"; Time cannot represent an invalid time-string __invalid__.') 213 | ] 214 | }) 215 | }) 216 | 217 | it('errors if the literal input value in a query is not a string', async () => { 218 | const query = ` 219 | { 220 | input(time: 4) 221 | } 222 | ` 223 | 224 | const response = await graphql(schema, query) 225 | 226 | expect(response).toEqual({ 227 | errors: [ 228 | new GraphQLError('Expected type Time, found 4; Time cannot represent non string type 4') 229 | ] 230 | }) 231 | }) 232 | -------------------------------------------------------------------------------- /src/utils/__tests__/validatorTest.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * Copyright (c) 2017, Dirk-Jan Rutten 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | * 9 | */ 10 | 11 | import { 12 | validateTime, 13 | validateDate, 14 | validateDateTime, 15 | validateUnixTimestamp, 16 | validateJSDate 17 | } from '../' 18 | 19 | describe('validator', () => { 20 | describe('validateTime', () => { 21 | [ 22 | '00:00:00Z', 23 | '23:00:00Z', 24 | '10:59:00Z', 25 | '00:11:59Z', 26 | '00:00:00+01:30', 27 | '00:00:00-01:30', 28 | '00:00:00.1Z', 29 | '00:00:00.1-01:30', 30 | '00:00:00.34Z', 31 | '00:00:00.34-01:30', 32 | '00:00:00.000Z', 33 | '00:00:00.999Z', 34 | '00:00:00.450+01:30', 35 | '00:00:00.450-01:30', 36 | '00:00:00.450-23:00', 37 | '00:00:00.450-00:59', 38 | '00:00:00.45643222345664443Z', 39 | '00:00:00.3455334564433+01:00' 40 | ].forEach(time => { 41 | it(`identifies ${time} as a valid time`, () => { 42 | expect(validateTime(time)).toEqual(true) 43 | }) 44 | }); 45 | 46 | [ 47 | 'Invalid date', 48 | '00Z', 49 | // Time with hours and minutes 50 | '00:00Z', 51 | // Time with hours, minutes and seconds 52 | '000059Z', 53 | '00:00:0Z', 54 | '00:00:00', 55 | '24:00:00Z', 56 | '13:60:00Z', 57 | '00:00:60Z', 58 | '13:60:61Z', 59 | '00:00:00+01', 60 | '00:00:00+0100', 61 | // Time with hours, minutes, seconds and fractional seconds 62 | '00:00:00.Z', 63 | '00:00:00.223', 64 | '00:00:00.000+0100', 65 | '00:00:00.000+01', 66 | '00:00:00.000+24:00', 67 | '00:00:00.000+00:60', 68 | // Date 69 | '2016-01-01T00:00:00.223Z', 70 | '2016-01-01T00Z' 71 | ].forEach(time => { 72 | it(`identifies ${time} as an invalid date`, () => { 73 | expect(validateTime(time)).toEqual(false) 74 | }) 75 | }) 76 | }) 77 | 78 | describe('validateDate', () => { 79 | [ 80 | '2016-12-17', 81 | '2016-02-01', 82 | '0000-01-01', 83 | '9999-01-01', 84 | '2016-02-29', 85 | '2000-02-29', 86 | '2016-05-31', 87 | '2016-11-20' 88 | ].forEach(date => { 89 | it(`identifies ${date} as a valid date`, () => { 90 | expect(validateDate(date)).toEqual(true) 91 | }) 92 | }); 93 | 94 | [ 95 | 'invalid date', 96 | '2016', 97 | '2016-01', 98 | '21233', 99 | '2016-02-01T25', 100 | '2016-02-01T00Z', 101 | '2016-02-01T00:00:00.223Z', 102 | '2015-02-29', 103 | '1900-02-29', 104 | '20162-12-11', 105 | '2015-13-11', 106 | '2015-8-32', 107 | '2015-111', 108 | '2016-04-31', 109 | '2016-06-31', 110 | '2016-09-31', 111 | '2016-11-31', 112 | '2016-02-30', 113 | '9999-00-31' 114 | ].forEach(date => { 115 | it(`identifies ${date} as an invalid date`, () => { 116 | expect(validateDate(date)).toEqual(false) 117 | }) 118 | }) 119 | }) 120 | 121 | describe('validateUnixTimestamp', () => { 122 | [ 123 | 854325678, 124 | 876535, 125 | 876535.8, 126 | 876535.8321, 127 | -876535.8, 128 | // The maximum representable unix timestamp 129 | 2147483647, 130 | // The minimum representable unit timestamp 131 | -2147483648 132 | ].forEach(timestamp => { 133 | it(`identifies ${timestamp} as a valid Unix timestamp`, () => { 134 | expect(validateUnixTimestamp(timestamp)).toEqual(true) 135 | }) 136 | }); 137 | 138 | [ 139 | Number.NaN, 140 | Number.POSITIVE_INFINITY, 141 | Number.POSITIVE_INFINITY, 142 | 2147483648, 143 | -2147483649 144 | ].forEach(timestamp => { 145 | it(`identifies ${timestamp} as an invalid Unix timestamp`, () => { 146 | expect(validateUnixTimestamp(timestamp)).toEqual(false) 147 | }) 148 | }) 149 | }) 150 | 151 | describe('validateDateTime', () => { 152 | [ 153 | // Datetime with hours, minutes and seconds 154 | '2016-02-01T00:00:00Z', 155 | '2016-02-01T00:00:15Z', 156 | '2016-02-01T00:00:59Z', 157 | '2016-02-01T00:00:00-11:00', 158 | '2017-01-07T11:25:00+01:00', 159 | '2017-01-07T00:00:00+01:00', 160 | // Datetime with hours, minutes, seconds and fractional seconds 161 | '2017-01-07T00:00:00.0Z', 162 | '2017-01-01T00:00:00.0+01:00', 163 | '2016-02-01T00:00:00.000Z', 164 | '2016-02-01T00:00:00.990Z', 165 | '2016-02-01T00:00:00.450Z', 166 | '2017-01-07T11:25:00.450+01:00', 167 | '2017-01-01T10:23:11.45686664Z', 168 | '2017-01-01T10:23:11.23545654+01:00' 169 | ].forEach(dateTime => { 170 | it(`identifies ${dateTime} as a valid date-time`, () => { 171 | expect(validateDateTime(dateTime)).toEqual(true) 172 | }) 173 | }); 174 | 175 | [ 176 | 'Invalid date', 177 | // Date-time with hours 178 | '2016-02-01T00Z', 179 | // Date-time with hours and minutes 180 | '2016-02-01T00:00Z', 181 | // Date-time with hours, minutes and seconds 182 | '2016-02-01T000059Z', 183 | '2016-02-01T00:00:60Z', 184 | '2016-02-01T00:00:0Z', 185 | '2015-02-29T00:00:00Z', 186 | '2016-02-01T00:00:00', 187 | '2017-01-07T11:25:00+0100', 188 | '2017-01-07T11:25:00+01', 189 | '2017-01-07T11:25:00+', 190 | // Date-time with hours, minutes, seconds and fractional seconds 191 | '2015-02-26T00:00:00.Z', 192 | '2015-02-29T00:00:00.000Z', 193 | '2016-02-01T00:00:00.223', 194 | '2016-02-01T00:00:00', 195 | '2017-01-07T11:25:00.450+0100', 196 | '2017-01-07T11:25:00.450+01', 197 | '2017-44-07T11:25:00.450+01:00', 198 | '2017-01-07T25:25:00.450+01:00', 199 | '2017-01-07T11:11:11+24:00' 200 | ].forEach(dateTime => { 201 | it(`identifies ${dateTime} as an invalid date-time`, () => { 202 | expect(validateDateTime(dateTime)).toEqual(false) 203 | }) 204 | }) 205 | }) 206 | 207 | describe('validateJSDate', () => { 208 | it('identifies invalid Date', () => { 209 | expect(validateJSDate(new Date('invalid'))).toBeFalsy() 210 | }) 211 | 212 | it('identifies a valid Date', () => { 213 | expect(validateJSDate(new Date(2016, 1, 1))).toBeTruthy() 214 | }) 215 | }) 216 | }) 217 | -------------------------------------------------------------------------------- /src/utils/__tests__/formatterTests.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * Copyright (c) 2017, Dirk-Jan Rutten 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | * 9 | */ 10 | 11 | import { 12 | serializeTime, 13 | serializeTimeString, 14 | serializeDate, 15 | serializeDateTime, 16 | serializeDateTimeString, 17 | serializeUnixTimestamp, 18 | parseTime, 19 | parseDate, 20 | parseDateTime 21 | } from '../' 22 | // flowlint-next-line untyped-import:off 23 | import { stringify } from 'jest-matcher-utils' 24 | // flowlint-next-line untyped-import:off 25 | import MockDate from 'mockdate' 26 | // Mock the new Date() call so it always returns 2017-01-01T00:00:00.000Z 27 | MockDate.set(new Date(Date.UTC(2017, 0, 1))) 28 | 29 | describe('formatting', () => { 30 | [ 31 | [ new Date(Date.UTC(2016, 1, 1)), '00:00:00.000Z' ], 32 | [ new Date(Date.UTC(2016, 1, 1, 2, 4, 10, 344)), '02:04:10.344Z' ] 33 | ].forEach(([date, time]) => { 34 | it(`serializes ${stringify(date)} into time-string ${time}`, () => { 35 | expect(serializeTime(date)).toEqual(time) 36 | }) 37 | }); 38 | 39 | [ 40 | [ '00:00:00.000Z', '00:00:00.000Z' ], 41 | [ '12:23:44Z', '12:23:44Z' ], 42 | [ '14:38:12+01:00', '13:38:12Z' ], 43 | [ '00:00:00.4567+01:30', '22:30:00.4567Z' ], 44 | [ '14:38:12.1+01:00', '13:38:12.1Z' ] 45 | ].forEach(([input, output]) => { 46 | it(`serializes time-string ${input} into UTC time-string ${output}`, () => { 47 | expect(serializeTimeString(input)).toEqual(output) 48 | }) 49 | }); 50 | 51 | [ 52 | [ new Date(Date.UTC(2016, 1, 1)), '2016-02-01' ], 53 | [ new Date(Date.UTC(2016, 1, 1, 4, 5, 5)), '2016-02-01' ], 54 | [ new Date(Date.UTC(2016, 2, 3)), '2016-03-03' ] 55 | ].forEach(([date, dateString]) => { 56 | it(`serializes ${stringify(date)} into date-string ${dateString}`, () => { 57 | expect(serializeDate(date)).toEqual(dateString) 58 | }) 59 | }); 60 | 61 | [ 62 | [ new Date(Date.UTC(2016, 1, 1)), '2016-02-01T00:00:00.000Z' ], 63 | [ new Date(Date.UTC(2016, 3, 5, 10, 1, 4, 555)), '2016-04-05T10:01:04.555Z' ] 64 | ].forEach(([date, dateTimeString]) => { 65 | it(`serializes ${stringify(date)} into date-time-string ${dateTimeString}`, () => { 66 | expect(serializeDateTime(date)).toEqual(dateTimeString) 67 | }) 68 | }); 69 | 70 | [ 71 | [ new Date(Date.UTC(2016, 1, 1)), '2016-02-01T00:00:00.000Z' ], 72 | [ new Date(Date.UTC(2016, 3, 5, 10, 1, 4, 555)), '2016-04-05T10:01:04.555Z' ] 73 | ].forEach(([date, dateTimeString]) => { 74 | it(`serializes ${stringify(date)} into date-time-string ${dateTimeString}`, () => { 75 | expect(serializeDateTime(date)).toEqual(dateTimeString) 76 | }) 77 | }); 78 | 79 | [ 80 | [ 854325678, '1997-01-27T00:41:18.000Z' ], 81 | [ 876535, '1970-01-11T03:28:55.000Z' ], 82 | [ 876535.8, '1970-01-11T03:28:55.800Z' ], 83 | [ 876535.8321, '1970-01-11T03:28:55.832Z' ], 84 | [ -876535.8, '1969-12-21T20:31:04.200Z' ], 85 | [ 0, '1970-01-01T00:00:00.000Z' ], 86 | // The maximum representable unix timestamp 87 | [ 2147483647, '2038-01-19T03:14:07.000Z' ], 88 | // The minimum representable unit timestamp 89 | [ -2147483648, '1901-12-13T20:45:52.000Z' ] 90 | ].forEach(([timestamp, dateTimeString]) => { 91 | it(`serializes Unix timestamp ${stringify(timestamp)} into date-time-string ${dateTimeString}`, () => { 92 | expect(serializeUnixTimestamp(timestamp)).toEqual(dateTimeString) 93 | }) 94 | }); 95 | 96 | [ 97 | [ '00:00:59Z', new Date(Date.UTC(2017, 0, 1, 0, 0, 59)) ], 98 | [ '00:00:00+01:30', new Date(Date.UTC(2016, 11, 31, 22, 30)) ], 99 | [ '00:00:00.1Z', new Date(Date.UTC(2017, 0, 1, 0, 0, 0, 100)) ], 100 | [ '00:00:00.12Z', new Date(Date.UTC(2017, 0, 1, 0, 0, 0, 120)) ], 101 | [ '00:00:00.000Z', new Date(Date.UTC(2017, 0, 1)) ], 102 | [ '00:00:00.993Z', new Date(Date.UTC(2017, 0, 1, 0, 0, 0, 993)) ], 103 | [ '00:00:00.123456Z', new Date(Date.UTC(2017, 0, 1, 0, 0, 0, 123)) ], 104 | // No rounding takes place! 105 | [ '00:00:00.12399Z', new Date(Date.UTC(2017, 0, 1, 0, 0, 0, 123)) ], 106 | [ '00:00:00.450+01:30', new Date(Date.UTC(2016, 11, 31, 22, 30, 0, 450)) ], 107 | [ '00:00:00.450-01:30', new Date(Date.UTC(2017, 0, 1, 1, 30, 0, 450)) ] 108 | ].forEach(([time, date]) => { 109 | it(`parses time ${stringify(time)} into Date ${stringify(date)}`, () => { 110 | expect(parseTime(time)).toEqual(date) 111 | }) 112 | }); 113 | 114 | [ 115 | [ '2016-12-17', new Date(Date.UTC(2016, 11, 17)) ], 116 | [ '2016-02-01', new Date(Date.UTC(2016, 1, 1)) ] 117 | ].forEach(([dateString, date]) => { 118 | it(`parses date ${stringify(dateString)} into Date ${stringify(date)}`, () => { 119 | expect(parseDate(dateString)).toEqual(date) 120 | }) 121 | }); 122 | 123 | [ 124 | // Datetime with hours, minutes and seconds 125 | [ '2016-02-01T00:00:00Z', new Date(Date.UTC(2016, 1, 1, 0, 0, 0)) ], 126 | [ '2016-02-01T00:00:15Z', new Date(Date.UTC(2016, 1, 1, 0, 0, 15)) ], 127 | [ '2016-02-01T00:00:59Z', new Date(Date.UTC(2016, 1, 1, 0, 0, 59)) ], 128 | [ '2016-02-01T00:00:00-11:00', new Date(Date.UTC(2016, 1, 1, 11)) ], 129 | [ '2017-01-07T11:25:00+01:00', new Date(Date.UTC(2017, 0, 7, 10, 25)) ], 130 | [ '2017-01-07T00:00:00+01:00', new Date(Date.UTC(2017, 0, 6, 23)) ], 131 | // Datetime with hours, minutes, seconds and fractional seconds. 132 | [ '2016-02-01T00:00:00.12Z', new Date(Date.UTC(2016, 1, 1, 0, 0, 0, 120)) ], 133 | [ '2016-02-01T00:00:00.123456Z', new Date(Date.UTC(2016, 1, 1, 0, 0, 0, 123)) ], 134 | [ '2016-02-01T00:00:00.12399Z', new Date(Date.UTC(2016, 1, 1, 0, 0, 0, 123)) ], 135 | [ '2016-02-01T00:00:00.000Z', new Date(Date.UTC(2016, 1, 1, 0, 0, 0, 0)) ], 136 | [ '2016-02-01T00:00:00.993Z', new Date(Date.UTC(2016, 1, 1, 0, 0, 0, 993)) ], 137 | [ '2017-01-07T11:25:00.450+01:00', new Date(Date.UTC(2017, 0, 7, 10, 25, 0, 450)) ] 138 | ].forEach(([dateTime, date]) => { 139 | it(`parses date-time ${stringify(dateTime)} into Date ${stringify(date)}`, () => { 140 | expect(parseDateTime(dateTime)).toEqual(date) 141 | }) 142 | }); 143 | 144 | [ 145 | [ '2016-02-01T00:00:00Z', '2016-02-01T00:00:00Z' ], 146 | [ '2016-02-01T12:23:44Z', '2016-02-01T12:23:44Z' ], 147 | [ '2016-02-01T14:38:12-01:00', '2016-02-01T15:38:12Z' ], 148 | [ '2016-02-02T00:00:00.4567+01:30', '2016-02-01T22:30:00.4567Z' ], 149 | [ '2016-02-01T14:38:12.1+01:00', '2016-02-01T13:38:12.1Z' ] 150 | ].forEach(([input, output]) => { 151 | it(`serializes date-time-string ${input} into UTC date-time-string ${output}`, () => { 152 | expect(serializeDateTimeString(input)).toEqual(output) 153 | }) 154 | }) 155 | }) 156 | -------------------------------------------------------------------------------- /src/dateTime/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`GraphQLDateTime has a description 1`] = `"A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the \`date-time\` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar."`; 4 | 5 | exports[`GraphQLDateTime literial parsing errors when parsing invalid literal {"kind": "Document"} 1`] = `"DateTime cannot represent non string type null"`; 6 | 7 | exports[`GraphQLDateTime literial parsing errors when parsing invalid literal {"kind": "FloatValue", "value": "5"} 1`] = `"DateTime cannot represent non string type 5"`; 8 | 9 | exports[`GraphQLDateTime literial parsing errors when parsing invalid literal {"kind": "StringValue", "value": "2015-02-24T00:00:00.000+0100"} 1`] = `"DateTime cannot represent an invalid date-time-string 2015-02-24T00:00:00.000+0100."`; 10 | 11 | exports[`GraphQLDateTime literial parsing errors when parsing invalid literal {"kind": "StringValue", "value": "2016-02-01T00:00:00.Z"} 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T00:00:00.Z."`; 12 | 13 | exports[`GraphQLDateTime literial parsing errors when parsing invalid literal {"kind": "StringValue", "value": "2016-02-01T00:00Z"} 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T00:00Z."`; 14 | 15 | exports[`GraphQLDateTime literial parsing errors when parsing invalid literal {"kind": "StringValue", "value": "2016-02-01T000059Z"} 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T000059Z."`; 16 | 17 | exports[`GraphQLDateTime literial parsing errors when parsing invalid literal {"kind": "StringValue", "value": "2016-02-01T00Z"} 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T00Z."`; 18 | 19 | exports[`GraphQLDateTime literial parsing errors when parsing invalid literal {"kind": "StringValue", "value": "Invalid date"} 1`] = `"DateTime cannot represent an invalid date-time-string Invalid date."`; 20 | 21 | exports[`GraphQLDateTime serialization throws an error serializing the invalid unix timestamp -2147483649 1`] = `"DateTime cannot represent an invalid Unix timestamp -2147483649"`; 22 | 23 | exports[`GraphQLDateTime serialization throws an error serializing the invalid unix timestamp 2147483648 1`] = `"DateTime cannot represent an invalid Unix timestamp 2147483648"`; 24 | 25 | exports[`GraphQLDateTime serialization throws an error serializing the invalid unix timestamp Infinity 1`] = `"DateTime cannot represent an invalid Unix timestamp Infinity"`; 26 | 27 | exports[`GraphQLDateTime serialization throws an error serializing the invalid unix timestamp Infinity 2`] = `"DateTime cannot represent an invalid Unix timestamp Infinity"`; 28 | 29 | exports[`GraphQLDateTime serialization throws an error serializing the invalid unix timestamp NaN 1`] = `"DateTime cannot represent an invalid Unix timestamp NaN"`; 30 | 31 | exports[`GraphQLDateTime serialization throws an error when serializing an invalid date-string "2015-02-24T00:00:00.000+0100" 1`] = `"DateTime cannot represent an invalid date-time-string 2015-02-24T00:00:00.000+0100."`; 32 | 33 | exports[`GraphQLDateTime serialization throws an error when serializing an invalid date-string "2016-02-01T00:00:00.Z" 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T00:00:00.Z."`; 34 | 35 | exports[`GraphQLDateTime serialization throws an error when serializing an invalid date-string "2016-02-01T00:00Z" 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T00:00Z."`; 36 | 37 | exports[`GraphQLDateTime serialization throws an error when serializing an invalid date-string "2016-02-01T000059Z" 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T000059Z."`; 38 | 39 | exports[`GraphQLDateTime serialization throws an error when serializing an invalid date-string "2016-02-01T00Z" 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T00Z."`; 40 | 41 | exports[`GraphQLDateTime serialization throws an error when serializing an invalid date-string "Invalid date" 1`] = `"DateTime cannot represent an invalid date-time-string Invalid date."`; 42 | 43 | exports[`GraphQLDateTime serialization throws error when serializing [] 1`] = `"DateTime cannot be serialized from a non string, non numeric or non Date type []"`; 44 | 45 | exports[`GraphQLDateTime serialization throws error when serializing {} 1`] = `"DateTime cannot be serialized from a non string, non numeric or non Date type {}"`; 46 | 47 | exports[`GraphQLDateTime serialization throws error when serializing invalid date 1`] = `"DateTime cannot represent an invalid Date instance"`; 48 | 49 | exports[`GraphQLDateTime serialization throws error when serializing null 1`] = `"DateTime cannot be serialized from a non string, non numeric or non Date type null"`; 50 | 51 | exports[`GraphQLDateTime serialization throws error when serializing true 1`] = `"DateTime cannot be serialized from a non string, non numeric or non Date type true"`; 52 | 53 | exports[`GraphQLDateTime serialization throws error when serializing undefined 1`] = `"DateTime cannot be serialized from a non string, non numeric or non Date type undefined"`; 54 | 55 | exports[`GraphQLDateTime value parsing throws an error parsing an invalid date-string "2015-02-24T00:00:00.000+0100" 1`] = `"DateTime cannot represent an invalid date-time-string 2015-02-24T00:00:00.000+0100."`; 56 | 57 | exports[`GraphQLDateTime value parsing throws an error parsing an invalid date-string "2016-02-01T00:00:00.Z" 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T00:00:00.Z."`; 58 | 59 | exports[`GraphQLDateTime value parsing throws an error parsing an invalid date-string "2016-02-01T00:00Z" 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T00:00Z."`; 60 | 61 | exports[`GraphQLDateTime value parsing throws an error parsing an invalid date-string "2016-02-01T000059Z" 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T000059Z."`; 62 | 63 | exports[`GraphQLDateTime value parsing throws an error parsing an invalid date-string "2016-02-01T00Z" 1`] = `"DateTime cannot represent an invalid date-time-string 2016-02-01T00Z."`; 64 | 65 | exports[`GraphQLDateTime value parsing throws an error parsing an invalid date-string "Invalid date" 1`] = `"DateTime cannot represent an invalid date-time-string Invalid date."`; 66 | 67 | exports[`GraphQLDateTime value parsing throws an error when parsing [] 1`] = `"DateTime cannot represent non string type []"`; 68 | 69 | exports[`GraphQLDateTime value parsing throws an error when parsing {} 1`] = `"DateTime cannot represent non string type {}"`; 70 | 71 | exports[`GraphQLDateTime value parsing throws an error when parsing 4566 1`] = `"DateTime cannot represent non string type 4566"`; 72 | 73 | exports[`GraphQLDateTime value parsing throws an error when parsing null 1`] = `"DateTime cannot represent non string type null"`; 74 | 75 | exports[`GraphQLDateTime value parsing throws an error when parsing true 1`] = `"DateTime cannot represent non string type true"`; 76 | -------------------------------------------------------------------------------- /src/dateTime/integration.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * Copyright (c) 2018, Dirk-Jan Rutten 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | * 9 | */ 10 | 11 | import { graphql, GraphQLObjectType, GraphQLSchema, GraphQLError } from 'graphql' 12 | import GraphQLDateTime from './' 13 | 14 | const schema = new GraphQLSchema({ 15 | query: new GraphQLObjectType({ 16 | name: 'Query', 17 | fields: { 18 | validDate: { 19 | type: GraphQLDateTime, 20 | resolve: () => new Date('2016-05-02T10:31:42.2Z') 21 | }, 22 | validUTCDateString: { 23 | type: GraphQLDateTime, 24 | resolve: () => '1991-12-24T00:00:00Z' 25 | }, 26 | validDateString: { 27 | type: GraphQLDateTime, 28 | resolve: () => '2016-02-01T00:00:00-11:00' 29 | }, 30 | validUnixTimestamp: { 31 | type: GraphQLDateTime, 32 | resolve: () => 854325678 33 | }, 34 | invalidDateString: { 35 | type: GraphQLDateTime, 36 | resolve: () => '2017-01-001T00:00:00Z' 37 | }, 38 | invalidDate: { 39 | type: GraphQLDateTime, 40 | resolve: () => new Date('wrong') 41 | }, 42 | invalidUnixTimestamp: { 43 | type: GraphQLDateTime, 44 | resolve: () => Number.POSITIVE_INFINITY 45 | }, 46 | invalidType: { 47 | type: GraphQLDateTime, 48 | resolve: () => [] 49 | }, 50 | input: { 51 | type: GraphQLDateTime, 52 | args: { 53 | date: { 54 | type: GraphQLDateTime 55 | } 56 | }, 57 | resolve: (_, input: { date: Date }) => input.date 58 | } 59 | } 60 | }) 61 | }) 62 | 63 | it('executes a query that includes a DateTime', async () => { 64 | const query = ` 65 | query DateTest($date: DateTime!) { 66 | validDate 67 | validUTCDateString, 68 | validDateString 69 | validUnixTimestamp 70 | input(date: $date) 71 | inputNull: input 72 | } 73 | ` 74 | 75 | const variables = { date: '2017-10-01T00:00:00Z' } 76 | 77 | const response = await graphql(schema, query, null, null, variables) 78 | 79 | expect(response).toEqual({ 80 | data: { 81 | validDate: '2016-05-02T10:31:42.200Z', 82 | validUTCDateString: '1991-12-24T00:00:00Z', 83 | validDateString: '2016-02-01T11:00:00Z', 84 | input: '2017-10-01T00:00:00.000Z', 85 | validUnixTimestamp: '1997-01-27T00:41:18.000Z', 86 | inputNull: null 87 | } 88 | }) 89 | }) 90 | 91 | it('shifts an input date-time to UTC', async () => { 92 | const query = ` 93 | query DateTest($date: DateTime!) { 94 | input(date: $date) 95 | } 96 | ` 97 | 98 | const variables = { date: '2016-02-01T00:00:00-11:00' } 99 | 100 | const response = await graphql(schema, query, null, null, variables) 101 | 102 | expect(response).toEqual({ 103 | data: { 104 | input: '2016-02-01T11:00:00.000Z' 105 | } 106 | }) 107 | }) 108 | 109 | it('parses input to a JS Date', done => { 110 | const schema = new GraphQLSchema({ 111 | query: new GraphQLObjectType({ 112 | name: 'Query', 113 | fields: { 114 | input: { 115 | type: GraphQLDateTime, 116 | args: { 117 | date: { 118 | type: GraphQLDateTime 119 | } 120 | }, 121 | resolve: (_, input) => { 122 | try { 123 | expect(input.date).toEqual(new Date(Date.UTC(2016, 1, 1, 0, 0, 15))) 124 | done() 125 | } catch (e) { 126 | done.fail(e) 127 | } 128 | } 129 | } 130 | } 131 | }) 132 | }) 133 | 134 | const query = ` 135 | query DateTest($date: DateTime!) { 136 | input(date: $date) 137 | } 138 | ` 139 | const variables = { date: '2016-02-01T00:00:15Z' } 140 | 141 | graphql(schema, query, null, null, variables) 142 | }) 143 | 144 | it('errors if an invalid date-time is returned from the resolver', async () => { 145 | const query = ` 146 | { 147 | invalidDateString 148 | invalidDate 149 | invalidUnixTimestamp 150 | invalidType 151 | } 152 | ` 153 | 154 | const response = await graphql(schema, query) 155 | 156 | expect(response).toEqual({ 157 | data: { 158 | invalidDateString: null, 159 | invalidDate: null, 160 | invalidUnixTimestamp: null, 161 | invalidType: null 162 | }, 163 | errors: [ 164 | new GraphQLError('DateTime cannot represent an invalid date-time-string 2017-01-001T00:00:00Z.'), 165 | new GraphQLError('DateTime cannot represent an invalid Date instance'), 166 | new GraphQLError('DateTime cannot represent an invalid Unix timestamp Infinity'), 167 | new GraphQLError('DateTime cannot be serialized from a non string, non numeric or non Date type []') 168 | ] 169 | }) 170 | }) 171 | 172 | it('errors if the variable value is not a valid date-time', async () => { 173 | const query = ` 174 | query DateTest($date: DateTime!) { 175 | input(date: $date) 176 | } 177 | ` 178 | 179 | const variables = { date: '2017-10-001T00:00:00Z' } 180 | 181 | const response = await graphql(schema, query, null, null, variables) 182 | 183 | expect(response).toEqual({ 184 | errors: [ 185 | new GraphQLError('Variable "$date" got invalid value "2017-10-001T00:00:00Z"; Expected type DateTime; DateTime cannot represent an invalid date-time-string 2017-10-001T00:00:00Z.') 186 | ] 187 | }) 188 | }) 189 | 190 | it('errors if the variable value is not of type string', async () => { 191 | const query = ` 192 | query DateTest($date: DateTime!) { 193 | input(date: $date) 194 | } 195 | ` 196 | 197 | const variables = { date: 4 } 198 | 199 | const response = await graphql(schema, query, null, null, variables) 200 | 201 | expect(response).toEqual({ 202 | errors: [ 203 | new GraphQLError('Variable "$date" got invalid value 4; Expected type DateTime; DateTime cannot represent non string type 4') 204 | ] 205 | }) 206 | }) 207 | 208 | it('errors if the literal input value is not a valid date-time', async () => { 209 | const query = ` 210 | { 211 | input(date: "2017-10-001T00:00:00") 212 | } 213 | ` 214 | 215 | const response = await graphql(schema, query) 216 | 217 | expect(response).toEqual({ 218 | errors: [ 219 | new GraphQLError('Expected type DateTime, found "2017-10-001T00:00:00"; DateTime cannot represent an invalid date-time-string 2017-10-001T00:00:00.') 220 | ] 221 | }) 222 | }) 223 | 224 | it('errors if the literal input value in a query is not a string', async () => { 225 | const query = ` 226 | { 227 | input(date: 4) 228 | } 229 | ` 230 | 231 | const response = await graphql(schema, query) 232 | 233 | expect(response).toEqual({ 234 | errors: [ 235 | new GraphQLError('Expected type DateTime, found 4; DateTime cannot represent non string type 4') 236 | ] 237 | }) 238 | }) 239 | -------------------------------------------------------------------------------- /src/dateTime/index.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * Copyright (c) 2017, Dirk-Jan Rutten 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | * 9 | */ 10 | 11 | import GraphQLDateTime from './' 12 | import { Kind } from 'graphql' 13 | // flowlint-next-line untyped-import:off 14 | import { stringify } from 'jest-matcher-utils' 15 | 16 | const invalidDates = [ 17 | // General 18 | 'Invalid date', 19 | // Datetime with hours 20 | '2016-02-01T00Z', 21 | // Datetime with hours and minutes 22 | '2016-02-01T00:00Z', 23 | // Datetime with hours, minutes and seconds 24 | '2016-02-01T000059Z', 25 | // Datetime with hours, minutes, seconds and fractional seconds 26 | '2016-02-01T00:00:00.Z', 27 | // Datetime with hours, minutes, seconds, fractional seconds and timezone. 28 | '2015-02-24T00:00:00.000+0100' 29 | ] 30 | 31 | const validDates = [ 32 | // Datetime with hours, minutes and seconds 33 | [ '2016-02-01T00:00:15Z', new Date(Date.UTC(2016, 1, 1, 0, 0, 15)) ], 34 | [ '2016-02-01T00:00:00-11:00', new Date(Date.UTC(2016, 1, 1, 11)) ], 35 | [ '2017-01-07T11:25:00+01:00', new Date(Date.UTC(2017, 0, 7, 10, 25)) ], 36 | [ '2017-01-07T00:00:00+01:20', new Date(Date.UTC(2017, 0, 6, 22, 40)) ], 37 | // Datetime with hours, minutes, seconds and fractional seconds 38 | [ '2016-02-01T00:00:00.1Z', new Date(Date.UTC(2016, 1, 1, 0, 0, 0, 100)) ], 39 | [ '2016-02-01T00:00:00.000Z', new Date(Date.UTC(2016, 1, 1, 0, 0, 0, 0)) ], 40 | [ '2016-02-01T00:00:00.990Z', new Date(Date.UTC(2016, 1, 1, 0, 0, 0, 990)) ], 41 | [ '2016-02-01T00:00:00.23498Z', new Date(Date.UTC(2016, 1, 1, 0, 0, 0, 234)) ], 42 | [ '2017-01-07T11:25:00.450+01:00', new Date(Date.UTC(2017, 0, 7, 10, 25, 0, 450)) ] 43 | ] 44 | 45 | describe('GraphQLDateTime', () => { 46 | it('has a description', () => { 47 | expect(GraphQLDateTime.description).toMatchSnapshot() 48 | }) 49 | 50 | describe('serialization', () => { 51 | [ 52 | {}, 53 | [], 54 | null, 55 | undefined, 56 | true 57 | ].forEach(invalidInput => { 58 | it(`throws error when serializing ${stringify(invalidInput)}`, () => { 59 | expect(() => 60 | GraphQLDateTime.serialize(invalidInput) 61 | ).toThrowErrorMatchingSnapshot() 62 | }) 63 | }); 64 | 65 | [ 66 | [ new Date(Date.UTC(2016, 0, 1)), '2016-01-01T00:00:00.000Z' ], 67 | [ new Date(Date.UTC(2016, 0, 1, 14, 48, 10, 30)), '2016-01-01T14:48:10.030Z' ] 68 | ].forEach(([ value, expected ]) => { 69 | it(`serializes javascript Date ${stringify(value)} into ${stringify(expected)}`, () => { 70 | expect( 71 | GraphQLDateTime.serialize(value) 72 | ).toEqual(expected) 73 | }) 74 | }) 75 | 76 | it(`throws error when serializing invalid date`, () => { 77 | expect(() => 78 | GraphQLDateTime.serialize(new Date('invalid date')) 79 | ).toThrowErrorMatchingSnapshot() 80 | }); 81 | 82 | [ 83 | [ '2016-02-01T00:00:15Z', '2016-02-01T00:00:15Z' ], 84 | [ '2016-02-01T00:00:00.23498Z', '2016-02-01T00:00:00.23498Z' ], 85 | [ '2016-02-01T00:00:00-11:00', '2016-02-01T11:00:00Z' ], 86 | [ '2017-01-07T00:00:00.1+01:20', '2017-01-06T22:40:00.1Z' ] 87 | ].forEach(([input, output]) => { 88 | it(`serializes date-time-string ${input} into UTC date-time-string ${output}`, () => { 89 | expect( 90 | GraphQLDateTime.serialize(input) 91 | ).toEqual(output) 92 | }) 93 | }) 94 | 95 | invalidDates.forEach(dateString => { 96 | it(`throws an error when serializing an invalid date-string ${stringify(dateString)}`, () => { 97 | expect(() => 98 | GraphQLDateTime.serialize(dateString) 99 | ).toThrowErrorMatchingSnapshot() 100 | }) 101 | }); 102 | 103 | // Serializes Unix timestamp 104 | [ 105 | [ 854325678, '1997-01-27T00:41:18.000Z' ], 106 | [ 876535, '1970-01-11T03:28:55.000Z' ], 107 | // The maximum representable unix timestamp 108 | [ 2147483647, '2038-01-19T03:14:07.000Z' ], 109 | // The minimum representable unit timestamp 110 | [ -2147483648, '1901-12-13T20:45:52.000Z' ] 111 | ].forEach(([ value, expected ]) => { 112 | it(`serializes unix timestamp ${stringify(value)} into date-string ${expected}`, () => { 113 | expect( 114 | GraphQLDateTime.serialize(value) 115 | ).toEqual(expected) 116 | }) 117 | }); 118 | 119 | [ 120 | Number.NaN, 121 | Number.POSITIVE_INFINITY, 122 | Number.POSITIVE_INFINITY, 123 | // assume Unix timestamp are 32-bit 124 | 2147483648, 125 | -2147483649 126 | ].forEach(value => { 127 | it(`throws an error serializing the invalid unix timestamp ${stringify(value)}`, () => { 128 | expect(() => 129 | GraphQLDateTime.serialize(value) 130 | ).toThrowErrorMatchingSnapshot() 131 | }) 132 | }) 133 | }) 134 | 135 | describe('value parsing', () => { 136 | validDates.forEach(([ value, expected ]) => { 137 | it(`parses date-string ${stringify(value)} into javascript Date ${stringify(expected)}`, () => { 138 | expect( 139 | GraphQLDateTime.parseValue(value) 140 | ).toEqual(expected) 141 | }) 142 | }); 143 | 144 | [ 145 | 4566, 146 | {}, 147 | [], 148 | true, 149 | null 150 | ].forEach(invalidInput => { 151 | it(`throws an error when parsing ${stringify(invalidInput)}`, () => { 152 | expect(() => 153 | GraphQLDateTime.parseValue(invalidInput) 154 | ).toThrowErrorMatchingSnapshot() 155 | }) 156 | }) 157 | 158 | invalidDates.forEach(dateString => { 159 | it(`throws an error parsing an invalid date-string ${stringify(dateString)}`, () => { 160 | expect(() => 161 | GraphQLDateTime.parseValue(dateString) 162 | ).toThrowErrorMatchingSnapshot() 163 | }) 164 | }) 165 | }) 166 | 167 | describe('literial parsing', () => { 168 | validDates.forEach(([ value, expected ]) => { 169 | const literal = { 170 | kind: Kind.STRING, value 171 | } 172 | 173 | it(`parses literal ${stringify(literal)} into javascript Date ${stringify(expected)}`, () => { 174 | expect( 175 | GraphQLDateTime.parseLiteral(literal) 176 | ).toEqual(expected) 177 | }) 178 | }) 179 | 180 | invalidDates.forEach(value => { 181 | const invalidLiteral = { 182 | kind: Kind.STRING, value 183 | } 184 | it(`errors when parsing invalid literal ${stringify(invalidLiteral)}`, () => { 185 | expect(() => 186 | GraphQLDateTime.parseLiteral(invalidLiteral) 187 | ).toThrowErrorMatchingSnapshot() 188 | }) 189 | }); 190 | 191 | [{ 192 | kind: Kind.FLOAT, value: '5' 193 | }, ({ 194 | kind: Kind.DOCUMENT 195 | // flowlint-next-line unclear-type:off 196 | }: any)].forEach(literal => { 197 | it(`errors when parsing invalid literal ${stringify(literal)}`, () => { 198 | expect(() => 199 | GraphQLDateTime.parseLiteral(literal) 200 | ).toThrowErrorMatchingSnapshot() 201 | }) 202 | }) 203 | }) 204 | }) 205 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQL ISO Date 2 | 3 | 4 | 5 | [![npm version](https://badge.fury.io/js/graphql-iso-date.svg)](http://badge.fury.io/js/graphql-iso-date) 6 | [![Build Status](https://travis-ci.org/excitement-engineer/graphql-iso-date.svg?branch=master)](https://travis-ci.org/excitement-engineer/graphql-iso-date) 7 | [![codecov](https://codecov.io/gh/excitement-engineer/graphql-iso-date/branch/master/graph/badge.svg)](https://codecov.io/gh/excitement-engineer/graphql-iso-date) 8 | 9 | **NOTICE: The scalars defined in this repository have moved to the [GraphQL-scalars](https://github.com/Urigo/graphql-scalars) repository where they will be maintained.** 10 | 11 | GraphQL ISO Date is a set of [RFC 3339](./rfc3339.txt) compliant date/time scalar types to be used with [graphQL.js](https://github.com/graphql/graphql-js). 12 | 13 | > RFC 3339 *"defines a date and time format for use in Internet 14 | protocols that is a profile of the ISO 8601 standard for 15 | representation of dates and times using the Gregorian calendar."* 16 | 17 | > **Date and Time on the Internet: Timestamps, July 2002.** 18 | 19 | A basic understanding of [GraphQL](http://facebook.github.io/graphql/) and of the [graphQL.js](https://github.com/graphql/graphql-js) implementation is needed to provide context for this library. 20 | 21 | This library contains the following scalars: 22 | 23 | - `Date`: A date string, such as 2007-12-03. 24 | - `Time`: A time string at UTC, such as 10:15:30Z 25 | - `DateTime`: A date-time string at UTC, such as 2007-12-03T10:15:30Z. 26 | 27 | ## Getting started 28 | 29 | Install `graphql-iso-date` using yarn 30 | 31 | ```sh 32 | yarn add graphql-iso-date 33 | ``` 34 | 35 | Or using npm 36 | 37 | ```sh 38 | npm install --save graphql-iso-date 39 | ``` 40 | 41 | GraphQL ISO Date exposes 3 different date/time scalars that can be used in combination with [graphQL.js](https://github.com/graphql/graphql-js). Let's build a simple schema using the scalars included in this library and execute a query: 42 | 43 | ```js 44 | import { 45 | graphql, 46 | GraphQLObjectType, 47 | GraphQLSchema, 48 | } from 'graphql'; 49 | 50 | import { 51 | GraphQLDate, 52 | GraphQLTime, 53 | GraphQLDateTime 54 | } from 'graphql-iso-date'; 55 | 56 | const schema = new GraphQLSchema({ 57 | query: new GraphQLObjectType({ 58 | name: 'Query', 59 | fields: { 60 | birthdate: { 61 | type: GraphQLDate, 62 | //resolver can take a Date or date string. 63 | resolve: () => new Date(1991, 11, 24) 64 | }, 65 | openingNYSE: { 66 | type: GraphQLTime, 67 | //resolver can take a Date or time string. 68 | resolve: () => new Date(Date.UTC(2017, 0, 10, 14, 30)) 69 | }, 70 | instant: { 71 | type: GraphQLDateTime, 72 | // resolver can take Date, date-time string or Unix timestamp (number). 73 | resolve: () => new Date(Date.UTC(2017, 0, 10, 21, 33, 15, 233)) 74 | } 75 | } 76 | }) 77 | }); 78 | 79 | const query = ` 80 | { 81 | birthdate 82 | openingNYSE 83 | instant 84 | } 85 | `; 86 | 87 | graphql(schema, query).then(result => { 88 | 89 | // Prints 90 | // { 91 | // data: { 92 | // birthdate: '1991-12-24', 93 | // openingNYSE: '14:30:00.000Z', 94 | // instant: '2017-01-10T21:33:15.233Z' 95 | // } 96 | // } 97 | console.log(result); 98 | }); 99 | ``` 100 | 101 | ## Examples 102 | 103 | This project includes several examples in the folder `/examples` explaining how to use the various scalars. You can also see some live editable examples on Launchpad: 104 | 105 | * [returning Date, Time, and DateTime](https://codesandbox.io/s/k9xm5z7223) 106 | * [taking a Date as a query parameter](https://codesandbox.io/s/m5782nj1w9) 107 | 108 | Run the examples by downloading this project and running the following commands: 109 | 110 | Install dependencies using yarn 111 | 112 | ```sh 113 | yarn 114 | ``` 115 | 116 | Or npm 117 | 118 | ```sh 119 | npm install 120 | ``` 121 | 122 | Run the examples 123 | 124 | ``` 125 | npm run examples 126 | ``` 127 | 128 | ## Scalars 129 | 130 | This section provides a detailed description of each of the scalars. 131 | 132 | > A reference is made to `coercion` in the description below. For further clarification on the meaning of this term, please refer to the GraphQL [spec](http://facebook.github.io/graphql/#sec-Scalars). 133 | 134 | ### Date 135 | 136 | A date string, such as 2007-12-03, compliant with the `full-date` format outlined in section 5.6 of the [RFC 3339](./rfc3339.txt) profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. 137 | 138 | This scalar is a description of the date, as used for birthdays for example. It cannot represent an instant on the time-line. 139 | 140 | **Result Coercion** 141 | 142 | Javascript Date instances are coerced to an RFC 3339 compliant date string. Invalid Date instances raise a field error. 143 | 144 | **Input Coercion** 145 | 146 | When expected as an input type, only RFC 3339 compliant date strings are accepted. All other input values raise a query error indicating an incorrect type. 147 | 148 | ### Time 149 | 150 | A time string at UTC, such as 10:15:30Z, compliant with the `full-time` format outlined in section 5.6 of the [RFC 3339](./rfc3339.txt) profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. 151 | 152 | This scalar is a description of a time instant such as the opening bell of the New York Stock Exchange for example. It cannot represent an exact instant on the time-line. 153 | 154 | This scalar ignores leap seconds (thereby assuming that a minute constitutes of 59 seconds), in this respect it diverges from the RFC 3339 profile. 155 | 156 | Where an RFC 3339 compliant time string has a time-zone other than UTC, it is shifted to UTC. For example, the time string "14:10:20+01:00" is shifted to "13:10:20Z". 157 | 158 | **Result Coercion** 159 | 160 | Javascript Date instances are coerced to an RFC 3339 compliant time string by extracting the UTC time part. Invalid Date instances raise a field error. 161 | 162 | **Input Coercion** 163 | 164 | When expected as an input type, only RFC 3339 compliant time strings are accepted. All other input values raise a query error indicating an incorrect type. 165 | 166 | ### DateTime 167 | 168 | A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the [RFC 3339](./rfc3339.txt) profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. 169 | 170 | This scalar is a description of an exact instant on the time-line such as the instant that a user account was created. 171 | 172 | This scalar ignores leap seconds (thereby assuming that a minute constitutes of 59 seconds). In this respect it diverges from the RFC 3339 profile. 173 | 174 | Where an RFC 3339 compliant date-time string has a time-zone other than UTC, it is shifted to UTC. For example, the date-time string "2016-01-01T14:10:20+01:00" is shifted to "2016-01-01T13:10:20Z". 175 | 176 | **Result Coercion** 177 | 178 | JavaScript Date instances and Unix timestamps (represented as 32-bit signed integers) are coerced to RFC 3339 compliant date-time strings. Invalid Date instances raise a field error. 179 | 180 | **Input Coercion** 181 | 182 | When expected as an input type, only RFC 3339 compliant date-time strings are accepted. All other input values raise a query error indicating an incorrect type. 183 | -------------------------------------------------------------------------------- /flow-typed/npm/jest_v22.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 6e1fc0a644aa956f79029fec0709e597 2 | // flow-typed version: 07ebad4796/jest_v22.x.x/flow_>=v0.39.x 3 | 4 | type JestMockFn, TReturn> = { 5 | (...args: TArguments): TReturn, 6 | /** 7 | * An object for introspecting mock calls 8 | */ 9 | mock: { 10 | /** 11 | * An array that represents all calls that have been made into this mock 12 | * function. Each call is represented by an array of arguments that were 13 | * passed during the call. 14 | */ 15 | calls: Array, 16 | /** 17 | * An array that contains all the object instances that have been 18 | * instantiated from this mock function. 19 | */ 20 | instances: Array 21 | }, 22 | /** 23 | * Resets all information stored in the mockFn.mock.calls and 24 | * mockFn.mock.instances arrays. Often this is useful when you want to clean 25 | * up a mock's usage data between two assertions. 26 | */ 27 | mockClear(): void, 28 | /** 29 | * Resets all information stored in the mock. This is useful when you want to 30 | * completely restore a mock back to its initial state. 31 | */ 32 | mockReset(): void, 33 | /** 34 | * Removes the mock and restores the initial implementation. This is useful 35 | * when you want to mock functions in certain test cases and restore the 36 | * original implementation in others. Beware that mockFn.mockRestore only 37 | * works when mock was created with jest.spyOn. Thus you have to take care of 38 | * restoration yourself when manually assigning jest.fn(). 39 | */ 40 | mockRestore(): void, 41 | /** 42 | * Accepts a function that should be used as the implementation of the mock. 43 | * The mock itself will still record all calls that go into and instances 44 | * that come from itself -- the only difference is that the implementation 45 | * will also be executed when the mock is called. 46 | */ 47 | mockImplementation( 48 | fn: (...args: TArguments) => TReturn 49 | ): JestMockFn, 50 | /** 51 | * Accepts a function that will be used as an implementation of the mock for 52 | * one call to the mocked function. Can be chained so that multiple function 53 | * calls produce different results. 54 | */ 55 | mockImplementationOnce( 56 | fn: (...args: TArguments) => TReturn 57 | ): JestMockFn, 58 | /** 59 | * Just a simple sugar function for returning `this` 60 | */ 61 | mockReturnThis(): void, 62 | /** 63 | * Deprecated: use jest.fn(() => value) instead 64 | */ 65 | mockReturnValue(value: TReturn): JestMockFn, 66 | /** 67 | * Sugar for only returning a value once inside your mock 68 | */ 69 | mockReturnValueOnce(value: TReturn): JestMockFn 70 | }; 71 | 72 | type JestAsymmetricEqualityType = { 73 | /** 74 | * A custom Jasmine equality tester 75 | */ 76 | asymmetricMatch(value: mixed): boolean 77 | }; 78 | 79 | type JestCallsType = { 80 | allArgs(): mixed, 81 | all(): mixed, 82 | any(): boolean, 83 | count(): number, 84 | first(): mixed, 85 | mostRecent(): mixed, 86 | reset(): void 87 | }; 88 | 89 | type JestClockType = { 90 | install(): void, 91 | mockDate(date: Date): void, 92 | tick(milliseconds?: number): void, 93 | uninstall(): void 94 | }; 95 | 96 | type JestMatcherResult = { 97 | message?: string | (() => string), 98 | pass: boolean 99 | }; 100 | 101 | type JestMatcher = (actual: any, expected: any) => JestMatcherResult; 102 | 103 | type JestPromiseType = { 104 | /** 105 | * Use rejects to unwrap the reason of a rejected promise so any other 106 | * matcher can be chained. If the promise is fulfilled the assertion fails. 107 | */ 108 | rejects: JestExpectType, 109 | /** 110 | * Use resolves to unwrap the value of a fulfilled promise so any other 111 | * matcher can be chained. If the promise is rejected the assertion fails. 112 | */ 113 | resolves: JestExpectType 114 | }; 115 | 116 | /** 117 | * Plugin: jest-enzyme 118 | */ 119 | type EnzymeMatchersType = { 120 | toBeChecked(): void, 121 | toBeDisabled(): void, 122 | toBeEmpty(): void, 123 | toBePresent(): void, 124 | toContainReact(element: React$Element): void, 125 | toHaveClassName(className: string): void, 126 | toHaveHTML(html: string): void, 127 | toHaveProp(propKey: string, propValue?: any): void, 128 | toHaveRef(refName: string): void, 129 | toHaveState(stateKey: string, stateValue?: any): void, 130 | toHaveStyle(styleKey: string, styleValue?: any): void, 131 | toHaveTagName(tagName: string): void, 132 | toHaveText(text: string): void, 133 | toIncludeText(text: string): void, 134 | toHaveValue(value: any): void, 135 | toMatchElement(element: React$Element): void, 136 | toMatchSelector(selector: string): void 137 | }; 138 | 139 | type JestExpectType = { 140 | not: JestExpectType & EnzymeMatchersType, 141 | /** 142 | * If you have a mock function, you can use .lastCalledWith to test what 143 | * arguments it was last called with. 144 | */ 145 | lastCalledWith(...args: Array): void, 146 | /** 147 | * toBe just checks that a value is what you expect. It uses === to check 148 | * strict equality. 149 | */ 150 | toBe(value: any): void, 151 | /** 152 | * Use .toHaveBeenCalled to ensure that a mock function got called. 153 | */ 154 | toBeCalled(): void, 155 | /** 156 | * Use .toBeCalledWith to ensure that a mock function was called with 157 | * specific arguments. 158 | */ 159 | toBeCalledWith(...args: Array): void, 160 | /** 161 | * Using exact equality with floating point numbers is a bad idea. Rounding 162 | * means that intuitive things fail. 163 | */ 164 | toBeCloseTo(num: number, delta: any): void, 165 | /** 166 | * Use .toBeDefined to check that a variable is not undefined. 167 | */ 168 | toBeDefined(): void, 169 | /** 170 | * Use .toBeFalsy when you don't care what a value is, you just want to 171 | * ensure a value is false in a boolean context. 172 | */ 173 | toBeFalsy(): void, 174 | /** 175 | * To compare floating point numbers, you can use toBeGreaterThan. 176 | */ 177 | toBeGreaterThan(number: number): void, 178 | /** 179 | * To compare floating point numbers, you can use toBeGreaterThanOrEqual. 180 | */ 181 | toBeGreaterThanOrEqual(number: number): void, 182 | /** 183 | * To compare floating point numbers, you can use toBeLessThan. 184 | */ 185 | toBeLessThan(number: number): void, 186 | /** 187 | * To compare floating point numbers, you can use toBeLessThanOrEqual. 188 | */ 189 | toBeLessThanOrEqual(number: number): void, 190 | /** 191 | * Use .toBeInstanceOf(Class) to check that an object is an instance of a 192 | * class. 193 | */ 194 | toBeInstanceOf(cls: Class<*>): void, 195 | /** 196 | * .toBeNull() is the same as .toBe(null) but the error messages are a bit 197 | * nicer. 198 | */ 199 | toBeNull(): void, 200 | /** 201 | * Use .toBeTruthy when you don't care what a value is, you just want to 202 | * ensure a value is true in a boolean context. 203 | */ 204 | toBeTruthy(): void, 205 | /** 206 | * Use .toBeUndefined to check that a variable is undefined. 207 | */ 208 | toBeUndefined(): void, 209 | /** 210 | * Use .toContain when you want to check that an item is in a list. For 211 | * testing the items in the list, this uses ===, a strict equality check. 212 | */ 213 | toContain(item: any): void, 214 | /** 215 | * Use .toContainEqual when you want to check that an item is in a list. For 216 | * testing the items in the list, this matcher recursively checks the 217 | * equality of all fields, rather than checking for object identity. 218 | */ 219 | toContainEqual(item: any): void, 220 | /** 221 | * Use .toEqual when you want to check that two objects have the same value. 222 | * This matcher recursively checks the equality of all fields, rather than 223 | * checking for object identity. 224 | */ 225 | toEqual(value: any): void, 226 | /** 227 | * Use .toHaveBeenCalled to ensure that a mock function got called. 228 | */ 229 | toHaveBeenCalled(): void, 230 | /** 231 | * Use .toHaveBeenCalledTimes to ensure that a mock function got called exact 232 | * number of times. 233 | */ 234 | toHaveBeenCalledTimes(number: number): void, 235 | /** 236 | * Use .toHaveBeenCalledWith to ensure that a mock function was called with 237 | * specific arguments. 238 | */ 239 | toHaveBeenCalledWith(...args: Array): void, 240 | /** 241 | * Use .toHaveBeenLastCalledWith to ensure that a mock function was last called 242 | * with specific arguments. 243 | */ 244 | toHaveBeenLastCalledWith(...args: Array): void, 245 | /** 246 | * Check that an object has a .length property and it is set to a certain 247 | * numeric value. 248 | */ 249 | toHaveLength(number: number): void, 250 | /** 251 | * 252 | */ 253 | toHaveProperty(propPath: string, value?: any): void, 254 | /** 255 | * Use .toMatch to check that a string matches a regular expression or string. 256 | */ 257 | toMatch(regexpOrString: RegExp | string): void, 258 | /** 259 | * Use .toMatchObject to check that a javascript object matches a subset of the properties of an object. 260 | */ 261 | toMatchObject(object: Object | Array): void, 262 | /** 263 | * This ensures that a React component matches the most recent snapshot. 264 | */ 265 | toMatchSnapshot(name?: string): void, 266 | /** 267 | * Use .toThrow to test that a function throws when it is called. 268 | * If you want to test that a specific error gets thrown, you can provide an 269 | * argument to toThrow. The argument can be a string for the error message, 270 | * a class for the error, or a regex that should match the error. 271 | * 272 | * Alias: .toThrowError 273 | */ 274 | toThrow(message?: string | Error | Class | RegExp): void, 275 | toThrowError(message?: string | Error | Class | RegExp): void, 276 | /** 277 | * Use .toThrowErrorMatchingSnapshot to test that a function throws a error 278 | * matching the most recent snapshot when it is called. 279 | */ 280 | toThrowErrorMatchingSnapshot(): void 281 | }; 282 | 283 | type JestObjectType = { 284 | /** 285 | * Disables automatic mocking in the module loader. 286 | * 287 | * After this method is called, all `require()`s will return the real 288 | * versions of each module (rather than a mocked version). 289 | */ 290 | disableAutomock(): JestObjectType, 291 | /** 292 | * An un-hoisted version of disableAutomock 293 | */ 294 | autoMockOff(): JestObjectType, 295 | /** 296 | * Enables automatic mocking in the module loader. 297 | */ 298 | enableAutomock(): JestObjectType, 299 | /** 300 | * An un-hoisted version of enableAutomock 301 | */ 302 | autoMockOn(): JestObjectType, 303 | /** 304 | * Clears the mock.calls and mock.instances properties of all mocks. 305 | * Equivalent to calling .mockClear() on every mocked function. 306 | */ 307 | clearAllMocks(): JestObjectType, 308 | /** 309 | * Resets the state of all mocks. Equivalent to calling .mockReset() on every 310 | * mocked function. 311 | */ 312 | resetAllMocks(): JestObjectType, 313 | /** 314 | * Restores all mocks back to their original value. 315 | */ 316 | restoreAllMocks(): JestObjectType, 317 | /** 318 | * Removes any pending timers from the timer system. 319 | */ 320 | clearAllTimers(): void, 321 | /** 322 | * The same as `mock` but not moved to the top of the expectation by 323 | * babel-jest. 324 | */ 325 | doMock(moduleName: string, moduleFactory?: any): JestObjectType, 326 | /** 327 | * The same as `unmock` but not moved to the top of the expectation by 328 | * babel-jest. 329 | */ 330 | dontMock(moduleName: string): JestObjectType, 331 | /** 332 | * Returns a new, unused mock function. Optionally takes a mock 333 | * implementation. 334 | */ 335 | fn, TReturn>( 336 | implementation?: (...args: TArguments) => TReturn 337 | ): JestMockFn, 338 | /** 339 | * Determines if the given function is a mocked function. 340 | */ 341 | isMockFunction(fn: Function): boolean, 342 | /** 343 | * Given the name of a module, use the automatic mocking system to generate a 344 | * mocked version of the module for you. 345 | */ 346 | genMockFromModule(moduleName: string): any, 347 | /** 348 | * Mocks a module with an auto-mocked version when it is being required. 349 | * 350 | * The second argument can be used to specify an explicit module factory that 351 | * is being run instead of using Jest's automocking feature. 352 | * 353 | * The third argument can be used to create virtual mocks -- mocks of modules 354 | * that don't exist anywhere in the system. 355 | */ 356 | mock( 357 | moduleName: string, 358 | moduleFactory?: any, 359 | options?: Object 360 | ): JestObjectType, 361 | /** 362 | * Returns the actual module instead of a mock, bypassing all checks on 363 | * whether the module should receive a mock implementation or not. 364 | */ 365 | requireActual(moduleName: string): any, 366 | /** 367 | * Returns a mock module instead of the actual module, bypassing all checks 368 | * on whether the module should be required normally or not. 369 | */ 370 | requireMock(moduleName: string): any, 371 | /** 372 | * Resets the module registry - the cache of all required modules. This is 373 | * useful to isolate modules where local state might conflict between tests. 374 | */ 375 | resetModules(): JestObjectType, 376 | /** 377 | * Exhausts the micro-task queue (usually interfaced in node via 378 | * process.nextTick). 379 | */ 380 | runAllTicks(): void, 381 | /** 382 | * Exhausts the macro-task queue (i.e., all tasks queued by setTimeout(), 383 | * setInterval(), and setImmediate()). 384 | */ 385 | runAllTimers(): void, 386 | /** 387 | * Exhausts all tasks queued by setImmediate(). 388 | */ 389 | runAllImmediates(): void, 390 | /** 391 | * Executes only the macro task queue (i.e. all tasks queued by setTimeout() 392 | * or setInterval() and setImmediate()). 393 | */ 394 | runTimersToTime(msToRun: number): void, 395 | /** 396 | * Executes only the macro-tasks that are currently pending (i.e., only the 397 | * tasks that have been queued by setTimeout() or setInterval() up to this 398 | * point) 399 | */ 400 | runOnlyPendingTimers(): void, 401 | /** 402 | * Explicitly supplies the mock object that the module system should return 403 | * for the specified module. Note: It is recommended to use jest.mock() 404 | * instead. 405 | */ 406 | setMock(moduleName: string, moduleExports: any): JestObjectType, 407 | /** 408 | * Indicates that the module system should never return a mocked version of 409 | * the specified module from require() (e.g. that it should always return the 410 | * real module). 411 | */ 412 | unmock(moduleName: string): JestObjectType, 413 | /** 414 | * Instructs Jest to use fake versions of the standard timer functions 415 | * (setTimeout, setInterval, clearTimeout, clearInterval, nextTick, 416 | * setImmediate and clearImmediate). 417 | */ 418 | useFakeTimers(): JestObjectType, 419 | /** 420 | * Instructs Jest to use the real versions of the standard timer functions. 421 | */ 422 | useRealTimers(): JestObjectType, 423 | /** 424 | * Creates a mock function similar to jest.fn but also tracks calls to 425 | * object[methodName]. 426 | */ 427 | spyOn(object: Object, methodName: string): JestMockFn, 428 | /** 429 | * Set the default timeout interval for tests and before/after hooks in milliseconds. 430 | * Note: The default timeout interval is 5 seconds if this method is not called. 431 | */ 432 | setTimeout(timeout: number): JestObjectType 433 | }; 434 | 435 | type JestSpyType = { 436 | calls: JestCallsType 437 | }; 438 | 439 | /** Runs this function after every test inside this context */ 440 | declare function afterEach( 441 | fn: (done: () => void) => ?Promise, 442 | timeout?: number 443 | ): void; 444 | /** Runs this function before every test inside this context */ 445 | declare function beforeEach( 446 | fn: (done: () => void) => ?Promise, 447 | timeout?: number 448 | ): void; 449 | /** Runs this function after all tests have finished inside this context */ 450 | declare function afterAll( 451 | fn: (done: () => void) => ?Promise, 452 | timeout?: number 453 | ): void; 454 | /** Runs this function before any tests have started inside this context */ 455 | declare function beforeAll( 456 | fn: (done: () => void) => ?Promise, 457 | timeout?: number 458 | ): void; 459 | 460 | /** A context for grouping tests together */ 461 | declare var describe: { 462 | /** 463 | * Creates a block that groups together several related tests in one "test suite" 464 | */ 465 | (name: string, fn: () => void): void, 466 | 467 | /** 468 | * Only run this describe block 469 | */ 470 | only(name: string, fn: () => void): void, 471 | 472 | /** 473 | * Skip running this describe block 474 | */ 475 | skip(name: string, fn: () => void): void 476 | }; 477 | 478 | /** An individual test unit */ 479 | declare var it: { 480 | /** 481 | * An individual test unit 482 | * 483 | * @param {string} Name of Test 484 | * @param {Function} Test 485 | * @param {number} Timeout for the test, in milliseconds. 486 | */ 487 | ( 488 | name: string, 489 | fn?: (done: () => void) => ?Promise, 490 | timeout?: number 491 | ): void, 492 | /** 493 | * Only run this test 494 | * 495 | * @param {string} Name of Test 496 | * @param {Function} Test 497 | * @param {number} Timeout for the test, in milliseconds. 498 | */ 499 | only( 500 | name: string, 501 | fn?: (done: () => void) => ?Promise, 502 | timeout?: number 503 | ): void, 504 | /** 505 | * Skip running this test 506 | * 507 | * @param {string} Name of Test 508 | * @param {Function} Test 509 | * @param {number} Timeout for the test, in milliseconds. 510 | */ 511 | skip( 512 | name: string, 513 | fn?: (done: () => void) => ?Promise, 514 | timeout?: number 515 | ): void, 516 | /** 517 | * Run the test concurrently 518 | * 519 | * @param {string} Name of Test 520 | * @param {Function} Test 521 | * @param {number} Timeout for the test, in milliseconds. 522 | */ 523 | concurrent( 524 | name: string, 525 | fn?: (done: () => void) => ?Promise, 526 | timeout?: number 527 | ): void 528 | }; 529 | declare function fit( 530 | name: string, 531 | fn: (done: () => void) => ?Promise, 532 | timeout?: number 533 | ): void; 534 | /** An individual test unit */ 535 | declare var test: typeof it; 536 | /** A disabled group of tests */ 537 | declare var xdescribe: typeof describe; 538 | /** A focused group of tests */ 539 | declare var fdescribe: typeof describe; 540 | /** A disabled individual test */ 541 | declare var xit: typeof it; 542 | /** A disabled individual test */ 543 | declare var xtest: typeof it; 544 | 545 | /** The expect function is used every time you want to test a value */ 546 | declare var expect: { 547 | /** The object that you want to make assertions against */ 548 | (value: any): JestExpectType & JestPromiseType & EnzymeMatchersType, 549 | /** Add additional Jasmine matchers to Jest's roster */ 550 | extend(matchers: { [name: string]: JestMatcher }): void, 551 | /** Add a module that formats application-specific data structures. */ 552 | addSnapshotSerializer(serializer: (input: Object) => string): void, 553 | assertions(expectedAssertions: number): void, 554 | hasAssertions(): void, 555 | any(value: mixed): JestAsymmetricEqualityType, 556 | anything(): void, 557 | arrayContaining(value: Array): void, 558 | objectContaining(value: Object): void, 559 | /** Matches any received string that contains the exact expected string. */ 560 | stringContaining(value: string): void, 561 | stringMatching(value: string | RegExp): void 562 | }; 563 | 564 | // TODO handle return type 565 | // http://jasmine.github.io/2.4/introduction.html#section-Spies 566 | declare function spyOn(value: mixed, method: string): Object; 567 | 568 | /** Holds all functions related to manipulating test runner */ 569 | declare var jest: JestObjectType; 570 | 571 | /** 572 | * The global Jasmine object, this is generally not exposed as the public API, 573 | * using features inside here could break in later versions of Jest. 574 | */ 575 | declare var jasmine: { 576 | DEFAULT_TIMEOUT_INTERVAL: number, 577 | any(value: mixed): JestAsymmetricEqualityType, 578 | anything(): void, 579 | arrayContaining(value: Array): void, 580 | clock(): JestClockType, 581 | createSpy(name: string): JestSpyType, 582 | createSpyObj( 583 | baseName: string, 584 | methodNames: Array 585 | ): { [methodName: string]: JestSpyType }, 586 | objectContaining(value: Object): void, 587 | stringMatching(value: string): void 588 | }; 589 | -------------------------------------------------------------------------------- /rfc3339.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Network Working Group G. Klyne 8 | Request for Comments: 3339 Clearswift Corporation 9 | Category: Standards Track C. Newman 10 | Sun Microsystems 11 | July 2002 12 | 13 | 14 | Date and Time on the Internet: Timestamps 15 | 16 | Status of this Memo 17 | 18 | This document specifies an Internet standards track protocol for the 19 | Internet community, and requests discussion and suggestions for 20 | improvements. Please refer to the current edition of the "Internet 21 | Official Protocol Standards" (STD 1) for the standardization state 22 | and status of this protocol. Distribution of this memo is unlimited. 23 | 24 | Copyright Notice 25 | 26 | Copyright (C) The Internet Society (2002). All Rights Reserved. 27 | 28 | Abstract 29 | 30 | This document defines a date and time format for use in Internet 31 | protocols that is a profile of the ISO 8601 standard for 32 | representation of dates and times using the Gregorian calendar. 33 | 34 | Table of Contents 35 | 36 | 1. Introduction ............................................ 2 37 | 2. Definitions ............................................. 3 38 | 3. Two Digit Years ......................................... 4 39 | 4. Local Time .............................................. 4 40 | 4.1. Coordinated Universal Time (UTC) ...................... 4 41 | 4.2. Local Offsets ......................................... 5 42 | 4.3. Unknown Local Offset Convention ....................... 5 43 | 4.4. Unqualified Local Time ................................ 5 44 | 5. Date and Time format .................................... 6 45 | 5.1. Ordering .............................................. 6 46 | 5.2. Human Readability ..................................... 6 47 | 5.3. Rarely Used Options ................................... 7 48 | 5.4. Redundant Information ................................. 7 49 | 5.5. Simplicity ............................................ 7 50 | 5.6. Internet Date/Time Format ............................. 8 51 | 5.7. Restrictions .......................................... 9 52 | 5.8. Examples ............................................. 10 53 | 6. References ............................................. 10 54 | 7. Security Considerations ................................ 11 55 | 56 | 57 | 58 | Klyne, et. al. Standards Track [Page 1] 59 | 60 | RFC 3339 Date and Time on the Internet: Timestamps July 2002 61 | 62 | 63 | Appendix A. ISO 8601 Collected ABNF ....................... 12 64 | Appendix B. Day of the Week ............................... 14 65 | Appendix C. Leap Years .................................... 14 66 | Appendix D. Leap Seconds ..............................,... 15 67 | Acknowledgements .......................................... 17 68 | Authors' Addresses ........................................ 17 69 | Full Copyright Statement .................................. 18 70 | 71 | 1. Introduction 72 | 73 | Date and time formats cause a lot of confusion and interoperability 74 | problems on the Internet. This document addresses many of the 75 | problems encountered and makes recommendations to improve consistency 76 | and interoperability when representing and using date and time in 77 | Internet protocols. 78 | 79 | This document includes an Internet profile of the ISO 8601 [ISO8601] 80 | standard for representation of dates and times using the Gregorian 81 | calendar. 82 | 83 | There are many ways in which date and time values might appear in 84 | Internet protocols: this document focuses on just one common usage, 85 | viz. timestamps for Internet protocol events. This limited 86 | consideration has the following consequences: 87 | 88 | o All dates and times are assumed to be in the "current era", 89 | somewhere between 0000AD and 9999AD. 90 | 91 | o All times expressed have a stated relationship (offset) to 92 | Coordinated Universal Time (UTC). (This is distinct from some 93 | usage in scheduling applications where a local time and location 94 | may be known, but the actual relationship to UTC may be dependent 95 | on the unknown or unknowable actions of politicians or 96 | administrators. The UTC time corresponding to 17:00 on 23rd March 97 | 2005 in New York may depend on administrative decisions about 98 | daylight savings time. This specification steers well clear of 99 | such considerations.) 100 | 101 | o Timestamps can express times that occurred before the introduction 102 | of UTC. Such timestamps are expressed relative to universal time, 103 | using the best available practice at the stated time. 104 | 105 | o Date and time expressions indicate an instant in time. 106 | Description of time periods, or intervals, is not covered here. 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | Klyne, et. al. Standards Track [Page 2] 115 | 116 | RFC 3339 Date and Time on the Internet: Timestamps July 2002 117 | 118 | 119 | 2. Definitions 120 | 121 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", 122 | "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this 123 | document are to be interpreted as described in RFC 2119 [RFC2119]. 124 | 125 | UTC Coordinated Universal Time as maintained by the Bureau 126 | International des Poids et Mesures (BIPM). 127 | 128 | second A basic unit of measurement of time in the 129 | International System of Units. It is defined as the 130 | duration of 9,192,631,770 cycles of microwave light 131 | absorbed or emitted by the hyperfine transition of 132 | cesium-133 atoms in their ground state undisturbed by 133 | external fields. 134 | 135 | minute A period of time of 60 seconds. However, see also the 136 | restrictions in section 5.7 and Appendix D for how 137 | leap seconds are denoted within minutes. 138 | 139 | hour A period of time of 60 minutes. 140 | 141 | day A period of time of 24 hours. 142 | 143 | leap year In the Gregorian calendar, a year which has 366 days. 144 | A leap year is a year whose number is divisible by 145 | four an integral number of times, except that if it is 146 | a centennial year (i.e. divisible by one hundred) it 147 | shall also be divisible by four hundred an integral 148 | number of times. 149 | 150 | ABNF Augmented Backus-Naur Form, a format used to represent 151 | permissible strings in a protocol or language, as 152 | defined in [ABNF]. 153 | 154 | Email Date/Time Format 155 | The date/time format used by Internet Mail as defined 156 | by RFC 2822 [IMAIL-UPDATE]. 157 | 158 | Internet Date/Time Format 159 | The date format defined in section 5 of this document. 160 | 161 | Timestamp This term is used in this document to refer to an 162 | unambiguous representation of some instant in time. 163 | 164 | Z A suffix which, when applied to a time, denotes a UTC 165 | offset of 00:00; often spoken "Zulu" from the ICAO 166 | phonetic alphabet representation of the letter "Z". 167 | 168 | 169 | 170 | Klyne, et. al. Standards Track [Page 3] 171 | 172 | RFC 3339 Date and Time on the Internet: Timestamps July 2002 173 | 174 | 175 | For more information about time scales, see Appendix E of [NTP], 176 | Section 3 of [ISO8601], and the appropriate ITU documents [ITU-R- 177 | TF]. 178 | 179 | 3. Two Digit Years 180 | 181 | The following requirements are to address the problems of ambiguity 182 | of 2-digit years: 183 | 184 | o Internet Protocols MUST generate four digit years in dates. 185 | 186 | o The use of 2-digit years is deprecated. If a 2-digit year is 187 | received, it should be accepted ONLY if an incorrect 188 | interpretation will not cause a protocol or processing failure 189 | (e.g. if used only for logging or tracing purposes). 190 | 191 | o It is possible that a program using two digit years will 192 | represent years after 1999 as three digits. This occurs if the 193 | program simply subtracts 1900 from the year and doesn't check 194 | the number of digits. Programs wishing to robustly deal with 195 | dates generated by such broken software may add 1900 to three 196 | digit years. 197 | 198 | o It is possible that a program using two digit years will 199 | represent years after 1999 as ":0", ":1", ... ":9", ";0", ... 200 | This occurs if the program simply subtracts 1900 from the year 201 | and adds the decade to the US-ASCII character zero. Programs 202 | wishing to robustly deal with dates generated by such broken 203 | software should detect non-numeric decades and interpret 204 | appropriately. 205 | 206 | The problems with two digit years amply demonstrate why all dates and 207 | times used in Internet protocols MUST be fully qualified. 208 | 209 | 4. Local Time 210 | 211 | 4.1. Coordinated Universal Time (UTC) 212 | 213 | Because the daylight saving rules for local time zones are so 214 | convoluted and can change based on local law at unpredictable times, 215 | true interoperability is best achieved by using Coordinated Universal 216 | Time (UTC). This specification does not cater to local time zone 217 | rules. 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | Klyne, et. al. Standards Track [Page 4] 227 | 228 | RFC 3339 Date and Time on the Internet: Timestamps July 2002 229 | 230 | 231 | 4.2. Local Offsets 232 | 233 | The offset between local time and UTC is often useful information. 234 | For example, in electronic mail (RFC2822, [IMAIL-UPDATE]) the local 235 | offset provides a useful heuristic to determine the probability of a 236 | prompt response. Attempts to label local offsets with alphabetic 237 | strings have resulted in poor interoperability in the past [IMAIL], 238 | [HOST-REQ]. As a result, RFC2822 [IMAIL-UPDATE] has made numeric 239 | offsets mandatory. 240 | 241 | Numeric offsets are calculated as "local time minus UTC". So the 242 | equivalent time in UTC can be determined by subtracting the offset 243 | from the local time. For example, 18:50:00-04:00 is the same time as 244 | 22:50:00Z. (This example shows negative offsets handled by adding 245 | the absolute value of the offset.) 246 | 247 | NOTE: Following ISO 8601, numeric offsets represent only time 248 | zones that differ from UTC by an integral number of minutes. 249 | However, many historical time zones differ from UTC by a non- 250 | integral number of minutes. To represent such historical time 251 | stamps exactly, applications must convert them to a representable 252 | time zone. 253 | 254 | 4.3. Unknown Local Offset Convention 255 | 256 | If the time in UTC is known, but the offset to local time is unknown, 257 | this can be represented with an offset of "-00:00". This differs 258 | semantically from an offset of "Z" or "+00:00", which imply that UTC 259 | is the preferred reference point for the specified time. RFC2822 260 | [IMAIL-UPDATE] describes a similar convention for email. 261 | 262 | 4.4. Unqualified Local Time 263 | 264 | A number of devices currently connected to the Internet run their 265 | internal clocks in local time and are unaware of UTC. While the 266 | Internet does have a tradition of accepting reality when creating 267 | specifications, this should not be done at the expense of 268 | interoperability. Since interpretation of an unqualified local time 269 | zone will fail in approximately 23/24 of the globe, the 270 | interoperability problems of unqualified local time are deemed 271 | unacceptable for the Internet. Systems that are configured with a 272 | local time, are unaware of the corresponding UTC offset, and depend 273 | on time synchronization with other Internet systems, MUST use a 274 | mechanism that ensures correct synchronization with UTC. Some 275 | suitable mechanisms are: 276 | 277 | o Use Network Time Protocol [NTP] to obtain the time in UTC. 278 | 279 | 280 | 281 | 282 | Klyne, et. al. Standards Track [Page 5] 283 | 284 | RFC 3339 Date and Time on the Internet: Timestamps July 2002 285 | 286 | 287 | o Use another host in the same local time zone as a gateway to the 288 | Internet. This host MUST correct unqualified local times that are 289 | transmitted to other hosts. 290 | 291 | o Prompt the user for the local time zone and daylight saving rule 292 | settings. 293 | 294 | 5. Date and Time format 295 | 296 | This section discusses desirable qualities of date and time formats 297 | and defines a profile of ISO 8601 for use in Internet protocols. 298 | 299 | 5.1. Ordering 300 | 301 | If date and time components are ordered from least precise to most 302 | precise, then a useful property is achieved. Assuming that the time 303 | zones of the dates and times are the same (e.g., all in UTC), 304 | expressed using the same string (e.g., all "Z" or all "+00:00"), and 305 | all times have the same number of fractional second digits, then the 306 | date and time strings may be sorted as strings (e.g., using the 307 | strcmp() function in C) and a time-ordered sequence will result. The 308 | presence of optional punctuation would violate this characteristic. 309 | 310 | 5.2. Human Readability 311 | 312 | Human readability has proved to be a valuable feature of Internet 313 | protocols. Human readable protocols greatly reduce the costs of 314 | debugging since telnet often suffices as a test client and network 315 | analyzers need not be modified with knowledge of the protocol. On 316 | the other hand, human readability sometimes results in 317 | interoperability problems. For example, the date format "10/11/1996" 318 | is completely unsuitable for global interchange because it is 319 | interpreted differently in different countries. In addition, the 320 | date format in [IMAIL] has resulted in interoperability problems when 321 | people assumed any text string was permitted and translated the three 322 | letter abbreviations to other languages or substituted date formats 323 | which were easier to generate (e.g. the format used by the C function 324 | ctime). For this reason, a balance must be struck between human 325 | readability and interoperability. 326 | 327 | Because no date and time format is readable according to the 328 | conventions of all countries, Internet clients SHOULD be prepared to 329 | transform dates into a display format suitable for the locality. 330 | This may include translating UTC to local time. 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | Klyne, et. al. Standards Track [Page 6] 339 | 340 | RFC 3339 Date and Time on the Internet: Timestamps July 2002 341 | 342 | 343 | 5.3. Rarely Used Options 344 | 345 | A format which includes rarely used options is likely to cause 346 | interoperability problems. This is because rarely used options are 347 | less likely to be used in alpha or beta testing, so bugs in parsing 348 | are less likely to be discovered. Rarely used options should be made 349 | mandatory or omitted for the sake of interoperability whenever 350 | possible. 351 | 352 | The format defined below includes only one rarely used option: 353 | fractions of a second. It is expected that this will be used only by 354 | applications which require strict ordering of date/time stamps or 355 | which have an unusual precision requirement. 356 | 357 | 5.4. Redundant Information 358 | 359 | If a date/time format includes redundant information, that introduces 360 | the possibility that the redundant information will not correlate. 361 | For example, including the day of the week in a date/time format 362 | introduces the possibility that the day of week is incorrect but the 363 | date is correct, or vice versa. Since it is not difficult to compute 364 | the day of week from a date (see Appendix B), the day of week should 365 | not be included in a date/time format. 366 | 367 | 5.5. Simplicity 368 | 369 | The complete set of date and time formats specified in ISO 8601 370 | [ISO8601] is quite complex in an attempt to provide multiple 371 | representations and partial representations. Appendix A contains an 372 | attempt to translate the complete syntax of ISO 8601 into ABNF. 373 | Internet protocols have somewhat different requirements and 374 | simplicity has proved to be an important characteristic. In 375 | addition, Internet protocols usually need complete specification of 376 | data in order to achieve true interoperability. Therefore, the 377 | complete grammar for ISO 8601 is deemed too complex for most Internet 378 | protocols. 379 | 380 | The following section defines a profile of ISO 8601 for use on the 381 | Internet. It is a conformant subset of the ISO 8601 extended format. 382 | Simplicity is achieved by making most fields and punctuation 383 | mandatory. 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | Klyne, et. al. Standards Track [Page 7] 395 | 396 | RFC 3339 Date and Time on the Internet: Timestamps July 2002 397 | 398 | 399 | 5.6. Internet Date/Time Format 400 | 401 | The following profile of ISO 8601 [ISO8601] dates SHOULD be used in 402 | new protocols on the Internet. This is specified using the syntax 403 | description notation defined in [ABNF]. 404 | 405 | date-fullyear = 4DIGIT 406 | date-month = 2DIGIT ; 01-12 407 | date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on 408 | ; month/year 409 | time-hour = 2DIGIT ; 00-23 410 | time-minute = 2DIGIT ; 00-59 411 | time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on leap second 412 | ; rules 413 | time-secfrac = "." 1*DIGIT 414 | time-numoffset = ("+" / "-") time-hour ":" time-minute 415 | time-offset = "Z" / time-numoffset 416 | 417 | partial-time = time-hour ":" time-minute ":" time-second 418 | [time-secfrac] 419 | full-date = date-fullyear "-" date-month "-" date-mday 420 | full-time = partial-time time-offset 421 | 422 | date-time = full-date "T" full-time 423 | 424 | NOTE: Per [ABNF] and ISO8601, the "T" and "Z" characters in this 425 | syntax may alternatively be lower case "t" or "z" respectively. 426 | 427 | This date/time format may be used in some environments or contexts 428 | that distinguish between the upper- and lower-case letters 'A'-'Z' 429 | and 'a'-'z' (e.g. XML). Specifications that use this format in 430 | such environments MAY further limit the date/time syntax so that 431 | the letters 'T' and 'Z' used in the date/time syntax must always 432 | be upper case. Applications that generate this format SHOULD use 433 | upper case letters. 434 | 435 | NOTE: ISO 8601 defines date and time separated by "T". 436 | Applications using this syntax may choose, for the sake of 437 | readability, to specify a full-date and full-time separated by 438 | (say) a space character. 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | Klyne, et. al. Standards Track [Page 8] 451 | 452 | RFC 3339 Date and Time on the Internet: Timestamps July 2002 453 | 454 | 455 | 5.7. Restrictions 456 | 457 | The grammar element date-mday represents the day number within the 458 | current month. The maximum value varies based on the month and year 459 | as follows: 460 | 461 | Month Number Month/Year Maximum value of date-mday 462 | ------------ ---------- -------------------------- 463 | 01 January 31 464 | 02 February, normal 28 465 | 02 February, leap year 29 466 | 03 March 31 467 | 04 April 30 468 | 05 May 31 469 | 06 June 30 470 | 07 July 31 471 | 08 August 31 472 | 09 September 30 473 | 10 October 31 474 | 11 November 30 475 | 12 December 31 476 | 477 | Appendix C contains sample C code to determine if a year is a leap 478 | year. 479 | 480 | The grammar element time-second may have the value "60" at the end of 481 | months in which a leap second occurs -- to date: June (XXXX-06- 482 | 30T23:59:60Z) or December (XXXX-12-31T23:59:60Z); see Appendix D for 483 | a table of leap seconds. It is also possible for a leap second to be 484 | subtracted, at which times the maximum value of time-second is "58". 485 | At all other times the maximum value of time-second is "59". 486 | Further, in time zones other than "Z", the leap second point is 487 | shifted by the zone offset (so it happens at the same instant around 488 | the globe). 489 | 490 | Leap seconds cannot be predicted far into the future. The 491 | International Earth Rotation Service publishes bulletins [IERS] that 492 | announce leap seconds with a few weeks' warning. Applications should 493 | not generate timestamps involving inserted leap seconds until after 494 | the leap seconds are announced. 495 | 496 | Although ISO 8601 permits the hour to be "24", this profile of ISO 497 | 8601 only allows values between "00" and "23" for the hour in order 498 | to reduce confusion. 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | Klyne, et. al. Standards Track [Page 9] 507 | 508 | RFC 3339 Date and Time on the Internet: Timestamps July 2002 509 | 510 | 511 | 5.8. Examples 512 | 513 | Here are some examples of Internet date/time format. 514 | 515 | 1985-04-12T23:20:50.52Z 516 | 517 | This represents 20 minutes and 50.52 seconds after the 23rd hour of 518 | April 12th, 1985 in UTC. 519 | 520 | 1996-12-19T16:39:57-08:00 521 | 522 | This represents 39 minutes and 57 seconds after the 16th hour of 523 | December 19th, 1996 with an offset of -08:00 from UTC (Pacific 524 | Standard Time). Note that this is equivalent to 1996-12-20T00:39:57Z 525 | in UTC. 526 | 527 | 1990-12-31T23:59:60Z 528 | 529 | This represents the leap second inserted at the end of 1990. 530 | 531 | 1990-12-31T15:59:60-08:00 532 | 533 | This represents the same leap second in Pacific Standard Time, 8 534 | hours behind UTC. 535 | 536 | 1937-01-01T12:00:27.87+00:20 537 | 538 | This represents the same instant of time as noon, January 1, 1937, 539 | Netherlands time. Standard time in the Netherlands was exactly 19 540 | minutes and 32.13 seconds ahead of UTC by law from 1909-05-01 through 541 | 1937-06-30. This time zone cannot be represented exactly using the 542 | HH:MM format, and this timestamp uses the closest representable UTC 543 | offset. 544 | 545 | 6. References 546 | 547 | [ZELLER] Zeller, C., "Kalender-Formeln", Acta Mathematica, Vol. 548 | 9, Nov 1886. 549 | 550 | [IMAIL] Crocker, D., "Standard for the Format of Arpa Internet 551 | Text Messages", STD 11, RFC 822, August 1982. 552 | 553 | [IMAIL-UPDATE] Resnick, P., "Internet Message Format", RFC 2822, 554 | April 2001. 555 | 556 | [ABNF] Crocker, D. and P. Overell, "Augmented BNF for Syntax 557 | Specifications: ABNF", RFC 2234, November 1997. 558 | 559 | 560 | 561 | 562 | Klyne, et. al. Standards Track [Page 10] 563 | 564 | RFC 3339 Date and Time on the Internet: Timestamps July 2002 565 | 566 | 567 | [ISO8601] "Data elements and interchange formats -- Information 568 | interchange -- Representation of dates and times", ISO 569 | 8601:1988(E), International Organization for 570 | Standardization, June, 1988. 571 | 572 | [ISO8601:2000] "Data elements and interchange formats -- Information 573 | interchange -- Representation of dates and times", ISO 574 | 8601:2000, International Organization for 575 | Standardization, December, 2000. 576 | 577 | [HOST-REQ] Braden, R., "Requirements for Internet Hosts -- 578 | Application and Support", STD 3, RFC 1123, October 579 | 1989. 580 | 581 | [IERS] International Earth Rotation Service Bulletins, 582 | . 584 | 585 | [NTP] Mills, D, "Network Time Protocol (Version 3) 586 | Specification, Implementation and Analysis", RFC 1305, 587 | March 1992. 588 | 589 | [ITU-R-TF] International Telecommunication Union Recommendations 590 | for Time Signals and Frequency Standards Emissions. 591 | 592 | 593 | [RFC2119] Bradner, S, "Key words for use in RFCs to Indicate 594 | Requirement Levels", BCP 14, RFC 2119, March 1997. 595 | 596 | 7. Security Considerations 597 | 598 | Since the local time zone of a site may be useful for determining a 599 | time when systems are less likely to be monitored and might be more 600 | susceptible to a security probe, some sites may wish to emit times in 601 | UTC only. Others might consider this to be loss of useful 602 | functionality at the hands of paranoia. 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | Klyne, et. al. Standards Track [Page 11] 619 | 620 | RFC 3339 Date and Time on the Internet: Timestamps July 2002 621 | 622 | 623 | Appendix A. ISO 8601 Collected ABNF 624 | 625 | This information is based on the 1988 version of ISO 8601. There may 626 | be some changes in the 2000 revision. 627 | 628 | ISO 8601 does not specify a formal grammar for the date and time 629 | formats it defines. The following is an attempt to create a formal 630 | grammar from ISO 8601. This is informational only and may contain 631 | errors. ISO 8601 remains the authoritative reference. 632 | 633 | Note that due to ambiguities in ISO 8601, some interpretations had to 634 | be made. First, ISO 8601 is not clear if mixtures of basic and 635 | extended format are permissible. This grammar permits mixtures. ISO 636 | 8601 is not clear on whether an hour of 24 is permissible only if 637 | minutes and seconds are 0. This assumes that an hour of 24 is 638 | permissible in any context. Restrictions on date-mday in section 5.7 639 | apply. ISO 8601 states that the "T" may be omitted under some 640 | circumstances. This grammar requires the "T" to avoid ambiguity. 641 | ISO 8601 also requires (in section 5.3.1.3) that a decimal fraction 642 | be proceeded by a "0" if less than unity. Annex B.2 of ISO 8601 643 | gives examples where the decimal fractions are not preceded by a "0". 644 | This grammar assumes section 5.3.1.3 is correct and that Annex B.2 is 645 | in error. 646 | 647 | date-century = 2DIGIT ; 00-99 648 | date-decade = DIGIT ; 0-9 649 | date-subdecade = DIGIT ; 0-9 650 | date-year = date-decade date-subdecade 651 | date-fullyear = date-century date-year 652 | date-month = 2DIGIT ; 01-12 653 | date-wday = DIGIT ; 1-7 ; 1 is Monday, 7 is Sunday 654 | date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on 655 | ; month/year 656 | date-yday = 3DIGIT ; 001-365, 001-366 based on year 657 | date-week = 2DIGIT ; 01-52, 01-53 based on year 658 | 659 | datepart-fullyear = [date-century] date-year ["-"] 660 | datepart-ptyear = "-" [date-subdecade ["-"]] 661 | datepart-wkyear = datepart-ptyear / datepart-fullyear 662 | 663 | dateopt-century = "-" / date-century 664 | dateopt-fullyear = "-" / datepart-fullyear 665 | dateopt-year = "-" / (date-year ["-"]) 666 | dateopt-month = "-" / (date-month ["-"]) 667 | dateopt-week = "-" / (date-week ["-"]) 668 | 669 | 670 | 671 | 672 | 673 | 674 | Klyne, et. al. Standards Track [Page 12] 675 | 676 | RFC 3339 Date and Time on the Internet: Timestamps July 2002 677 | 678 | 679 | datespec-full = datepart-fullyear date-month ["-"] date-mday 680 | datespec-year = date-century / dateopt-century date-year 681 | datespec-month = "-" dateopt-year date-month [["-"] date-mday] 682 | datespec-mday = "--" dateopt-month date-mday 683 | datespec-week = datepart-wkyear "W" 684 | (date-week / dateopt-week date-wday) 685 | datespec-wday = "---" date-wday 686 | datespec-yday = dateopt-fullyear date-yday 687 | 688 | date = datespec-full / datespec-year 689 | / datespec-month / 690 | datespec-mday / datespec-week / datespec-wday / datespec-yday 691 | 692 | Time: 693 | 694 | time-hour = 2DIGIT ; 00-24 695 | time-minute = 2DIGIT ; 00-59 696 | time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on 697 | ; leap-second rules 698 | time-fraction = ("," / ".") 1*DIGIT 699 | time-numoffset = ("+" / "-") time-hour [[":"] time-minute] 700 | time-zone = "Z" / time-numoffset 701 | 702 | timeopt-hour = "-" / (time-hour [":"]) 703 | timeopt-minute = "-" / (time-minute [":"]) 704 | 705 | timespec-hour = time-hour [[":"] time-minute [[":"] time-second]] 706 | timespec-minute = timeopt-hour time-minute [[":"] time-second] 707 | timespec-second = "-" timeopt-minute time-second 708 | timespec-base = timespec-hour / timespec-minute / timespec-second 709 | 710 | time = timespec-base [time-fraction] [time-zone] 711 | 712 | iso-date-time = date "T" time 713 | 714 | Durations: 715 | 716 | dur-second = 1*DIGIT "S" 717 | dur-minute = 1*DIGIT "M" [dur-second] 718 | dur-hour = 1*DIGIT "H" [dur-minute] 719 | dur-time = "T" (dur-hour / dur-minute / dur-second) 720 | dur-day = 1*DIGIT "D" 721 | dur-week = 1*DIGIT "W" 722 | dur-month = 1*DIGIT "M" [dur-day] 723 | dur-year = 1*DIGIT "Y" [dur-month] 724 | dur-date = (dur-day / dur-month / dur-year) [dur-time] 725 | 726 | duration = "P" (dur-date / dur-time / dur-week) 727 | 728 | 729 | 730 | Klyne, et. al. Standards Track [Page 13] 731 | 732 | RFC 3339 Date and Time on the Internet: Timestamps July 2002 733 | 734 | 735 | Periods: 736 | 737 | period-explicit = iso-date-time "/" iso-date-time 738 | period-start = iso-date-time "/" duration 739 | period-end = duration "/" iso-date-time 740 | 741 | period = period-explicit / period-start / period-end 742 | 743 | Appendix B. Day of the Week 744 | 745 | The following is a sample C subroutine loosely based on Zeller's 746 | Congruence [Zeller] which may be used to obtain the day of the week 747 | for dates on or after 0000-03-01: 748 | 749 | char *day_of_week(int day, int month, int year) 750 | { 751 | int cent; 752 | char *dayofweek[] = { 753 | "Sunday", "Monday", "Tuesday", "Wednesday", 754 | "Thursday", "Friday", "Saturday" 755 | }; 756 | 757 | /* adjust months so February is the last one */ 758 | month -= 2; 759 | if (month < 1) { 760 | month += 12; 761 | --year; 762 | } 763 | /* split by century */ 764 | cent = year / 100; 765 | year %= 100; 766 | return (dayofweek[((26 * month - 2) / 10 + day + year 767 | + year / 4 + cent / 4 + 5 * cent) % 7]); 768 | } 769 | 770 | Appendix C. Leap Years 771 | 772 | Here is a sample C subroutine to calculate if a year is a leap year: 773 | 774 | /* This returns non-zero if year is a leap year. Must use 4 digit 775 | year. 776 | */ 777 | int leap_year(int year) 778 | { 779 | return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)); 780 | } 781 | 782 | 783 | 784 | 785 | 786 | Klyne, et. al. Standards Track [Page 14] 787 | 788 | RFC 3339 Date and Time on the Internet: Timestamps July 2002 789 | 790 | 791 | Appendix D. Leap Seconds 792 | 793 | Information about leap seconds can be found at: 794 | . In particular, it notes 795 | that: 796 | 797 | The decision to introduce a leap second in UTC is the 798 | responsibility of the International Earth Rotation Service (IERS). 799 | According to the CCIR Recommendation, first preference is given to 800 | the opportunities at the end of December and June, and second 801 | preference to those at the end of March and September. 802 | 803 | When required, insertion of a leap second occurs as an extra second 804 | at the end of a day in UTC, represented by a timestamp of the form 805 | YYYY-MM-DDT23:59:60Z. A leap second occurs simultaneously in all 806 | time zones, so that time zone relationships are not affected. See 807 | section 5.8 for some examples of leap second times. 808 | 809 | The following table is an excerpt from the table maintained by the 810 | United States Naval Observatory. The source data is located at: 811 | 812 | 813 | 814 | 815 | 816 | 817 | 818 | 819 | 820 | 821 | 822 | 823 | 824 | 825 | 826 | 827 | 828 | 829 | 830 | 831 | 832 | 833 | 834 | 835 | 836 | 837 | 838 | 839 | 840 | 841 | 842 | Klyne, et. al. Standards Track [Page 15] 843 | 844 | RFC 3339 Date and Time on the Internet: Timestamps July 2002 845 | 846 | 847 | This table shows the date of the leap second, and the difference 848 | between the time standard TAI (which isn't adjusted by leap seconds) 849 | and UTC after that leap second. 850 | 851 | UTC Date TAI - UTC After Leap Second 852 | -------- --------------------------- 853 | 1972-06-30 11 854 | 1972-12-31 12 855 | 1973-12-31 13 856 | 1974-12-31 14 857 | 1975-12-31 15 858 | 1976-12-31 16 859 | 1977-12-31 17 860 | 1978-12-31 18 861 | 1979-12-31 19 862 | 1981-06-30 20 863 | 1982-06-30 21 864 | 1983-06-30 22 865 | 1985-06-30 23 866 | 1987-12-31 24 867 | 1989-12-31 25 868 | 1990-12-31 26 869 | 1992-06-30 27 870 | 1993-06-30 28 871 | 1994-06-30 29 872 | 1995-12-31 30 873 | 1997-06-30 31 874 | 1998-12-31 32 875 | 876 | 877 | 878 | 879 | 880 | 881 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 890 | 891 | 892 | 893 | 894 | 895 | 896 | 897 | 898 | Klyne, et. al. Standards Track [Page 16] 899 | 900 | RFC 3339 Date and Time on the Internet: Timestamps July 2002 901 | 902 | 903 | Acknowledgements 904 | 905 | The following people provided helpful advice for an earlier 906 | incarnation of this document: Ned Freed, Neal McBurnett, David 907 | Keegel, Markus Kuhn, Paul Eggert and Robert Elz. Thanks are also due 908 | to participants of the IETF Calendaring/Scheduling working group 909 | mailing list, and participants of the time zone mailing list. 910 | 911 | The following reviewers contributed helpful suggestions for the 912 | present revision: Tom Harsch, Markus Kuhn, Pete Resnick, Dan Kohn. 913 | Paul Eggert provided many careful observations regarding the 914 | subtleties of leap seconds and time zone offsets. The following 915 | people noted corrections and improvements to earlier drafts: Dr John 916 | Stockton, Jutta Degener, Joe Abley, and Dan Wing. 917 | 918 | Authors' Addresses 919 | 920 | Chris Newman 921 | Sun Microsystems 922 | 1050 Lakes Drive, Suite 250 923 | West Covina, CA 91790 USA 924 | 925 | EMail: chris.newman@sun.com 926 | 927 | 928 | Graham Klyne (editor, this revision) 929 | Clearswift Corporation 930 | 1310 Waterside 931 | Arlington Business Park 932 | Theale, Reading RG7 4SA 933 | UK 934 | 935 | Phone: +44 11 8903 8903 936 | Fax: +44 11 8903 9000 937 | EMail: GK@ACM.ORG 938 | 939 | 940 | 941 | 942 | 943 | 944 | 945 | 946 | 947 | 948 | 949 | 950 | 951 | 952 | 953 | 954 | Klyne, et. al. Standards Track [Page 17] 955 | 956 | RFC 3339 Date and Time on the Internet: Timestamps July 2002 957 | 958 | 959 | Full Copyright Statement 960 | 961 | Copyright (C) The Internet Society (2002). All Rights Reserved. 962 | 963 | This document and translations of it may be copied and furnished to 964 | others, and derivative works that comment on or otherwise explain it 965 | or assist in its implementation may be prepared, copied, published 966 | and distributed, in whole or in part, without restriction of any 967 | kind, provided that the above copyright notice and this paragraph are 968 | included on all such copies and derivative works. However, this 969 | document itself may not be modified in any way, such as by removing 970 | the copyright notice or references to the Internet Society or other 971 | Internet organizations, except as needed for the purpose of 972 | developing Internet standards in which case the procedures for 973 | copyrights defined in the Internet Standards process must be 974 | followed, or as required to translate it into languages other than 975 | English. 976 | 977 | The limited permissions granted above are perpetual and will not be 978 | revoked by the Internet Society or its successors or assigns. 979 | 980 | This document and the information contained herein is provided on an 981 | "AS IS" basis and THE INTERNET SOCIETY AND THE INTERNET ENGINEERING 982 | TASK FORCE DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING 983 | BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE INFORMATION 984 | HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED WARRANTIES OF 985 | MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. 986 | 987 | Acknowledgement 988 | 989 | Funding for the RFC Editor function is currently provided by the 990 | Internet Society. 991 | 992 | 993 | 994 | 995 | 996 | 997 | 998 | 999 | 1000 | 1001 | 1002 | 1003 | 1004 | 1005 | 1006 | 1007 | 1008 | 1009 | 1010 | Klyne, et. al. Standards Track [Page 18] 1011 | 1012 | --------------------------------------------------------------------------------