diff --git a/.eslintrc.js b/.eslintrc.js index 9b8544c84..27fe09a0c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,7 +6,13 @@ module.exports = { '@typescript-eslint', 'license-header', 'jsdoc', + 'jasmine', + 'jest', ], + env: { + jasmine: true, + 'jest/globals': true, + }, parserOptions: { tsconfigRootDir: __dirname, project: './tsconfig.test.json', @@ -17,6 +23,9 @@ module.exports = { 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended-requiring-type-checking', + 'plugin:jasmine/recommended', + 'plugin:jest/recommended', + 'plugin:jest/style', ], rules: { // Automatic fixers @@ -113,6 +122,13 @@ module.exports = { MethodDefinition: true, } }], + 'jest/no-jasmine-globals': 'off', + 'jest/no-alias-methods': 'off', + 'jest/no-conditional-expect': 'warn', + 'jest/no-standalone-expect': 'warn', + 'jest/no-test-prefixes': 'off', + 'jest/prefer-to-be': 'warn', + 'jest/prefer-to-have-length': 'off', }, overrides: [ { @@ -127,5 +143,11 @@ module.exports = { 'sort-keys': ['error', 'asc'], } }, + { + files: ['**/*.spec.ts'], + rules: { + '@typescript-eslint/no-non-null-assertion': 'off', + } + } ], } diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 0c008061a..b4c0826dd 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -32,4 +32,4 @@ jobs: - name: Run audit run: | - npm audit --omit='dev' + npm run audit diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d5eb450d..76b86ebbd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Fixed + +- Fixed an issue where cells were not recalculated after adding, removing and renaming sheets. [#1116](https://github.com/handsontable/hyperformula/issues/1116) +- Fixed an issue where overwriting a non-computed cell caused the `Value of the formula cell is not computed` error. [#1194](https://github.com/handsontable/hyperformula/issues/1194) + ## [3.1.0] - 2025-10-14 ### Changed diff --git a/docs/guide/basic-operations.md b/docs/guide/basic-operations.md index 0bcc0a771..20a4b1edc 100644 --- a/docs/guide/basic-operations.md +++ b/docs/guide/basic-operations.md @@ -228,7 +228,7 @@ consists of a sheet ID, column ID, and row ID, like this: Alternatively, you can work with the **A1 notation** known from spreadsheets like Excel or Google Sheets. The API provides the helper -function [`simpleCellAddressFromString`](../api/globals.md#simplecelladdressfromstring) which you can use to retrieve +function [`simpleCellAddressFromString`](../api/classes/hyperformula.md#simplecelladdressfromstring) which you can use to retrieve the [`SimpleCellAddress`](../api/interfaces/simplecelladdress) . ::: diff --git a/docs/guide/compatibility-with-microsoft-excel.md b/docs/guide/compatibility-with-microsoft-excel.md index c26d9efe5..fade7ed96 100644 --- a/docs/guide/compatibility-with-microsoft-excel.md +++ b/docs/guide/compatibility-with-microsoft-excel.md @@ -13,6 +13,17 @@ That said, there are cases when HyperFormula can't be compatible with all three Still, with the right configuration, you can achieve nearly full compatibility. +### Excel function coverage + +HyperFormula implements **350 out of 515 Excel functions** (68% coverage), as of version 3.1.0 and Excel 2024. This means that **165 Excel functions** (32%) are not yet available in HyperFormula. + +Additionally, HyperFormula includes some functions that are not part of Excel's standard function set, bringing the total number of available functions to **{{ $page.functionsCount }}**. + +For a complete list of supported functions, see the [built-in functions](built-in-functions.md) page. + +If you need any of the missing Excel functions, you can [contact us](contact.md) or implement them as [custom functions](custom-functions.md), extending HyperFormula's capabilities to meet your specific requirements. + + ## Configure compatibility with Microsoft Excel ### String comparison rules diff --git a/jest.config.js b/jest.config.js index e1603dc61..f7573a049 100644 --- a/jest.config.js +++ b/jest.config.js @@ -50,4 +50,11 @@ module.exports = { transform: { "^.+\\.(ts|tsx)$": "ts-jest" }, + + watchPathIgnorePatterns: [ + '/node_modules/', + '/dist/', + '/commonjs/', + '/es/', + ] }; diff --git a/package-lock.json b/package-lock.json index 42965509d..3e0449c60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,8 @@ "env-cmd": "^10.1.0", "eslint": "^8.57.1", "eslint-config-prettier": "^9.1.0", + "eslint-plugin-jasmine": "^4.2.2", + "eslint-plugin-jest": "^27.9.0", "eslint-plugin-jsdoc": "^50.5.0", "eslint-plugin-license-header": "^0.6.1", "eslint-plugin-prettier": "^5.2.1", @@ -2353,9 +2355,9 @@ } }, "node_modules/@es-joy/jsdoccomment/node_modules/@typescript-eslint/types": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.0.tgz", - "integrity": "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.1.tgz", + "integrity": "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ==", "dev": true, "license": "MIT", "engines": { @@ -3581,9 +3583,9 @@ } }, "node_modules/@types/jasmine": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.9.tgz", - "integrity": "sha512-8t4HtkW4wxiPVedMpeZ63n3vlWxEIquo/zc1Tm8ElU+SqVV7+D3Na2PWaJUp179AzTragMWVwkMv7mvty0NfyQ==", + "version": "5.1.10", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.10.tgz", + "integrity": "sha512-ctILpOSFD58PTyGwr400GadefIHPqdWQ8VlZI9D65USOWR/aIw8dkDAw17KPCu/Wn+e+r9BWpPtmuB5T/pP8ng==", "dev": true, "license": "MIT" }, @@ -7266,9 +7268,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.8.13", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.13.tgz", - "integrity": "sha512-7s16KR8io8nIBWQyCYhmFhd+ebIzb9VKTzki+wOJXHTxTnV6+mFGH3+Jwn1zoKaY9/H9T/0BcKCZnzXljPnpSQ==", + "version": "2.8.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz", + "integrity": "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -8258,9 +8260,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001749", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001749.tgz", - "integrity": "sha512-0rw2fJOmLfnzCRbkm8EyHL8SvI2Apu5UbnQuTsJ0ClgrH8hcwFooJ1s5R0EP8o8aVrFu8++ae29Kt9/gZAZp/Q==", + "version": "1.0.30001750", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001750.tgz", + "integrity": "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==", "dev": true, "funding": [ { @@ -9379,9 +9381,9 @@ } }, "node_modules/core-js": { - "version": "3.45.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.1.tgz", - "integrity": "sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==", + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.46.0.tgz", + "integrity": "sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -9391,13 +9393,13 @@ } }, "node_modules/core-js-compat": { - "version": "3.45.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.1.tgz", - "integrity": "sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==", + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.46.0.tgz", + "integrity": "sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.25.3" + "browserslist": "^4.26.3" }, "funding": { "type": "opencollective", @@ -11155,9 +11157,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.233", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.233.tgz", - "integrity": "sha512-iUdTQSf7EFXsDdQsp8MwJz5SVk4APEFqXU/S47OtQ0YLqacSwPXdZ5vRlMX3neb07Cy2vgioNuRnWUXFwuslkg==", + "version": "1.5.237", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.237.tgz", + "integrity": "sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==", "dev": true, "license": "ISC" }, @@ -11390,9 +11392,9 @@ } }, "node_modules/envinfo": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.17.0.tgz", - "integrity": "sha512-GpfViocsFM7viwClFgxK26OtjMlKN67GCR5v6ASFkotxtpBWd9d+vNy+AH7F2E1TUkMDZ8P/dDPZX71/NG8xnQ==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.18.0.tgz", + "integrity": "sha512-02QGCLRW+Jb8PC270ic02lat+N57iBaWsvHjcJViqp6UVupRB+Vsg7brYPTqEFXvsdTql3KnSczv5ModZFpl8Q==", "dev": true, "license": "MIT", "bin": { @@ -11985,6 +11987,43 @@ "eslint": ">=7.0.0" } }, + "node_modules/eslint-plugin-jasmine": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jasmine/-/eslint-plugin-jasmine-4.2.2.tgz", + "integrity": "sha512-nALbewRk63uz28UGNhUTJyd6GofXxVNFpWFNAwr9ySc6kpSRIoO4suwZqIYz3cfJmCacilmjp7+1Ocjr7zRagA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8", + "npm": ">=6" + } + }, + "node_modules/eslint-plugin-jest": { + "version": "27.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.9.0.tgz", + "integrity": "sha512-QIT7FH7fNmd9n4se7FFKHbsLKGQiw885Ds6Y/sxKgCZ6natwCsXdgPOADnYVxN2QrRweF0FZWbJ6S7Rsn7llug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^5.10.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^5.0.0 || ^6.0.0 || ^7.0.0", + "eslint": "^7.0.0 || ^8.0.0", + "jest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, "node_modules/eslint-plugin-jsdoc": { "version": "50.8.0", "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.8.0.tgz", @@ -25219,9 +25258,9 @@ } }, "node_modules/std-env": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", - "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 13a7fe324..429636c4b 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "test": "npm-run-all lint test:unit test:browser test:compatibility", "test:unit": "cross-env NODE_ICU_DATA=node_modules/full-icu jest", "test:watch": "cross-env NODE_ICU_DATA=node_modules/full-icu jest --watch", - "test:tdd": "cross-env NODE_ICU_DATA=node_modules/full-icu jest --watch custom-functions", + "test:tdd": "cross-env NODE_ICU_DATA=node_modules/full-icu jest --watch adding-sheet", "test:coverage": "npm run test:unit -- --coverage", "test:logMemory": "cross-env NODE_ICU_DATA=node_modules/full-icu jest --runInBand --logHeapUsage", "test:unit.ci": "cross-env NODE_ICU_DATA=node_modules/full-icu node --expose-gc ./node_modules/jest/bin/jest --forceExit", @@ -93,6 +93,7 @@ "benchmark:compare-benchmarks": "npm run tsnode test/performance/compare-benchmarks.ts", "lint": "eslint . --ext .js,.ts", "lint:fix": "eslint . --ext .js,.ts --fix", + "audit": "npm audit --omit=dev", "clean": "rimraf coverage/ commonjs/ dist/ es/ languages/ lib/ typings/ test-jasmine/", "compile": "tsc", "check:licenses": "license-checker --production --excludePackages=\"hyperformula@3.0.1\" --onlyAllow=\"MIT; Apache-2.0; BSD-3-Clause; BSD-2-Clause; ISC; BSD; Unlicense\"", @@ -133,6 +134,8 @@ "env-cmd": "^10.1.0", "eslint": "^8.57.1", "eslint-config-prettier": "^9.1.0", + "eslint-plugin-jasmine": "^4.2.2", + "eslint-plugin-jest": "^27.9.0", "eslint-plugin-jsdoc": "^50.5.0", "eslint-plugin-license-header": "^0.6.1", "eslint-plugin-prettier": "^5.2.1", diff --git a/src/BuildEngineFactory.ts b/src/BuildEngineFactory.ts index 9ab3574ca..1faff1e88 100644 --- a/src/BuildEngineFactory.ts +++ b/src/BuildEngineFactory.ts @@ -87,13 +87,17 @@ export class BuildEngineFactory { throw new SheetSizeLimitExceededError() } const sheetId = sheetMapping.addSheet(sheetName) - addressMapping.autoAddSheet(sheetId, boundaries) + addressMapping.addSheetAndSetStrategyBasedOnBoundaries(sheetId, boundaries, { throwIfSheetAlreadyExists: true }) } } - const parser = new ParserWithCaching(config, functionRegistry, sheetMapping.get) + const parser = new ParserWithCaching( + config, + functionRegistry, + dependencyGraph.sheetReferenceRegistrar.ensureSheetRegistered.bind(dependencyGraph.sheetReferenceRegistrar) + ) lazilyTransformingAstService.parser = parser - const unparser = new Unparser(config, buildLexerConfig(config), sheetMapping.fetchDisplayName, namedExpressions) + const unparser = new Unparser(config, sheetMapping, namedExpressions) const dateTimeHelper = new DateTimeHelper(config) const numberLiteralHelper = new NumberLiteralHelper(config) const arithmeticHelper = new ArithmeticHelper(config, dateTimeHelper, numberLiteralHelper) @@ -106,7 +110,7 @@ export class BuildEngineFactory { const clipboardOperations = new ClipboardOperations(config, dependencyGraph, operations) const crudOperations = new CrudOperations(config, operations, undoRedo, clipboardOperations, dependencyGraph, columnSearch, parser, cellContentParser, lazilyTransformingAstService, namedExpressions) - const exporter = new Exporter(config, namedExpressions, sheetMapping.fetchDisplayName, lazilyTransformingAstService) + const exporter = new Exporter(config, namedExpressions, sheetMapping, lazilyTransformingAstService) const serialization = new Serialization(dependencyGraph, unparser, exporter) const interpreter = new Interpreter(config, dependencyGraph, columnSearch, stats, arithmeticHelper, functionRegistry, namedExpressions, serialization, arraySizePredictor, dateTimeHelper) diff --git a/src/Cell.ts b/src/Cell.ts index e303d4df5..faab24bab 100644 --- a/src/Cell.ts +++ b/src/Cell.ts @@ -3,8 +3,8 @@ * Copyright (c) 2025 Handsoncode. All rights reserved. */ -import {ArrayVertex, CellVertex, FormulaCellVertex, ParsingErrorVertex, ValueCellVertex} from './DependencyGraph' -import {FormulaVertex} from './DependencyGraph/FormulaCellVertex' +import {ArrayFormulaVertex, CellVertex, ScalarFormulaVertex, ParsingErrorVertex, ValueCellVertex} from './DependencyGraph' +import {FormulaVertex} from './DependencyGraph/FormulaVertex' import {ErrorMessage} from './error-message' import { EmptyValue, @@ -59,14 +59,14 @@ export enum CellType { } export const getCellType = (vertex: Maybe, address: SimpleCellAddress): CellType => { - if (vertex instanceof ArrayVertex) { + if (vertex instanceof ArrayFormulaVertex) { if (vertex.isLeftCorner(address)) { return CellType.ARRAYFORMULA } else { return CellType.ARRAY } } - if (vertex instanceof FormulaCellVertex || vertex instanceof ParsingErrorVertex) { + if (vertex instanceof ScalarFormulaVertex || vertex instanceof ParsingErrorVertex) { return CellType.FORMULA } if (vertex instanceof ValueCellVertex) { @@ -196,7 +196,12 @@ export interface SimpleCellAddress { } export const simpleCellAddress = (sheet: number, col: number, row: number): SimpleCellAddress => ({sheet, col, row}) -export const invalidSimpleCellAddress = (address: SimpleCellAddress): boolean => (address.col < 0 || address.row < 0) + +/** + * Checks if the column or row id is negative. + */ +export const isColOrRowInvalid = (address: SimpleCellAddress): boolean => (address.col < 0 || address.row < 0) + export const movedSimpleCellAddress = (address: SimpleCellAddress, toSheet: number, toRight: number, toBottom: number): SimpleCellAddress => { return simpleCellAddress(toSheet, address.col + toRight, address.row + toBottom) } diff --git a/src/ClipboardOperations.ts b/src/ClipboardOperations.ts index eabcf6226..45848085d 100644 --- a/src/ClipboardOperations.ts +++ b/src/ClipboardOperations.ts @@ -4,7 +4,7 @@ */ import {AbsoluteCellRange} from './AbsoluteCellRange' -import {invalidSimpleCellAddress, simpleCellAddress, SimpleCellAddress} from './Cell' +import {isColOrRowInvalid, simpleCellAddress, SimpleCellAddress} from './Cell' import {RawCellContent} from './CellContentParser' import {Config} from './Config' import {DependencyGraph} from './DependencyGraph' @@ -119,7 +119,7 @@ export class ClipboardOperations { return } - if (invalidSimpleCellAddress(destinationLeftCorner) || + if (isColOrRowInvalid(destinationLeftCorner) || !this.dependencyGraph.sheetMapping.hasSheetWithId(destinationLeftCorner.sheet)) { throw new InvalidArgumentsError('a valid target address.') } diff --git a/src/CrudOperations.ts b/src/CrudOperations.ts index 4f44518cb..bf0238dbc 100644 --- a/src/CrudOperations.ts +++ b/src/CrudOperations.ts @@ -4,7 +4,7 @@ */ import {AbsoluteCellRange} from './AbsoluteCellRange' -import {invalidSimpleCellAddress, simpleCellAddress, SimpleCellAddress} from './Cell' +import {isColOrRowInvalid, simpleCellAddress, SimpleCellAddress} from './Cell' import {CellContent, CellContentParser, RawCellContent} from './CellContentParser' import {ClipboardCell, ClipboardOperations} from './ClipboardOperations' import {Config} from './Config' @@ -206,29 +206,35 @@ export class CrudOperations { this.ensureItIsPossibleToAddSheet(name) } this.undoRedo.clearRedoStack() - const addedSheetName = this.operations.addSheet(name) - this.undoRedo.saveOperation(new AddSheetUndoEntry(addedSheetName)) - return addedSheetName + const { sheetName, sheetId } = this.operations.addSheet(name) + this.undoRedo.saveOperation(new AddSheetUndoEntry(sheetName, sheetId)) + return sheetName } public removeSheet(sheetId: number): void { this.ensureScopeIdIsValid(sheetId) this.undoRedo.clearRedoStack() this.clipboardOperations.abortCut() - const originalName = this.sheetMapping.fetchDisplayName(sheetId) + const originalName = this.sheetMapping.getSheetNameOrThrowError(sheetId) const oldSheetContent = this.operations.getSheetClipboardCells(sheetId) - const {version, scopedNamedExpressions} = this.operations.removeSheet(sheetId) - this.undoRedo.saveOperation(new RemoveSheetUndoEntry(originalName, sheetId, oldSheetContent, scopedNamedExpressions, version)) + const scopedNamedExpressions = this.operations.removeSheet(sheetId) + this.undoRedo.saveOperation(new RemoveSheetUndoEntry(originalName, sheetId, oldSheetContent, scopedNamedExpressions)) } public renameSheet(sheetId: number, newName: string): Maybe { this.ensureItIsPossibleToRenameSheet(sheetId, newName) - const oldName = this.operations.renameSheet(sheetId, newName) - if (oldName !== undefined) { + const { previousDisplayName, version, mergedPlaceholderSheetId } = this.operations.renameSheet(sheetId, newName) + if (previousDisplayName !== undefined) { this.undoRedo.clearRedoStack() - this.undoRedo.saveOperation(new RenameSheetUndoEntry(sheetId, oldName, newName)) + this.undoRedo.saveOperation(new RenameSheetUndoEntry( + sheetId, + previousDisplayName, + newName, + version, + mergedPlaceholderSheetId, + )) } - return oldName + return previousDisplayName } public clearSheet(sheetId: number): void { @@ -495,8 +501,8 @@ export class CrudOperations { if ( !this.sheetMapping.hasSheetWithId(sheet) - || invalidSimpleCellAddress(sourceStart) - || invalidSimpleCellAddress(targetStart) + || isColOrRowInvalid(sourceStart) + || isColOrRowInvalid(targetStart) || !isPositiveInteger(numberOfRows) || (targetRow <= startRow + numberOfRows && targetRow >= startRow) ) { @@ -522,8 +528,8 @@ export class CrudOperations { if ( !this.sheetMapping.hasSheetWithId(sheet) - || invalidSimpleCellAddress(sourceStart) - || invalidSimpleCellAddress(targetStart) + || isColOrRowInvalid(sourceStart) + || isColOrRowInvalid(targetStart) || !isPositiveInteger(numberOfColumns) || (targetColumn <= startColumn + numberOfColumns && targetColumn >= startColumn) ) { @@ -552,14 +558,14 @@ export class CrudOperations { throw new NoSheetWithIdError(sheetId) } - const existingSheetId = this.sheetMapping.get(name) + const existingSheetId = this.sheetMapping.getSheetId(name) if (existingSheetId !== undefined && existingSheetId !== sheetId) { throw new SheetNameAlreadyTakenError(name) } } public ensureItIsPossibleToChangeContent(address: SimpleCellAddress): void { - if (invalidSimpleCellAddress(address)) { + if (isColOrRowInvalid(address)) { throw new InvalidAddressError(address) } if (!this.sheetMapping.hasSheetWithId(address.sheet)) { diff --git a/src/DependencyGraph/AddressMapping/AddressMapping.ts b/src/DependencyGraph/AddressMapping/AddressMapping.ts index ef02f762a..e329edb7e 100644 --- a/src/DependencyGraph/AddressMapping/AddressMapping.ts +++ b/src/DependencyGraph/AddressMapping/AddressMapping.ts @@ -10,41 +10,86 @@ import {EmptyValue, InterpreterValue} from '../../interpreter/InterpreterValue' import {Maybe} from '../../Maybe' import {SheetBoundaries} from '../../Sheet' import {ColumnsSpan, RowsSpan} from '../../Span' -import {ArrayVertex, ValueCellVertex} from '../index' +import {ArrayFormulaVertex, DenseStrategy, ValueCellVertex} from '../index' import {CellVertex} from '../Vertex' import {ChooseAddressMapping} from './ChooseAddressMappingPolicy' import {AddressMappingStrategy} from './AddressMappingStrategy' +/** + * Options for adding a sheet to the address mapping. + */ +export interface AddressMappingAddSheetOptions { + throwIfSheetAlreadyExists: boolean, +} + +export interface AddressMappingGetCellOptions { + throwIfSheetNotExists?: boolean, + throwIfCellNotExists?: boolean, +} + +/** + * Manages cell vertices and provides access to vertex by SimpleCellAddress. + * For each sheet it stores vertices according to AddressMappingStrategy: DenseStrategy or SparseStrategy. + * + * Pleceholder sheets: + * - for placeholders sheets (sheets that are used in formulas but not yet added), it stores placeholder strategy entries (DenseStrategy(0, 0)) + * - placeholder strategy entries may contain EmptyCellVertex-es but never ValueCellVertex or FormulaVertex as they content is empty + * - vertices in placeholder strategy entries are used only for dependency tracking + */ export class AddressMapping { private mapping: Map = new Map() constructor( - private readonly policy: ChooseAddressMapping - ) { - } + private readonly policy: ChooseAddressMapping, + ) {} - /** @inheritDoc */ - public getCell(address: SimpleCellAddress): Maybe { + /** + * Gets the cell vertex at the specified address. + */ + public getCell(address: SimpleCellAddress, options: AddressMappingGetCellOptions = {}): Maybe { const sheetMapping = this.mapping.get(address.sheet) - if (sheetMapping === undefined) { - throw new NoSheetWithIdError(address.sheet) + + if (!sheetMapping) { + if (options.throwIfSheetNotExists) { + throw new NoSheetWithIdError(address.sheet) + } + return undefined } - return sheetMapping.getCell(address) + + const cell = sheetMapping.getCell(address) + + if (!cell && options.throwIfCellNotExists) { + throw Error('Vertex for address missing in AddressMapping') + } + + return cell } - public fetchCell(address: SimpleCellAddress): CellVertex { + /** + * Gets the cell vertex at the specified address or throws if it doesn't exist. + * @throws {NoSheetWithIdError} if sheet doesn't exist + * @throws {Error} if cell doesn't exist + */ + public getCellOrThrow(address: SimpleCellAddress): CellVertex { const sheetMapping = this.mapping.get(address.sheet) - if (sheetMapping === undefined) { - throw new NoSheetWithIdError(address.sheet) + + if (!sheetMapping) { + throw new NoSheetWithIdError(address.sheet) } - const vertex = sheetMapping.getCell(address) - if (!vertex) { + + const cell = sheetMapping.getCell(address) + if (!cell) { throw Error('Vertex for address missing in AddressMapping') } - return vertex + + return cell } - public strategyFor(sheetId: number): AddressMappingStrategy { + /** + * Gets the address mapping strategy for the specified sheet. + * @throws {NoSheetWithIdError} if sheet doesn't exist + */ + public getStrategyForSheetOrThrow(sheetId: number): AddressMappingStrategy { const strategy = this.mapping.get(sheetId) if (strategy === undefined) { throw new NoSheetWithIdError(sheetId) @@ -53,71 +98,171 @@ export class AddressMapping { return strategy } - public addSheet(sheetId: number, strategy: AddressMappingStrategy) { - if (this.mapping.has(sheetId)) { - throw Error('Sheet already added') + /** + * Adds a new sheet with the specified strategy. + * @throws {Error} if sheet is already added and throwIfSheetAlreadyExists is true + */ + public addSheetWithStrategy(sheetId: number, strategy: AddressMappingStrategy, options: AddressMappingAddSheetOptions = { throwIfSheetAlreadyExists: true }): AddressMappingStrategy { + const strategyFound = this.mapping.get(sheetId) + + if (strategyFound) { + if (options.throwIfSheetAlreadyExists) { + throw Error('Sheet already added') + } + + return strategyFound } this.mapping.set(sheetId, strategy) + return strategy } - public autoAddSheet(sheetId: number, sheetBoundaries: SheetBoundaries) { + /** + * Adds a sheet or changes the strategy for an existing sheet. + * Designed for the purpose of exchanging the placeholder strategy for a real strategy. + */ + public addSheetOrChangeStrategy(sheetId: number, sheetBoundaries: SheetBoundaries): AddressMappingStrategy { + const newStrategy = this.createStrategyBasedOnBoundaries(sheetBoundaries) + const strategyPlaceholder = this.mapping.get(sheetId) + + if (!strategyPlaceholder) { + this.mapping.set(sheetId, newStrategy) + return newStrategy + } + + if (newStrategy instanceof DenseStrategy) { // new strategy is the same as the placeholder + return strategyPlaceholder + } + + this.moveStrategyContent(strategyPlaceholder, newStrategy, sheetId) + this.mapping.set(sheetId, newStrategy) + + return newStrategy + } + + /** + * Moves the content of the source strategy to the target strategy. + */ + private moveStrategyContent(sourceStrategy: AddressMappingStrategy, targetStrategy: AddressMappingStrategy, sheetContext: number) { + const sourceVertices = sourceStrategy.getEntries(sheetContext) + for (const [address, vertex] of sourceVertices) { + targetStrategy.setCell(address, vertex) + } + } + + /** + * Adds a sheet and sets the strategy based on the sheet boundaries. + * @throws {Error} if sheet already exists and throwIfSheetAlreadyExists is true + */ + public addSheetAndSetStrategyBasedOnBoundaries(sheetId: number, sheetBoundaries: SheetBoundaries, options: AddressMappingAddSheetOptions = { throwIfSheetAlreadyExists: true }) { + this.addSheetWithStrategy(sheetId, this.createStrategyBasedOnBoundaries(sheetBoundaries), options) + } + + /** + * Creates a strategy based on the sheet boundaries. + */ + private createStrategyBasedOnBoundaries(sheetBoundaries: SheetBoundaries): AddressMappingStrategy { const {height, width, fill} = sheetBoundaries const strategyConstructor = this.policy.call(fill) - this.addSheet(sheetId, new strategyConstructor(width, height)) + return new strategyConstructor(width, height) + } + + /** + * Adds a placeholder strategy (DenseStrategy) for a sheet. If the sheet already exists, does nothing. + */ + public addSheetStrategyPlaceholderIfNotExists(sheetId: number): void { + if (this.mapping.has(sheetId)) { + return + } + + this.mapping.set(sheetId, new DenseStrategy(0, 0)) + } + + /** + * Removes a sheet from the address mapping. + * If sheet does not exist, does nothing. + * @returns {boolean} true if sheet was removed, false if it did not exist. + */ + public removeSheetIfExists(sheetId: number): boolean { + return this.mapping.delete(sheetId) } + /** + * Gets the interpreter value of a cell at the specified address. + * @returns {InterpreterValue} The interpreter value (returns EmptyValue if cell doesn't exist) + */ public getCellValue(address: SimpleCellAddress): InterpreterValue { const vertex = this.getCell(address) if (vertex === undefined) { return EmptyValue - } else if (vertex instanceof ArrayVertex) { + } else if (vertex instanceof ArrayFormulaVertex) { return vertex.getArrayCellValue(address) } else { return vertex.getCellValue() } } + /** + * Gets the raw cell content at the specified address. + * @returns {RawCellContent} The raw cell content or null if cell doesn't exist or is not a value cell + */ public getRawValue(address: SimpleCellAddress): RawCellContent { const vertex = this.getCell(address) if (vertex instanceof ValueCellVertex) { return vertex.getValues().rawValue - } else if (vertex instanceof ArrayVertex) { + } else if (vertex instanceof ArrayFormulaVertex) { return vertex.getArrayCellRawValue(address) } else { return null } } - /** @inheritDoc */ + /** + * Sets a cell vertex at the specified address. + * @throws {Error} if sheet not initialized + */ public setCell(address: SimpleCellAddress, newVertex: CellVertex) { const sheetMapping = this.mapping.get(address.sheet) + if (!sheetMapping) { throw Error('Sheet not initialized') } sheetMapping.setCell(address, newVertex) } + /** + * Moves a cell from source address to destination address. + * Supports cross-sheet moves (used for placeholder sheet merging). + * @throws {Error} if source sheet not initialized + * @throws {Error} if destination occupied + * @throws {Error} if source cell doesn't exist + */ public moveCell(source: SimpleCellAddress, destination: SimpleCellAddress) { const sheetMapping = this.mapping.get(source.sheet) + if (!sheetMapping) { throw Error('Sheet not initialized.') } - if (source.sheet !== destination.sheet) { - throw Error('Cannot move cells between sheets.') - } - if (sheetMapping.has(destination)) { + + if (this.has(destination)) { throw new Error('Cannot move cell. Destination already occupied.') } + const vertex = sheetMapping.getCell(source) + if (vertex === undefined) { throw new Error('Cannot move cell. No cell with such address.') } + this.setCell(destination, vertex) this.removeCell(source) } + /** + * Removes a cell at the specified address. + * @throws Error if sheet not initialized + */ public removeCell(address: SimpleCellAddress) { const sheetMapping = this.mapping.get(address.sheet) if (!sheetMapping) { @@ -126,7 +271,9 @@ export class AddressMapping { sheetMapping.removeCell(address) } - /** @inheritDoc */ + /** + * Checks if a cell exists at the specified address. + */ public has(address: SimpleCellAddress): boolean { const sheetMapping = this.mapping.get(address.sheet) if (sheetMapping === undefined) { @@ -135,88 +282,110 @@ export class AddressMapping { return sheetMapping.has(address) } - /** @inheritDoc */ - public getHeight(sheetId: number): number { - const sheetMapping = this.mapping.get(sheetId) - if (sheetMapping === undefined) { - throw new NoSheetWithIdError(sheetId) - } + /** + * Gets the height of the specified sheet. + */ + public getSheetHeight(sheetId: number): number { + const sheetMapping = this.getStrategyForSheetOrThrow(sheetId) return sheetMapping.getHeight() } - /** @inheritDoc */ - public getWidth(sheetId: number): number { - const sheetMapping = this.mapping.get(sheetId) - if (!sheetMapping) { - throw new NoSheetWithIdError(sheetId) - } + /** + * Gets the width of the specified sheet. + */ + public getSheetWidth(sheetId: number): number { + const sheetMapping = this.getStrategyForSheetOrThrow(sheetId) return sheetMapping.getWidth() } - public addRows(sheet: number, row: number, numberOfRows: number) { - const sheetMapping = this.mapping.get(sheet) - if (sheetMapping === undefined) { - throw new NoSheetWithIdError(sheet) - } + /** + * Adds rows to a sheet. + */ + public addRows(sheetId: number, row: number, numberOfRows: number) { + const sheetMapping = this.getStrategyForSheetOrThrow(sheetId) sheetMapping.addRows(row, numberOfRows) } + /** + * Removes rows from a sheet. + */ public removeRows(removedRows: RowsSpan) { - const sheetMapping = this.mapping.get(removedRows.sheet) - if (sheetMapping === undefined) { - throw new NoSheetWithIdError(removedRows.sheet) - } + const sheetMapping = this.getStrategyForSheetOrThrow(removedRows.sheet) sheetMapping.removeRows(removedRows) } - public removeSheet(sheetId: number) { - this.mapping.delete(sheetId) - } - - public addColumns(sheet: number, column: number, numberOfColumns: number) { - const sheetMapping = this.mapping.get(sheet) - if (sheetMapping === undefined) { - throw new NoSheetWithIdError(sheet) - } + /** + * Adds columns to a sheet starting at the specified column index. + */ + public addColumns(sheetId: number, column: number, numberOfColumns: number) { + const sheetMapping = this.getStrategyForSheetOrThrow(sheetId) sheetMapping.addColumns(column, numberOfColumns) } + /** + * Removes columns from a sheet. + */ public removeColumns(removedColumns: ColumnsSpan) { - const sheetMapping = this.mapping.get(removedColumns.sheet) - if (sheetMapping === undefined) { - throw new NoSheetWithIdError(removedColumns.sheet) - } + const sheetMapping = this.getStrategyForSheetOrThrow(removedColumns.sheet) sheetMapping.removeColumns(removedColumns) } + /** + * Returns an iterator of cell vertices within the specified rows span. + */ public* verticesFromRowsSpan(rowsSpan: RowsSpan): IterableIterator { yield* this.mapping.get(rowsSpan.sheet)!.verticesFromRowsSpan(rowsSpan) // eslint-disable-line @typescript-eslint/no-non-null-assertion } + /** + * Returns an iterator of cell vertices within the specified columns span. + */ public* verticesFromColumnsSpan(columnsSpan: ColumnsSpan): IterableIterator { yield* this.mapping.get(columnsSpan.sheet)!.verticesFromColumnsSpan(columnsSpan) // eslint-disable-line @typescript-eslint/no-non-null-assertion } + /** + * Returns an iterator of address-vertex pairs within the specified rows span. + */ public* entriesFromRowsSpan(rowsSpan: RowsSpan): IterableIterator<[SimpleCellAddress, CellVertex]> { - yield* this.mapping.get(rowsSpan.sheet)!.entriesFromRowsSpan(rowsSpan) + const sheetMapping = this.getStrategyForSheetOrThrow(rowsSpan.sheet) + yield* sheetMapping.entriesFromRowsSpan(rowsSpan) } + /** + * Returns an iterator of address-vertex pairs within the specified columns span. + */ public* entriesFromColumnsSpan(columnsSpan: ColumnsSpan): IterableIterator<[SimpleCellAddress, CellVertex]> { - yield* this.mapping.get(columnsSpan.sheet)!.entriesFromColumnsSpan(columnsSpan) + const sheetMapping = this.getStrategyForSheetOrThrow(columnsSpan.sheet) + yield* sheetMapping.entriesFromColumnsSpan(columnsSpan) } + /** + * Returns an iterator of all address-vertex pairs across all sheets. + * @returns {IterableIterator<[SimpleCellAddress, Maybe]>} Iterator of [address, vertex] tuples + */ public* entries(): IterableIterator<[SimpleCellAddress, Maybe]> { for (const [sheet, mapping] of this.mapping.entries()) { yield* mapping.getEntries(sheet) } } - public* sheetEntries(sheet: number): IterableIterator<[SimpleCellAddress, CellVertex]> { - const sheetMapping = this.mapping.get(sheet) - if (sheetMapping !== undefined) { - yield* sheetMapping.getEntries(sheet) - } else { - throw new NoSheetWithIdError(sheet) - } + /** + * Returns an iterator of address-vertex pairs for a specific sheet. + * @returns {IterableIterator<[SimpleCellAddress, CellVertex]>} Iterator of [address, vertex] tuples + * @throws {NoSheetWithIdError} if sheet doesn't exist + */ + public* sheetEntries(sheetId: number): IterableIterator<[SimpleCellAddress, CellVertex]> { + const sheetMapping = this.getStrategyForSheetOrThrow(sheetId) + yield* sheetMapping.getEntries(sheetId) + } + + /** + * Checks if a sheet has any entries. + * @throws {NoSheetWithIdError} if sheet doesn't exist + */ + public hasAnyEntries(sheetId: number): boolean { + const iterator = this.sheetEntries(sheetId) + return !iterator.next().done } } diff --git a/src/DependencyGraph/ArrayMapping.ts b/src/DependencyGraph/ArrayMapping.ts index 87ffcaf2b..ee3e48820 100644 --- a/src/DependencyGraph/ArrayMapping.ts +++ b/src/DependencyGraph/ArrayMapping.ts @@ -7,12 +7,23 @@ import {AbsoluteCellRange} from '../AbsoluteCellRange' import {addressKey, SimpleCellAddress} from '../Cell' import {Maybe} from '../Maybe' import {ColumnsSpan, RowsSpan} from '../Span' -import {ArrayVertex} from './' +import {ArrayFormulaVertex} from './' +/** + * Maps top-left corner addresses to their ArrayFormulaVertex instances. + * An ArrayFormulaVertex is created for formulas that output multiple values (e.g., MMULT, TRANSPOSE, array literals). + * The same ArrayFormulaVertex is referenced in AddressMapping for all cells within its spill range. + * ArrayFormulaVertex lifecycle: + * - Prediction (ArraySizePredictor.checkArraySize) → determines if formula will produce array + * - Creation → new ArrayFormulaVertex(...) added via addArrayFormulaVertex() + * - Address registration → setAddressMappingForArrayFormulaVertex() sets the vertex for all cells in range + * - Evaluation → computes actual values, stores in ArrayFormulaVertex.array + * - Shrinking → if content placed in array area, array shrinks via shrinkArrayToCorner() + */ export class ArrayMapping { - public readonly arrayMapping: Map = new Map() + public readonly arrayMapping: Map = new Map() - public getArray(range: AbsoluteCellRange): Maybe { + public getArray(range: AbsoluteCellRange): Maybe { const array = this.getArrayByCorner(range.start) if (array?.getRange().sameAs(range)) { return array @@ -20,11 +31,11 @@ export class ArrayMapping { return } - public getArrayByCorner(address: SimpleCellAddress): Maybe { + public getArrayByCorner(address: SimpleCellAddress): Maybe { return this.arrayMapping.get(addressKey(address)) } - public setArray(range: AbsoluteCellRange, vertex: ArrayVertex) { + public setArray(range: AbsoluteCellRange, vertex: ArrayFormulaVertex) { this.arrayMapping.set(addressKey(range.start), vertex) } @@ -40,7 +51,7 @@ export class ArrayMapping { return this.arrayMapping.size } - public* arraysInRows(rowsSpan: RowsSpan): IterableIterator<[string, ArrayVertex]> { + public* arraysInRows(rowsSpan: RowsSpan): IterableIterator<[string, ArrayFormulaVertex]> { for (const [mtxKey, mtx] of this.arrayMapping.entries()) { if (mtx.spansThroughSheetRows(rowsSpan.sheet, rowsSpan.rowStart, rowsSpan.rowEnd)) { yield [mtxKey, mtx] @@ -48,7 +59,7 @@ export class ArrayMapping { } } - public* arraysInCols(col: ColumnsSpan): IterableIterator<[string, ArrayVertex]> { + public* arraysInCols(col: ColumnsSpan): IterableIterator<[string, ArrayFormulaVertex]> { for (const [mtxKey, mtx] of this.arrayMapping.entries()) { if (mtx.spansThroughSheetColumn(col.sheet, col.columnStart, col.columnEnd)) { yield [mtxKey, mtx] @@ -113,21 +124,21 @@ export class ArrayMapping { } public moveArrayVerticesAfterRowByRows(sheet: number, row: number, numberOfRows: number) { - this.updateArrayVerticesInSheet(sheet, (key: string, vertex: ArrayVertex) => { + this.updateArrayVerticesInSheet(sheet, (key: string, vertex: ArrayFormulaVertex) => { const range = vertex.getRange() return row <= range.start.row ? [range.shifted(0, numberOfRows), vertex] : undefined }) } public moveArrayVerticesAfterColumnByColumns(sheet: number, column: number, numberOfColumns: number) { - this.updateArrayVerticesInSheet(sheet, (key: string, vertex: ArrayVertex) => { + this.updateArrayVerticesInSheet(sheet, (key: string, vertex: ArrayFormulaVertex) => { const range = vertex.getRange() return column <= range.start.col ? [range.shifted(numberOfColumns, 0), vertex] : undefined }) } - private updateArrayVerticesInSheet(sheet: number, fn: (key: string, vertex: ArrayVertex) => Maybe<[AbsoluteCellRange, ArrayVertex]>) { - const updated = Array<[AbsoluteCellRange, ArrayVertex]>() + private updateArrayVerticesInSheet(sheet: number, fn: (key: string, vertex: ArrayFormulaVertex) => Maybe<[AbsoluteCellRange, ArrayFormulaVertex]>) { + const updated = Array<[AbsoluteCellRange, ArrayFormulaVertex]>() for (const [key, vertex] of this.arrayMapping.entries()) { if (vertex.sheet !== sheet) { diff --git a/src/DependencyGraph/DependencyGraph.ts b/src/DependencyGraph/DependencyGraph.ts index 0cdf1778a..530d4bd81 100644 --- a/src/DependencyGraph/DependencyGraph.ts +++ b/src/DependencyGraph/DependencyGraph.ts @@ -28,10 +28,10 @@ import {Ast, collectDependencies, NamedExpressionDependency} from '../parser' import {ColumnsSpan, RowsSpan, Span} from '../Span' import {Statistics, StatType} from '../statistics' import { - ArrayVertex, + ArrayFormulaVertex, CellVertex, EmptyCellVertex, - FormulaCellVertex, + ScalarFormulaVertex, ParsingErrorVertex, RangeVertex, ValueCellVertex, @@ -40,16 +40,19 @@ import { import {AddressMapping} from './AddressMapping/AddressMapping' import {ArrayMapping} from './ArrayMapping' import {collectAddressesDependentToRange} from './collectAddressesDependentToRange' -import {FormulaVertex} from './FormulaCellVertex' +import {FormulaVertex} from './FormulaVertex' import {DependencyQuery, Graph} from './Graph' import {RangeMapping} from './RangeMapping' import {SheetMapping} from './SheetMapping' +import {SheetReferenceRegistrar} from './SheetReferenceRegistrar' import {RawAndParsedValue} from './ValueCellVertex' import {TopSortResult} from './TopSort' +import { findBoundaries } from '../Sheet' export class DependencyGraph { public readonly graph: Graph private changes: ContentChanges = ContentChanges.empty() + public readonly sheetReferenceRegistrar: SheetReferenceRegistrar constructor( public readonly addressMapping: AddressMapping, @@ -62,6 +65,7 @@ export class DependencyGraph { public readonly namedExpressions: NamedExpressions, ) { this.graph = new Graph(this.dependencyQueryVertices) + this.sheetReferenceRegistrar = new SheetReferenceRegistrar(sheetMapping, addressMapping) } /** @@ -109,7 +113,7 @@ export class DependencyGraph { public setValueToCell(address: SimpleCellAddress, value: RawAndParsedValue): ContentChanges { const vertex = this.shrinkPossibleArrayAndGetCell(address) - if (vertex instanceof ArrayVertex) { + if (vertex instanceof ArrayFormulaVertex) { this.arrayMapping.removeArray(vertex.getRange()) } @@ -131,6 +135,11 @@ export class DependencyGraph { return this.getAndClearContentChanges() } + /** + * Sets a cell empty. + * - if vertex has no dependents, removes it from graph, address mapping and range mapping and cleans up its dependencies + * - if vertex has dependents, exchanges it for an EmptyCellVertex and marks it as dirty + */ public setCellEmpty(address: SimpleCellAddress): ContentChanges { const vertex = this.shrinkPossibleArrayAndGetCell(address) if (vertex === undefined) { @@ -163,7 +172,11 @@ export class DependencyGraph { } public processCellDependencies(cellDependencies: CellDependency[], endVertex: Vertex) { - const endVertexId = this.graph.getNodeId(endVertex)! + const endVertexId = this.graph.getNodeId(endVertex) + + if (endVertexId === undefined) { + throw new Error('End vertex not found') + } cellDependencies.forEach((dep: CellDependency) => { if (dep instanceof AbsoluteCellRange) { @@ -172,11 +185,15 @@ export class DependencyGraph { let rangeVertex = this.getRange(range.start, range.end) if (rangeVertex === undefined) { rangeVertex = new RangeVertex(range) - this.rangeMapping.setRange(rangeVertex) + this.rangeMapping.addOrUpdateVertex(rangeVertex) } this.graph.addNodeAndReturnId(rangeVertex) - const rangeVertexId = this.graph.getNodeId(rangeVertex)! + const rangeVertexId = this.graph.getNodeId(rangeVertex) + + if (rangeVertexId === undefined) { + throw new Error('Range vertex not found') + } if (!range.isFinite()) { this.graph.markNodeAsInfiniteRange(rangeVertexId) @@ -210,7 +227,7 @@ export class DependencyGraph { this.correctInfiniteRangesDependenciesByRangeVertex(rangeVertex) } } else if (dep instanceof NamedExpressionDependency) { - const sheetOfVertex = (endVertex as FormulaCellVertex).getAddress(this.lazilyTransformingAstService).sheet + const sheetOfVertex = (endVertex as ScalarFormulaVertex).getAddress(this.lazilyTransformingAstService).sheet const { vertex, id } = this.fetchNamedExpressionVertex(dep.name, sheetOfVertex) this.graph.addEdge(id ?? vertex, endVertexId) } else { @@ -252,7 +269,7 @@ export class DependencyGraph { for (const adjacentNode of this.graph.adjacentNodes(vertex)) { this.graph.markNodeAsDirty(adjacentNode) } - if (vertex instanceof ArrayVertex) { + if (vertex instanceof ArrayFormulaVertex) { if (vertex.isLeftCorner(address)) { this.shrinkArrayToCorner(vertex) this.arrayMapping.removeArray(vertex.getRange()) @@ -285,33 +302,70 @@ export class DependencyGraph { } } - public removeSheet(removedSheetId: number) { - this.clearSheet(removedSheetId) + /** + * Adds a new sheet to the graph. + * If the sheetId was a placeholder sheet, marks its vertices as dirty. + */ + public addSheet(sheetId: number): void { + this.addressMapping.addSheetOrChangeStrategy(sheetId, findBoundaries([])) - for (const [adr, vertex] of this.addressMapping.sheetEntries(removedSheetId)) { - for (const adjacentNode of this.graph.adjacentNodes(vertex)) { - this.graph.markNodeAsDirty(adjacentNode) - } - this.removeVertex(vertex) - this.addressMapping.removeCell(adr) - } + this.stats.measure(StatType.ADJUSTING_ADDRESS_MAPPING, () => { + this.markAllCellsAsDirtyInSheet(sheetId) + }) this.stats.measure(StatType.ADJUSTING_RANGES, () => { - const rangesToRemove = this.rangeMapping.removeRangesInSheet(removedSheetId) - for (const range of rangesToRemove) { - this.removeVertex(range) - } - - this.stats.measure(StatType.ADJUSTING_ADDRESS_MAPPING, () => { - this.addressMapping.removeSheet(removedSheetId) - }) + this.markAllRangesAsDirtyInSheet(sheetId) }) } + /** + * Removes all vertices without dependents in other sheets from address mapping, range mapping and array mapping. + * - If nothing is left, removes the sheet from sheet mapping and address mapping. + * - Otherwise, marks it as placeholder. + */ + public removeSheet(sheetId: number): void { + this.clearSheet(sheetId) + const addressMappingCleared = !this.addressMapping.hasAnyEntries(sheetId) + const rangeMappingCleared = this.rangeMapping.getNumberOfRangesInSheet(sheetId) === 0 + + if (addressMappingCleared && rangeMappingCleared) { + this.sheetMapping.removeSheetIfExists(sheetId) + this.addressMapping.removeSheetIfExists(sheetId) + } else { + this.sheetMapping.markSheetAsPlaceholder(sheetId) + } + } + + /** + * Removes placeholderSheetToDelete and reroutes edges to the corresponding vertices in sheetToKeep + * + * Assumptions about placeholderSheetToDelete: + * - is empty (contains only empty cell vertices and range vertices), + * - empty cell vertices have no dependencies, + * - range vertices have dependencies only in placeholderSheetToDelete, + * - vertices may have dependents in placeholderSheetToDelete and other sheets, + */ + public mergeSheets(sheetToKeep: number, placeholderSheetToDelete: number): void { + if (!this.isPlaceholder(placeholderSheetToDelete)) { + throw new Error(`Cannot merge sheets: sheet ${placeholderSheetToDelete} is not a placeholder`) + } + + this.mergeRangeVertices(sheetToKeep, placeholderSheetToDelete) + this.mergeCellVertices(sheetToKeep, placeholderSheetToDelete) + this.addressMapping.removeSheetIfExists(placeholderSheetToDelete) + this.addStructuralNodesToChangeSet() + } + + /** + * Clears the sheet content. + * - removes all cell vertices without dependents + * - removes all array vertices + * - for vertices with dependents, exchanges them for EmptyCellVertex and marks them as dirty + */ public clearSheet(sheetId: number) { - const arrays: Set = new Set() + const arrays: Set = new Set() for (const [address, vertex] of this.addressMapping.sheetEntries(sheetId)) { - if (vertex instanceof ArrayVertex) { + if (vertex instanceof ArrayFormulaVertex) { arrays.add(vertex) } else { this.setCellEmpty(address) @@ -331,7 +385,7 @@ export class DependencyGraph { for (const adjacentNode of this.graph.adjacentNodes(vertex)) { this.graph.markNodeAsDirty(adjacentNode) } - if (vertex instanceof ArrayVertex) { + if (vertex instanceof ArrayFormulaVertex) { if (vertex.isLeftCorner(address)) { this.shrinkArrayToCorner(vertex) this.arrayMapping.removeArray(vertex.getRange()) @@ -370,7 +424,7 @@ export class DependencyGraph { }) const affectedArrays = this.stats.measure(StatType.ADJUSTING_RANGES, () => { - const result = this.rangeMapping.moveAllRangesInSheetAfterRowByRows(addedRows.sheet, addedRows.rowStart, addedRows.numberOfRows) + const result = this.rangeMapping.moveAllRangesInSheetAfterAddingRows(addedRows.sheet, addedRows.rowStart, addedRows.numberOfRows) this.fixRangesWhenAddingRows(addedRows.sheet, addedRows.rowStart, addedRows.numberOfRows) return this.getArrayVerticesRelatedToRanges(result.verticesWithChangedSize) }) @@ -394,7 +448,7 @@ export class DependencyGraph { }) const affectedArrays = this.stats.measure(StatType.ADJUSTING_RANGES, () => { - const result = this.rangeMapping.moveAllRangesInSheetAfterColumnByColumns(addedColumns.sheet, addedColumns.columnStart, addedColumns.numberOfColumns) + const result = this.rangeMapping.moveAllRangesInSheetAfterAddingColumns(addedColumns.sheet, addedColumns.columnStart, addedColumns.numberOfColumns) this.fixRangesWhenAddingColumns(addedColumns.sheet, addedColumns.columnStart, addedColumns.numberOfColumns) return this.getArrayVerticesRelatedToRanges(result.verticesWithChangedSize) }) @@ -412,7 +466,7 @@ export class DependencyGraph { return {affectedArrays, contentChanges: this.getAndClearContentChanges()} } - public isThereSpaceForArray(arrayVertex: ArrayVertex): boolean { + public isThereSpaceForArray(arrayVertex: ArrayFormulaVertex): boolean { const range = arrayVertex.getRangeOrUndef() if (range === undefined) { return false @@ -482,15 +536,22 @@ export class DependencyGraph { this.rangeMapping.moveRangesInsideSourceRange(sourceRange, toRight, toBottom, toSheet) } - public setArrayEmpty(arrayVertex: ArrayVertex) { + /** + * Sets an array empty. + * - removes all corresponding entries from address mapping + * - reroutes the edges + * - removes vertex from graph and cleans up its dependencies + * - removes vertex from range mapping and array mapping + */ + public setArrayEmpty(arrayVertex: ArrayFormulaVertex) { const arrayRange = AbsoluteCellRange.spanFrom(arrayVertex.getAddress(this.lazilyTransformingAstService), arrayVertex.width, arrayVertex.height) - const adjacentNodes = this.graph.adjacentNodes(arrayVertex) + const dependentVertices = this.graph.adjacentNodes(arrayVertex) for (const address of arrayRange.addresses(this)) { this.addressMapping.removeCell(address) } - for (const adjacentNode of adjacentNodes.values()) { + for (const adjacentNode of dependentVertices.values()) { const nodeDependencies = collectAddressesDependentToRange(this.functionRegistry, adjacentNode, arrayVertex.getRange(), this.lazilyTransformingAstService, this) for (const address of nodeDependencies) { const { vertex, id } = this.fetchCellOrCreateEmpty(address) @@ -510,14 +571,17 @@ export class DependencyGraph { this.addressMapping.setCell(address, vertex) } - public addArrayVertex(address: SimpleCellAddress, vertex: ArrayVertex): void { + public addArrayVertex(address: SimpleCellAddress, vertex: ArrayFormulaVertex): void { this.graph.addNodeAndReturnId(vertex) this.setAddressMappingForArrayVertex(vertex, address) } - public* arrayFormulaNodes(): IterableIterator { + /** + * Iterator over all array formula nodes in the graph. + */ + public* arrayFormulaNodes(): IterableIterator { for (const vertex of this.graph.getNodes()) { - if (vertex instanceof ArrayVertex) { + if (vertex instanceof ArrayFormulaVertex) { yield vertex } } @@ -532,22 +596,38 @@ export class DependencyGraph { } public fetchCell(address: SimpleCellAddress): CellVertex { - return this.addressMapping.fetchCell(address) + return this.addressMapping.getCellOrThrow(address) } + /** + * Gets the cell vertex at the specified address. + * @throws {NoSheetWithIdError} if sheet doesn't exist + */ public getCell(address: SimpleCellAddress): Maybe { - return this.addressMapping.getCell(address) + return this.addressMapping.getCell(address, { throwIfSheetNotExists: true }) } public getCellValue(address: SimpleCellAddress): InterpreterValue { + if (this.isPlaceholder(address.sheet)) { + return new CellError(ErrorType.REF, ErrorMessage.SheetRef) + } + return this.addressMapping.getCellValue(address) } public getRawValue(address: SimpleCellAddress): RawCellContent { + if (this.isPlaceholder(address.sheet)) { + return null + } + return this.addressMapping.getRawValue(address) } public getScalarValue(address: SimpleCellAddress): InternalScalarValue { + if (this.isPlaceholder(address.sheet)) { + return new CellError(ErrorType.REF, ErrorMessage.SheetRef) + } + const value = this.addressMapping.getCellValue(address) if (value instanceof SimpleRangeValue) { return new CellError(ErrorType.VALUE, ErrorMessage.ScalarExpected) @@ -560,23 +640,23 @@ export class DependencyGraph { } public getSheetId(sheetName: string): number { - return this.sheetMapping.fetch(sheetName) + return this.sheetMapping.getSheetIdOrThrowError(sheetName) } public getSheetHeight(sheet: number): number { - return this.addressMapping.getHeight(sheet) + return this.addressMapping.getSheetHeight(sheet) } public getSheetWidth(sheet: number): number { - return this.addressMapping.getWidth(sheet) + return this.addressMapping.getSheetWidth(sheet) } - public getArray(range: AbsoluteCellRange): Maybe { + public getArray(range: AbsoluteCellRange): Maybe { return this.arrayMapping.getArray(range) } public getRange(start: SimpleCellAddress, end: SimpleCellAddress): Maybe { - return this.rangeMapping.getRange(start, end) + return this.rangeMapping.getRangeVertex(start, end) } public topSortWithScc(): TopSortResult { @@ -593,7 +673,7 @@ export class DependencyGraph { public forceApplyPostponedTransformations() { for (const vertex of this.graph.getNodes()) { - if (vertex instanceof FormulaCellVertex) { + if (vertex instanceof ScalarFormulaVertex) { vertex.ensureRecentData(this.lazilyTransformingAstService) } } @@ -639,7 +719,7 @@ export class DependencyGraph { return values } - public shrinkArrayToCorner(array: ArrayVertex) { + public shrinkArrayToCorner(array: ArrayFormulaVertex) { this.cleanAddressMappingUnderArray(array) for (const adjacentVertex of this.adjacentArrayVertices(array)) { let relevantDependencies @@ -665,7 +745,7 @@ export class DependencyGraph { public isArrayInternalCell(address: SimpleCellAddress): boolean { const vertex = this.getCell(address) - return vertex instanceof ArrayVertex && !vertex.isLeftCorner(address) + return vertex instanceof ArrayFormulaVertex && !vertex.isLeftCorner(address) } public getAndClearContentChanges(): ContentChanges { @@ -678,16 +758,108 @@ export class DependencyGraph { const deps = this.graph.adjacentNodes(inputVertex) const ret: (SimpleCellRange | SimpleCellAddress)[] = [] deps.forEach((vertex: Vertex) => { - const castVertex = vertex as RangeVertex | FormulaCellVertex | ArrayVertex - if (castVertex instanceof RangeVertex) { - ret.push(simpleCellRange(castVertex.start, castVertex.end)) - } else { - ret.push(castVertex.getAddress(this.lazilyTransformingAstService)) + if (vertex instanceof RangeVertex) { + ret.push(simpleCellRange(vertex.start, vertex.end)) + } else if (vertex instanceof FormulaVertex) { + ret.push(vertex.getAddress(this.lazilyTransformingAstService)) } }) return ret } + /** + * Marks all cell vertices in the sheet as dirty. + */ + private markAllCellsAsDirtyInSheet(sheetId: number): void { + const sheetCells = this.addressMapping.sheetEntries(sheetId) + for (const [, vertex] of sheetCells) { + this.graph.markNodeAsDirty(vertex) + } + } + + /** + * Marks all range vertices in the sheet as dirty. + */ + private markAllRangesAsDirtyInSheet(sheetId: number): void { + const sheetRanges = this.rangeMapping.rangesInSheet(sheetId) + + for (const vertex of sheetRanges) { + this.graph.markNodeAsDirty(vertex) + } + } + + /** + * For each range vertex in placeholderSheetToDelete: + * - reroutes dependencies and dependents of range vertex to the corresponding vertex in sheetToKeep + * - removes range vertex from graph and range mapping + * - cleans up dependencies of the removed vertex + */ + private mergeRangeVertices(sheetToKeep: number, placeholderSheetToDelete: number): void { + const rangeVertices = Array.from(this.rangeMapping.rangesInSheet(placeholderSheetToDelete)) + + for (const vertexToDelete of rangeVertices) { + if (!this.graph.hasNode(vertexToDelete)) { + continue + } + + const start = vertexToDelete.start + const end = vertexToDelete.end + + if (start.sheet !== placeholderSheetToDelete && end.sheet !== placeholderSheetToDelete) { + continue + } + + const targetStart = simpleCellAddress(sheetToKeep, start.col, start.row) + const targetEnd = simpleCellAddress(sheetToKeep, end.col, end.row) + const vertexToKeep = this.rangeMapping.getRangeVertex(targetStart, targetEnd) + + if (vertexToKeep) { + this.rerouteDependents(vertexToDelete, vertexToKeep) + this.removeVertexAndRerouteDependencies(vertexToDelete, vertexToKeep) + this.rangeMapping.removeVertexIfExists(vertexToDelete) + this.graph.markNodeAsDirty(vertexToKeep) + } else { + this.rangeMapping.removeVertexIfExists(vertexToDelete) + vertexToDelete.range.moveToSheet(sheetToKeep) + this.rangeMapping.addOrUpdateVertex(vertexToDelete) + this.graph.markNodeAsDirty(vertexToDelete) + } + } + } + + /** + * For each cell vertex in placeholderSheetToDelete: + * - reroutes dependents of cell vertex to the corresponding vertex in sheetToKeep + * - removes cell vertex from graph and address mapping + * - cleans up dependencies of the removed vertex + */ + private mergeCellVertices(sheetToKeep: number, placeholderSheetToDelete: number): void { + const cellVertices = Array.from(this.addressMapping.sheetEntries(placeholderSheetToDelete)) // placeholder sheet contains only EmptyCellVertex-es + + for (const [addressToDelete, vertexToDelete] of cellVertices) { + const addressToKeep = simpleCellAddress(sheetToKeep, addressToDelete.col, addressToDelete.row) + const vertexToKeep = this.getCell(addressToKeep) + + if (vertexToKeep) { + this.rerouteDependents(vertexToDelete, vertexToKeep) + this.removeVertexAndCleanupDependencies(vertexToDelete) + this.addressMapping.removeCell(addressToDelete) + this.graph.markNodeAsDirty(vertexToKeep) + } else { + this.addressMapping.moveCell(addressToDelete, addressToKeep) + this.graph.markNodeAsDirty(vertexToDelete) + } + } + } + + /** + * Checks if the given sheet ID refers to a placeholder sheet (doesn't exist but is referenced by other sheets) + */ + private isPlaceholder(sheetId: number): boolean { + return sheetId !== NamedExpressions.SHEET_FOR_WORKBOOK_EXPRESSIONS && + !this.sheetMapping.hasSheetWithId(sheetId, { includePlaceholders: false }) + } + private exchangeGraphNode(oldNode: Vertex, newNode: Vertex) { this.graph.addNodeAndReturnId(newNode) const adjNodesStored = this.graph.adjacentNodes(oldNode) @@ -699,7 +871,7 @@ export class DependencyGraph { }) } - private setArray(range: AbsoluteCellRange, vertex: ArrayVertex): void { + private setArray(range: AbsoluteCellRange, vertex: ArrayFormulaVertex): void { this.arrayMapping.setArray(range, vertex) } @@ -712,7 +884,11 @@ export class DependencyGraph { } const { vertex, id: maybeVertexId } = this.fetchCellOrCreateEmpty(address) - const vertexId = maybeVertexId ?? this.graph.getNodeId(vertex)! + const vertexId = maybeVertexId ?? this.graph.getNodeId(vertex) + + if (vertexId === undefined) { + throw new Error('Vertex not found') + } relevantInfiniteRanges.forEach(({ id }) => { this.graph.addEdge(vertexId, id) @@ -736,12 +912,12 @@ export class DependencyGraph { const [address, dependencies] = dependenciesResult return dependencies.map((dependency: CellDependency) => { if (dependency instanceof AbsoluteCellRange) { - return [dependency.start, this.rangeMapping.fetchRange(dependency.start, dependency.end)] + return [dependency.start, this.rangeMapping.getVertexOrThrow(dependency.start, dependency.end)] } else if (dependency instanceof NamedExpressionDependency) { const namedExpression = this.namedExpressions.namedExpressionOrPlaceholder(dependency.name, address.sheet) - return [namedExpression.address, this.addressMapping.fetchCell(namedExpression.address)] + return [namedExpression.address, this.addressMapping.getCellOrThrow(namedExpression.address)] } else { - return [dependency, this.addressMapping.fetchCell(dependency)] + return [dependency, this.addressMapping.getCellOrThrow(dependency)] } }) } else { @@ -750,8 +926,8 @@ export class DependencyGraph { } } - private getArrayVerticesRelatedToRanges(ranges: RangeVertex[]): Set { - const arrayVertices = new Set() + private getArrayVerticesRelatedToRanges(ranges: RangeVertex[]): Set { + const arrayVertices = new Set() ranges.forEach(range => { if (!this.graph.hasNode(range)) { @@ -759,7 +935,7 @@ export class DependencyGraph { } this.graph.adjacentNodes(range).forEach(adjacentVertex => { - if (adjacentVertex instanceof ArrayVertex) { + if (adjacentVertex instanceof ArrayFormulaVertex) { arrayVertices.add(adjacentVertex) } }) @@ -784,7 +960,7 @@ export class DependencyGraph { }) } - private cleanAddressMappingUnderArray(vertex: ArrayVertex) { + private cleanAddressMappingUnderArray(vertex: ArrayFormulaVertex) { const arrayRange = vertex.getRange() for (const address of arrayRange.addresses(this)) { const oldValue = vertex.getArrayCellValue(address) @@ -801,7 +977,7 @@ export class DependencyGraph { } } - private* formulaDirectDependenciesToArray(vertex: FormulaVertex, array: ArrayVertex): IterableIterator<[SimpleCellAddress, CellVertex]> { + private* formulaDirectDependenciesToArray(vertex: FormulaVertex, array: ArrayFormulaVertex): IterableIterator<[SimpleCellAddress, CellVertex]> { const [, formulaDependencies] = this.formulaDependencyQuery(vertex) ?? [] if (formulaDependencies === undefined) { return @@ -817,7 +993,7 @@ export class DependencyGraph { } } - private* rangeDirectDependenciesToArray(vertex: RangeVertex, array: ArrayVertex): IterableIterator<[SimpleCellAddress, CellVertex]> { + private* rangeDirectDependenciesToArray(vertex: RangeVertex, array: ArrayFormulaVertex): IterableIterator<[SimpleCellAddress, CellVertex]> { const {restRange: range} = this.rangeMapping.findSmallerRange(vertex.range) for (const address of range.addresses(this)) { if (array.getRange().addressInRange(address)) { @@ -827,7 +1003,7 @@ export class DependencyGraph { } } - private* adjacentArrayVertices(vertex: ArrayVertex): IterableIterator { + private* adjacentArrayVertices(vertex: ArrayFormulaVertex): IterableIterator { const adjacentNodes = this.graph.adjacentNodes(vertex) for (const item of adjacentNodes) { if (item instanceof FormulaVertex || item instanceof RangeVertex) { @@ -840,12 +1016,14 @@ export class DependencyGraph { const allDeps: [(SimpleCellAddress | AbsoluteCellRange), Vertex][] = [] const {smallerRangeVertex, restRange} = this.rangeMapping.findSmallerRange((vertex as RangeVertex).range) //checking whether this range was splitted by bruteForce or not let range + if (smallerRangeVertex !== undefined && this.graph.adjacentNodes(smallerRangeVertex).has(vertex)) { range = restRange allDeps.push([new AbsoluteCellRange(smallerRangeVertex.start, smallerRangeVertex.end), smallerRangeVertex]) } else { //did we ever need to use full range range = (vertex as RangeVertex).range } + for (const address of range.addresses(this)) { const cell = this.addressMapping.getCell(address) if (cell !== undefined) { @@ -890,7 +1068,7 @@ export class DependencyGraph { } while (find.smallerRangeVertex === undefined) { const newRangeVertex = new RangeVertex(AbsoluteCellRange.spanFrom(currentRangeVertex.range.start, currentRangeVertex.range.width(), currentRangeVertex.range.height() - 1)) - this.rangeMapping.setRange(newRangeVertex) + this.rangeMapping.addOrUpdateVertex(newRangeVertex) this.graph.addNodeAndReturnId(newRangeVertex) const restRange = new AbsoluteCellRange(simpleCellAddress(currentRangeVertex.range.start.sheet, currentRangeVertex.range.start.col, currentRangeVertex.range.end.row), currentRangeVertex.range.end) this.addAllFromRange(restRange, currentRangeVertex) @@ -934,13 +1112,13 @@ export class DependencyGraph { const address = vertex.getAddress(this.lazilyTransformingAstService) const range = AbsoluteCellRange.spanFrom(address, vertex.width, vertex.height) const oldNode = this.shrinkPossibleArrayAndGetCell(address) - if (vertex instanceof ArrayVertex) { + if (vertex instanceof ArrayFormulaVertex) { this.setArray(range, vertex) } this.exchangeOrAddGraphNode(oldNode, vertex) this.addressMapping.setCell(address, vertex) - if (vertex instanceof ArrayVertex) { + if (vertex instanceof ArrayFormulaVertex) { if (!this.isThereSpaceForArray(vertex)) { return } @@ -961,7 +1139,7 @@ export class DependencyGraph { private setAddressMappingForArrayVertex(vertex: CellVertex, formulaAddress: SimpleCellAddress): void { this.addressMapping.setCell(formulaAddress, vertex) - if (!(vertex instanceof ArrayVertex)) { + if (!(vertex instanceof ArrayFormulaVertex)) { return } @@ -987,7 +1165,8 @@ export class DependencyGraph { verticesWithChangedSize } = this.rangeMapping.truncateRanges(span, coordinate) for (const [existingVertex, mergedVertex] of verticesToMerge) { - this.mergeRangeVertices(existingVertex, mergedVertex) + this.rerouteDependents(mergedVertex, existingVertex) + this.removeVertexAndCleanupDependencies(mergedVertex) } for (const rangeVertex of verticesToRemove) { this.removeVertexAndCleanupDependencies(rangeVertex) @@ -1067,7 +1246,7 @@ export class DependencyGraph { private shrinkPossibleArrayAndGetCell(address: SimpleCellAddress): Maybe { const vertex = this.getCell(address) - if (!(vertex instanceof ArrayVertex)) { + if (!(vertex instanceof ArrayFormulaVertex)) { return vertex } this.setNoSpaceIfArray(vertex) @@ -1075,35 +1254,64 @@ export class DependencyGraph { } private setNoSpaceIfArray(vertex: Maybe) { - if (vertex instanceof ArrayVertex) { + if (vertex instanceof ArrayFormulaVertex) { this.shrinkArrayToCorner(vertex) vertex.setNoSpace() } } + /** + * Removes a vertex from the graph and range mapping and cleans up its dependencies. + */ private removeVertex(vertex: Vertex) { this.removeVertexAndCleanupDependencies(vertex) if (vertex instanceof RangeVertex) { - this.rangeMapping.removeRange(vertex) + this.rangeMapping.removeVertexIfExists(vertex) } } - private mergeRangeVertices(existingVertex: RangeVertex, newVertex: RangeVertex) { - const adjNodesStored = this.graph.adjacentNodes(newVertex) + /** + * Reroutes dependent vertices of source to target. Also removes the edge target -> source if it exists. + */ + private rerouteDependents(source: Vertex, target: Vertex) { + const dependents = this.graph.adjacentNodes(source) + this.graph.removeEdgeIfExists(target, source) - this.removeVertexAndCleanupDependencies(newVertex) - this.graph.removeEdgeIfExists(existingVertex, newVertex) - adjNodesStored.forEach((adjacentNode) => { + dependents.forEach((adjacentNode) => { if (this.graph.hasNode(adjacentNode)) { - this.graph.addEdge(existingVertex, adjacentNode) + this.graph.addEdge(target, adjacentNode) + } + }) + + } + + /** + * Removes a vertex from graph and reroutes its dependencies to other vertex. Also removes the edge vertexToKeep -> vertexToDelete if it exists. + */ + private removeVertexAndRerouteDependencies(vertexToDelete: Vertex, vertexToKeep: Vertex) { + const dependencies = this.graph.removeNode(vertexToDelete) + + this.graph.removeEdgeIfExists(vertexToKeep, vertexToDelete) + + dependencies.forEach(([_, dependency]) => { + if (this.graph.hasNode(dependency)) { + this.graph.addEdge(dependency, vertexToKeep) } }) } + /** + * Removes a vertex from graph and cleans up its dependencies. + * Dependency clean up = remove all RangeVertex and EmptyCellVertex dependencies if no other vertex depends on them. + * Also cleans up placeholder sheets that have no remaining vertices (not needed anymore) + */ private removeVertexAndCleanupDependencies(inputVertex: Vertex) { const dependencies = new Set(this.graph.removeNode(inputVertex)) + const affectedSheets = new Set() + while (dependencies.size > 0) { - const dependency = dependencies.values().next().value + const dependency = dependencies.values().next().value as [SimpleCellAddress | SimpleCellRange, Vertex] + dependencies.delete(dependency) const [address, vertex] = dependency if (this.graph.hasNode(vertex) && this.graph.adjacentNodesCount(vertex) === 0) { @@ -1111,17 +1319,35 @@ export class DependencyGraph { this.graph.removeNode(vertex).forEach((candidate) => dependencies.add(candidate)) } if (vertex instanceof RangeVertex) { - this.rangeMapping.removeRange(vertex) - } else if (vertex instanceof EmptyCellVertex) { + this.rangeMapping.removeVertexIfExists(vertex) + affectedSheets.add(vertex.sheet) + } else if (vertex instanceof EmptyCellVertex && isSimpleCellAddress(address)) { this.addressMapping.removeCell(address) + affectedSheets.add(address.sheet) } } } + + this.cleanupPlaceholderSheets(affectedSheets) + } + + /** + * Removes placeholder sheets that have no remaining vertices. + */ + private cleanupPlaceholderSheets(sheetIds: Set): void { + for (const sheetId of sheetIds) { + if (this.isPlaceholder(sheetId) && + !this.addressMapping.hasAnyEntries(sheetId) && + this.rangeMapping.getNumberOfRangesInSheet(sheetId) === 0) { + this.sheetMapping.removeSheetIfExists(sheetId, { includePlaceholders: true }) + this.addressMapping.removeSheetIfExists(sheetId) + } + } } } export interface ArrayAffectingGraphChangeResult { - affectedArrays: Set, + affectedArrays: Set, } export interface EagerChangesGraphChangeResult extends ArrayAffectingGraphChangeResult { diff --git a/src/DependencyGraph/FormulaCellVertex.ts b/src/DependencyGraph/FormulaVertex.ts similarity index 96% rename from src/DependencyGraph/FormulaCellVertex.ts rename to src/DependencyGraph/FormulaVertex.ts index 2656e1238..cf1c02161 100644 --- a/src/DependencyGraph/FormulaCellVertex.ts +++ b/src/DependencyGraph/FormulaVertex.ts @@ -33,9 +33,9 @@ export abstract class FormulaVertex { static fromAst(formula: Ast, address: SimpleCellAddress, size: ArraySize, version: number) { if (size.isScalar()) { - return new FormulaCellVertex(formula, address, version) + return new ScalarFormulaVertex(formula, address, version) } else { - return new ArrayVertex(formula, address, size, version) + return new ArrayFormulaVertex(formula, address, size, version) } } @@ -79,7 +79,7 @@ export abstract class FormulaVertex { public abstract isComputed(): boolean } -export class ArrayVertex extends FormulaVertex { +export class ArrayFormulaVertex extends FormulaVertex { array: CellArray constructor(formula: Ast, cellAddress: SimpleCellAddress, size: ArraySize, version: number = 0) { @@ -223,7 +223,7 @@ export class ArrayVertex extends FormulaVertex { /** * Represents vertex which keeps formula */ -export class FormulaCellVertex extends FormulaVertex { +export class ScalarFormulaVertex extends FormulaVertex { /** Most recently computed value of this formula. */ private cachedCellValue?: InterpreterValue diff --git a/src/DependencyGraph/Graph.ts b/src/DependencyGraph/Graph.ts index 9a0fc2162..60c5f3487 100644 --- a/src/DependencyGraph/Graph.ts +++ b/src/DependencyGraph/Graph.ts @@ -414,6 +414,6 @@ export class Graph { * Returns error for missing node. */ private missingNodeError(node: Node): Error { - return new Error(`Unknown node ${node}`) + return new Error(`Unknown node ${JSON.stringify(node)}`) } } diff --git a/src/DependencyGraph/RangeMapping.ts b/src/DependencyGraph/RangeMapping.ts index 9666ad021..df5d4b9be 100644 --- a/src/DependencyGraph/RangeMapping.ts +++ b/src/DependencyGraph/RangeMapping.ts @@ -19,39 +19,56 @@ export interface TruncateRangesResult extends AdjustRangesResult { verticesWithChangedSize: RangeVertex[], } +type AdjustVerticesOperationResult = { + changedSize: boolean, + vertex: RangeVertex, +} + +type AdjustVerticesOperation = (key: string, vertex: RangeVertex) => Maybe + /** - * Mapping from address ranges to range vertices + * Maintains a per-sheet map from serialized start/end coordinates to `RangeVertex`. + * - Every range vertex in dependency graph should be stored in this mapping. + * - Guarantees uniqueness: one vertex per distinct rectangle, enabling cache reuse. + * - Implements "smaller prefix + tail row" optimization: if A1:A4 exists, A1:A5 depends on it + only A5. + * - RangeVertex stores cached results for associative aggregates (SUM, COUNT) and criterion functions. */ export class RangeMapping { - /** Map in which actual data is stored. */ - private rangeMapping: Map> = new Map() + /** + * Map sheetId -> address of start and end (as string) -> vertex + */ + private rangeMapping: Map> = new Map() - public getMappingSize(sheet: number): Maybe { + /** + * Returns number of ranges in the sheet or 0 if the sheet does not exist + */ + public getNumberOfRangesInSheet(sheet: number): number { return this.rangeMapping.get(sheet)?.size ?? 0 } /** - * Saves range vertex - * - * @param vertex - vertex to save + * Adds or updates vertex in the mapping */ - public setRange(vertex: RangeVertex) { - let sheetMap = this.rangeMapping.get(vertex.getStart().sheet) + public addOrUpdateVertex(vertex: RangeVertex): void { + let sheetMap = this.rangeMapping.get(vertex.sheet) if (sheetMap === undefined) { sheetMap = new Map() - this.rangeMapping.set(vertex.getStart().sheet, sheetMap) + this.rangeMapping.set(vertex.sheet, sheetMap) } - const key = keyFromAddresses(vertex.getStart(), vertex.getEnd()) + const key = RangeMapping.calculateRangeKey(vertex.start, vertex.end) sheetMap.set(key, vertex) } - public removeRange(vertex: RangeVertex) { - const sheet = vertex.getStart().sheet + /** + * Removes vertex from the mapping if it exists + */ + public removeVertexIfExists(vertex: RangeVertex): void { + const sheet = vertex.sheet const sheetMap = this.rangeMapping.get(sheet) if (sheetMap === undefined) { return } - const key = keyFromAddresses(vertex.getStart(), vertex.getEnd()) + const key = RangeMapping.calculateRangeKey(vertex.start, vertex.end) sheetMap.delete(key) if (sheetMap.size === 0) { this.rangeMapping.delete(sheet) @@ -60,18 +77,18 @@ export class RangeMapping { /** * Returns associated vertex for given range - * - * @param start - top-left corner of the range - * @param end - bottom-right corner of the range */ - public getRange(start: SimpleCellAddress, end: SimpleCellAddress): Maybe { + public getRangeVertex(start: SimpleCellAddress, end: SimpleCellAddress): Maybe { const sheetMap = this.rangeMapping.get(start.sheet) - const key = keyFromAddresses(start, end) + const key = RangeMapping.calculateRangeKey(start, end) return sheetMap?.get(key) } - public fetchRange(start: SimpleCellAddress, end: SimpleCellAddress): RangeVertex { - const maybeRange = this.getRange(start, end) + /** + * Returns associated vertex for given range or throws an error if not found + */ + public getVertexOrThrow(start: SimpleCellAddress, end: SimpleCellAddress): RangeVertex { + const maybeRange = this.getRangeVertex(start, end) if (!maybeRange) { throw Error('Range does not exist') } @@ -99,9 +116,9 @@ export class RangeMapping { } const verticesToMerge: [RangeVertex, RangeVertex][] = [] - updated.sort((left, right) => compareBy(left[1], right[1], coordinate)) + updated.sort((left, right) => RangeMapping.compareBy(left[1], right[1], coordinate)) for (const [oldKey, vertex] of updated) { - const newKey = keyFromRange(vertex.range) + const newKey = RangeMapping.calculateRangeKey(vertex.range.start, vertex.range.end) if (newKey === oldKey) { continue } @@ -111,7 +128,7 @@ export class RangeMapping { if (existingVertex !== undefined && vertex != existingVertex) { verticesToMerge.push([existingVertex, vertex]) } else { - this.setRange(vertex) + this.addOrUpdateVertex(vertex) } } @@ -122,7 +139,7 @@ export class RangeMapping { } } - public moveAllRangesInSheetAfterRowByRows(sheet: number, row: number, numberOfRows: number): AdjustRangesResult { + public moveAllRangesInSheetAfterAddingRows(sheet: number, row: number, numberOfRows: number): AdjustRangesResult { return this.updateVerticesFromSheet(sheet, (key: string, vertex: RangeVertex) => { if (row <= vertex.start.row) { vertex.range.shiftByRows(numberOfRows) @@ -142,7 +159,7 @@ export class RangeMapping { }) } - public moveAllRangesInSheetAfterColumnByColumns(sheet: number, column: number, numberOfColumns: number): AdjustRangesResult { + public moveAllRangesInSheetAfterAddingColumns(sheet: number, column: number, numberOfColumns: number): AdjustRangesResult { return this.updateVerticesFromSheet(sheet, (key: string, vertex: RangeVertex) => { if (column <= vertex.start.col) { vertex.range.shiftByColumns(numberOfColumns) @@ -178,15 +195,6 @@ export class RangeMapping { }) } - public removeRangesInSheet(sheet: number): IterableIterator { - if (this.rangeMapping.has(sheet)) { - const ranges = this.rangeMapping.get(sheet)!.values() - this.rangeMapping.delete(sheet) - return ranges - } - return [][Symbol.iterator]() - } - public* rangesInSheet(sheet: number): IterableIterator { const sheetMap = this.rangeMapping.get(sheet) if (!sheetMap) { @@ -204,14 +212,12 @@ export class RangeMapping { } /** - * Finds smaller range does have own vertex. - * - * @param range + * Finds smaller range if exists. */ public findSmallerRange(range: AbsoluteCellRange): { smallerRangeVertex?: RangeVertex, restRange: AbsoluteCellRange } { if (range.height() > 1 && Number.isFinite(range.height())) { const valuesRangeEndRowLess = simpleCellAddress(range.end.sheet, range.end.col, range.end.row - 1) - const rowLessVertex = this.getRange(range.start, valuesRangeEndRowLess) + const rowLessVertex = this.getRangeVertex(range.start, valuesRangeEndRowLess) if (rowLessVertex !== undefined) { const restRange = AbsoluteCellRange.fromSimpleCellAddresses(simpleCellAddress(range.start.sheet, range.start.col, range.end.row), range.end) return { @@ -225,6 +231,28 @@ export class RangeMapping { } } + /** + * Calculates a string key from start and end addresses + */ + private static calculateRangeKey(start: SimpleCellAddress, end: SimpleCellAddress): string { + return `${start.col},${start.row},${end.col},${end.row}` + } + + /** + * Compares two range vertices by their start and end addresses using the provided coordinate function + */ + private static compareBy(left: RangeVertex, right: RangeVertex, coordinate: (address: SimpleCellAddress) => number): number { + const leftStart = coordinate(left.range.start) + const rightStart = coordinate(right.range.start) + if (leftStart === rightStart) { + const leftEnd = coordinate(left.range.end) + const rightEnd = coordinate(right.range.end) + return leftEnd - rightEnd + } else { + return leftStart - rightStart + } + } + private* entriesFromSheet(sheet: number): IterableIterator<[string, RangeVertex]> { const sheetMap = this.rangeMapping.get(sheet) if (!sheetMap) { @@ -233,8 +261,13 @@ export class RangeMapping { yield* sheetMap.entries() } - private removeByKey(sheet: number, key: string) { - this.rangeMapping.get(sheet)!.delete(key) + private removeByKey(sheet: number, key: string): void { + const sheetMap = this.rangeMapping.get(sheet) + + if (!sheetMap) { + throw new Error(`Sheet ${sheet} not found`) + } + sheetMap.delete(key) } private getByKey(sheet: number, key: string): RangeVertex | undefined { @@ -242,7 +275,7 @@ export class RangeMapping { } private updateVerticesFromSheet(sheet: number, fn: AdjustVerticesOperation): AdjustRangesResult { - const updated = Array() + const updated = Array() for (const [key, vertex] of this.entriesFromSheet(sheet)) { const result = fn(key, vertex) @@ -253,7 +286,7 @@ export class RangeMapping { } updated.forEach(entry => { - this.setRange(entry.vertex) + this.addOrUpdateVertex(entry.vertex) }) return { @@ -263,30 +296,3 @@ export class RangeMapping { } } } - -type AdjustVeticesOperationResult = { - changedSize: boolean, - vertex: RangeVertex, -} - -type AdjustVerticesOperation = (key: string, vertex: RangeVertex) => Maybe - -function keyFromAddresses(start: SimpleCellAddress, end: SimpleCellAddress): string { - return `${start.col},${start.row},${end.col},${end.row}` -} - -function keyFromRange(range: AbsoluteCellRange): string { - return keyFromAddresses(range.start, range.end) -} - -const compareBy = (left: RangeVertex, right: RangeVertex, coordinate: (address: SimpleCellAddress) => number) => { - const leftStart = coordinate(left.range.start) - const rightStart = coordinate(left.range.start) - if (leftStart === rightStart) { - const leftEnd = coordinate(left.range.end) - const rightEnd = coordinate(right.range.end) - return leftEnd - rightEnd - } else { - return leftStart - rightStart - } -} diff --git a/src/DependencyGraph/RangeVertex.ts b/src/DependencyGraph/RangeVertex.ts index e92213dcd..b64e1187d 100644 --- a/src/DependencyGraph/RangeVertex.ts +++ b/src/DependencyGraph/RangeVertex.ts @@ -3,8 +3,8 @@ * Copyright (c) 2025 Handsoncode. All rights reserved. */ +import { SimpleCellAddress } from '..' import {AbsoluteCellRange} from '../AbsoluteCellRange' -import {SimpleCellAddress} from '../Cell' import {CriterionLambda} from '../interpreter/Criterion' /** @@ -30,15 +30,15 @@ export class RangeVertex { this.bruteForce = false } - public get start() { + public get start(): SimpleCellAddress { return this.range.start } - public get end() { + public get end(): SimpleCellAddress { return this.range.end } - public get sheet() { + public get sheet(): number { return this.range.start.sheet } @@ -105,18 +105,4 @@ export class RangeVertex { this.dependentCacheRanges.forEach(range => range.criterionFunctionCache.clear()) this.dependentCacheRanges.clear() } - - /** - * Returns start of the range (it's top-left corner) - */ - public getStart(): SimpleCellAddress { - return this.start - } - - /** - * Returns end of the range (it's bottom-right corner) - */ - public getEnd(): SimpleCellAddress { - return this.end - } } diff --git a/src/DependencyGraph/SheetMapping.ts b/src/DependencyGraph/SheetMapping.ts index 703007116..836479a5c 100644 --- a/src/DependencyGraph/SheetMapping.ts +++ b/src/DependencyGraph/SheetMapping.ts @@ -7,126 +7,371 @@ import {NoSheetWithIdError, NoSheetWithNameError, SheetNameAlreadyTakenError} fr import {TranslationPackage, UIElement} from '../i18n' import {Maybe} from '../Maybe' -function canonicalize(sheetDisplayName: string): string { - return sheetDisplayName.toLowerCase() +/** + * Options for querying the sheet mapping. + */ +export interface SheetMappingQueryOptions { + includePlaceholders?: boolean, } +/** + * Representation of a sheet internal to SheetMapping. Not exported outside of this file. + */ class Sheet { constructor( public readonly id: number, public displayName: string, - ) { - } + public isPlaceholder: boolean = false, + ) {} - public get canonicalName() { - return canonicalize(this.displayName) + /** + * Returns the canonical (normalized) name of the sheet. + */ + public get canonicalName(): string { + return SheetMapping.canonicalizeSheetName(this.displayName) } } +/** + * Manages the sheets in the instance. + * - Can convert between sheet names and ids and vice versa. + * - Also stores placeholders for sheets that are used in formulas but not yet added. They are marked as isPlaceholder=true. + * - Sheetnames thet differ only in case are considered the same. (See: canonicalizeSheetName) + */ export class SheetMapping { - private readonly mappingFromCanonicalName: Map = new Map() - private readonly mappingFromId: Map = new Map() + /** + * Prefix for new sheet names if no name is provided by the user + */ private readonly sheetNamePrefix: string + /** + * Last used sheet ID. Used to generate new sheet IDs. + */ private lastSheetId = -1 + /** + * Mapping from canonical sheet name to sheet ID. + */ + private mappingFromCanonicalNameToId: Map = new Map() + /** + * Mapping from sheet ID to sheet. + */ + private allSheets: Map = new Map() - constructor(private languages: TranslationPackage) { + constructor(languages: TranslationPackage) { this.sheetNamePrefix = languages.getUITranslation(UIElement.NEW_SHEET_PREFIX) } + /** + * Converts sheet name to canonical/normalized form. + * @static + */ + public static canonicalizeSheetName(sheetDisplayName: string): string { + return sheetDisplayName.toLowerCase() + } + + /** + * Returns sheet ID for the given name. By default excludes placeholders. + */ + public getSheetId(sheetName: string, options: SheetMappingQueryOptions = {}): Maybe { + return this._getSheetByName(sheetName, options)?.id + } + + /** + * Returns sheet ID for the given name. Excludes placeholders. + * + * @throws {NoSheetWithNameError} if the sheet with the given name does not exist. + */ + public getSheetIdOrThrowError(sheetName: string): number { + const sheet = this._getSheetByName(sheetName, {}) + + if (sheet === undefined) { + throw new NoSheetWithNameError(sheetName) + } + return sheet.id + } + + /** + * Returns display name for the given sheet ID. Excludes placeholders. + * + * @returns {Maybe} the display name, or undefined if the sheet with the given ID does not exist. + */ + public getSheetName(sheetId: number): Maybe { + return this._getSheet(sheetId, {})?.displayName + } + + /** + * Returns display name for the given sheet ID. Excludes placeholders. + * + * @throws {NoSheetWithIdError} if the sheet with the given ID does not exist. + */ + public getSheetNameOrThrowError(sheetId: number, options: SheetMappingQueryOptions = {}): string { + return this._getSheetOrThrowError(sheetId, options).displayName + } + + /** + * Iterates over all sheet display names. By default excludes placeholders. + */ + public* iterateSheetNames(options: SheetMappingQueryOptions = {}): IterableIterator { + for (const sheet of this.allSheets.values()) { + if (options.includePlaceholders || !sheet.isPlaceholder) { + yield sheet.displayName + } + } + } + + /** + * Returns array of all sheet display names. By default excludes placeholders. + */ + public getSheetNames(options: SheetMappingQueryOptions = {}): string[] { + return Array.from(this.iterateSheetNames(options)) + } + + /** + * Returns total count of sheets. By default excludes placeholders. + */ + public numberOfSheets(options: SheetMappingQueryOptions = {}): number { + return this.getSheetNames(options).length + } + + /** + * Checks if sheet with given ID exists. By default excludes placeholders. + */ + public hasSheetWithId(sheetId: number, options: SheetMappingQueryOptions = {}): boolean { + return this._getSheet(sheetId, options) !== undefined + } + + /** + * Checks if sheet with given name exists (case-insensitive). Excludes placeholders. + */ + public hasSheetWithName(sheetName: string): boolean { + return this._getSheetByName(sheetName, {}) !== undefined + } + + /** + * Adds new sheet with optional name and returns its ID. + * If called with a name of an existing placeholder sheet, converts the placeholder sheet to a real sheet. + * + * @throws {SheetNameAlreadyTakenError} if the sheet with the given name already exists. + */ public addSheet(newSheetDisplayName: string = `${this.sheetNamePrefix}${this.lastSheetId + 2}`): number { - const newSheetCanonicalName = canonicalize(newSheetDisplayName) - if (this.mappingFromCanonicalName.has(newSheetCanonicalName)) { - throw new SheetNameAlreadyTakenError(newSheetDisplayName) + const sheetWithConflictingName = this._getSheetByName(newSheetDisplayName, { includePlaceholders: true }) + + if (sheetWithConflictingName) { + if (!sheetWithConflictingName.isPlaceholder) { + throw new SheetNameAlreadyTakenError(newSheetDisplayName) + } + + sheetWithConflictingName.isPlaceholder = false + return sheetWithConflictingName.id } this.lastSheetId++ const sheet = new Sheet(this.lastSheetId, newSheetDisplayName) - this.store(sheet) + this._storeSheetInMappings(sheet) return sheet.id } - public removeSheet(sheetId: number) { - const sheet = this.fetchSheetById(sheetId) - if (sheetId == this.lastSheetId) { - --this.lastSheetId + /** + * Adds a sheet with a specific ID and name. Used for redo operations. + * If called with a name of an existing placeholder sheet, converts the placeholder sheet to a real sheet. + * + * @throws {SheetNameAlreadyTakenError} if the sheet with the given name already exists. + */ + public addSheetWithId(sheetId: number, sheetDisplayName: string): void { + const sheetWithConflictingName = this._getSheetByName(sheetDisplayName, { includePlaceholders: true }) + + if (sheetWithConflictingName) { + if (sheetWithConflictingName.id !== sheetId) { + throw new SheetNameAlreadyTakenError(sheetDisplayName) + } + + if (!sheetWithConflictingName.isPlaceholder) { + throw new SheetNameAlreadyTakenError(sheetDisplayName) + } + + sheetWithConflictingName.isPlaceholder = false + return + } + + if (sheetId > this.lastSheetId) { + this.lastSheetId = sheetId } - this.mappingFromCanonicalName.delete(sheet.canonicalName) - this.mappingFromId.delete(sheet.id) + + const sheet = new Sheet(sheetId, sheetDisplayName) + this._storeSheetInMappings(sheet) } - public fetch = (sheetName: string): number => { - const sheet = this.mappingFromCanonicalName.get(canonicalize(sheetName)) - if (sheet === undefined) { - throw new NoSheetWithNameError(sheetName) + /** + * Adds a placeholder sheet with the given name if it does not exist yet + */ + public addPlaceholderIfNotExists(sheetName: string): number { + const sheetWithConflictingName = this._getSheetByName(sheetName, { includePlaceholders: true }) + + if (sheetWithConflictingName) { + return sheetWithConflictingName.id } + + this.lastSheetId++ + const sheet = new Sheet(this.lastSheetId, sheetName, true) + this._storeSheetInMappings(sheet) return sheet.id } - public get = (sheetName: string): Maybe => { - return this.mappingFromCanonicalName.get(canonicalize(sheetName))?.id - } + /** + * Adds a placeholder sheet with a specific ID and name. + * Used for undo operations to restore previously merged placeholder sheets. + * + * @throws {SheetNameAlreadyTakenError} if the sheet with the given name already exists. + */ + public addPlaceholderWithId(sheetId: number, sheetDisplayName: string): void { + const sheetWithConflictingName = this._getSheetByName(sheetDisplayName, { includePlaceholders: true }) - public fetchDisplayName = (sheetId: number): string => { - return this.fetchSheetById(sheetId).displayName - } + if (sheetWithConflictingName) { + throw new SheetNameAlreadyTakenError(sheetDisplayName) + } - public getDisplayName(sheetId: number): Maybe { - return this.mappingFromId.get(sheetId)?.displayName - } + if (this.hasSheetWithId(sheetId, { includePlaceholders: true })) { + throw new Error(`Sheet with id ${sheetId} already exists`) + } - public* displayNames(): IterableIterator { - for (const sheet of this.mappingFromCanonicalName.values()) { - yield sheet.displayName + if (sheetId > this.lastSheetId) { + this.lastSheetId = sheetId } + const sheet = new Sheet(sheetId, sheetDisplayName, true) + this._storeSheetInMappings(sheet) } - public numberOfSheets(): number { - return this.mappingFromCanonicalName.size - } + /** + * + * Removes sheet with given ID. + * If sheet does not exist, does nothing. + * @returns {boolean} true if sheet was removed, false if it did not exist. + */ + public removeSheetIfExists(sheetId: number, options: SheetMappingQueryOptions = {}): boolean { + const sheet = this._getSheet(sheetId, options) - public hasSheetWithId(sheetId: number): boolean { - return this.mappingFromId.has(sheetId) + if (!sheet) { + return false + } + + this.allSheets.delete(sheetId) + this.mappingFromCanonicalNameToId.delete(sheet.canonicalName) + + if (sheetId === this.lastSheetId) { + this.lastSheetId-- + } + + return true } - public hasSheetWithName(sheetName: string): boolean { - return this.mappingFromCanonicalName.has(canonicalize(sheetName)) + /** + * Marks sheet with given ID as a placeholder. + * @throws {NoSheetWithIdError} if the sheet with the given ID does not exist + */ + public markSheetAsPlaceholder(sheetId: number): void { + const sheet = this._getSheetOrThrowError(sheetId, {}) + sheet.isPlaceholder = true } - public renameSheet(sheetId: number, newDisplayName: string): Maybe { - const sheet = this.fetchSheetById(sheetId) + /** + * Renames sheet. + * - If called with sheetId of a placeholder sheet, throws {NoSheetWithIdError}. + * - If newDisplayName is conflicting with an existing sheet, throws {SheetNameAlreadyTakenError}. + * - If newDisplayName is conflicting with a placeholder sheet name, deletes the placeholder sheet and returns its id as mergedWithPlaceholderSheet. + * + * @throws {SheetNameAlreadyTakenError} if the sheet with the given name already exists. + * @throws {NoSheetWithIdError} if the sheet with the given ID does not exist. + */ + public renameSheet(sheetId: number, newDisplayName: string): { previousDisplayName: Maybe, mergedWithPlaceholderSheet?: number } { + const sheet = this._getSheetOrThrowError(sheetId, {}) const currentDisplayName = sheet.displayName if (currentDisplayName === newDisplayName) { - return undefined + return { previousDisplayName: undefined } } - const sheetWithThisCanonicalName = this.mappingFromCanonicalName.get(canonicalize(newDisplayName)) - if (sheetWithThisCanonicalName !== undefined && sheetWithThisCanonicalName.id !== sheet.id) { - throw new SheetNameAlreadyTakenError(newDisplayName) + const sheetWithConflictingName = this._getSheetByName(newDisplayName, { includePlaceholders: true }) + let mergedWithPlaceholderSheet: number | undefined = undefined + + if (sheetWithConflictingName !== undefined && sheetWithConflictingName.id !== sheet.id) { + if (!sheetWithConflictingName.isPlaceholder) { + throw new SheetNameAlreadyTakenError(newDisplayName) + } else { + this.mappingFromCanonicalNameToId.delete(sheetWithConflictingName.canonicalName) + this.allSheets.delete(sheetWithConflictingName.id) + + if (sheetWithConflictingName.id === this.lastSheetId) { + this.lastSheetId-- + } + + mergedWithPlaceholderSheet = sheetWithConflictingName.id + } } const currentCanonicalName = sheet.canonicalName - this.mappingFromCanonicalName.delete(currentCanonicalName) + this.mappingFromCanonicalNameToId.delete(currentCanonicalName) sheet.displayName = newDisplayName - this.store(sheet) - return currentDisplayName + this._storeSheetInMappings(sheet) + return { previousDisplayName: currentDisplayName, mergedWithPlaceholderSheet } + } + + /** + * Stores sheet in both internal mappings. + * - If ID exists, it is updated. If not, it is added. + * - If canonical name exists, it is updated. If not, it is added. + * + * @internal + */ + private _storeSheetInMappings(sheet: Sheet): void { + this.allSheets.set(sheet.id, sheet) + this.mappingFromCanonicalNameToId.set(sheet.canonicalName, sheet.id) } - public sheetNames(): string[] { - return Array.from(this.mappingFromId.values()).map((s) => s.displayName) + /** + * Returns sheet by ID + * + * @returns {Maybe} the sheet, or undefined if not found. + * @internal + */ + private _getSheet(sheetId: number, options: SheetMappingQueryOptions): Maybe { + const retrievedSheet = this.allSheets.get(sheetId) + + if (retrievedSheet === undefined) { + return undefined + } + + return (options.includePlaceholders || !retrievedSheet.isPlaceholder) ? retrievedSheet : undefined } - private store(sheet: Sheet): void { - this.mappingFromId.set(sheet.id, sheet) - this.mappingFromCanonicalName.set(sheet.canonicalName, sheet) + /** + * Returns sheet by name + * + * @returns {Maybe} the sheet, or undefined if not found. + * @internal + */ + private _getSheetByName(sheetName: string, options: SheetMappingQueryOptions): Maybe { + const sheetId = this.mappingFromCanonicalNameToId.get(SheetMapping.canonicalizeSheetName(sheetName)) + + if (sheetId === undefined) { + return undefined + } + + return this._getSheet(sheetId, options) } - private fetchSheetById(sheetId: number): Sheet { - const sheet = this.mappingFromId.get(sheetId) + /** + * Returns sheet by ID + * + * @throws {NoSheetWithIdError} if the sheet with the given ID does not exist. + * @internal + */ + private _getSheetOrThrowError(sheetId: number, options: SheetMappingQueryOptions): Sheet { + const sheet = this._getSheet(sheetId, options) + if (sheet === undefined) { throw new NoSheetWithIdError(sheetId) } + return sheet } } diff --git a/src/DependencyGraph/SheetReferenceRegistrar.ts b/src/DependencyGraph/SheetReferenceRegistrar.ts new file mode 100644 index 000000000..7c2cfa123 --- /dev/null +++ b/src/DependencyGraph/SheetReferenceRegistrar.ts @@ -0,0 +1,27 @@ +/** + * @license + * Copyright (c) 2025 Handsoncode. All rights reserved. + */ + +import {AddressMapping} from './AddressMapping/AddressMapping' +import {SheetMapping} from './SheetMapping' + +/** + * Converts sheet names to ids and adds placeholder sheets if they don't exist. + */ +export class SheetReferenceRegistrar { + constructor( + private readonly sheetMapping: SheetMapping, + private readonly addressMapping: AddressMapping, + ) {} + + /** + * Adds placeholder sheet if it doesn't exist and adds placeholder strategy to address mapping. + * @returns {number} sheet id + */ + public ensureSheetRegistered(sheetName: string): number { + const sheetId = this.sheetMapping.addPlaceholderIfNotExists(sheetName) + this.addressMapping.addSheetStrategyPlaceholderIfNotExists(sheetId) + return sheetId + } +} diff --git a/src/DependencyGraph/Vertex.ts b/src/DependencyGraph/Vertex.ts index 1c9baa6d2..4b8ae0d42 100644 --- a/src/DependencyGraph/Vertex.ts +++ b/src/DependencyGraph/Vertex.ts @@ -4,7 +4,7 @@ */ import {EmptyCellVertex, ParsingErrorVertex, RangeVertex, ValueCellVertex} from './' -import {FormulaVertex} from './FormulaCellVertex' +import {FormulaVertex} from './FormulaVertex' /** * Represents vertex which keeps values of one or more cells diff --git a/src/DependencyGraph/collectAddressesDependentToRange.ts b/src/DependencyGraph/collectAddressesDependentToRange.ts index d75d078cb..efa77d5dd 100644 --- a/src/DependencyGraph/collectAddressesDependentToRange.ts +++ b/src/DependencyGraph/collectAddressesDependentToRange.ts @@ -9,7 +9,7 @@ import {FunctionRegistry} from '../interpreter/FunctionRegistry' import {LazilyTransformingAstService} from '../LazilyTransformingAstService' import {AddressDependency, Ast, collectDependencies} from '../parser' import {DependencyGraph} from './DependencyGraph' -import {FormulaVertex} from './FormulaCellVertex' +import {FormulaVertex} from './FormulaVertex' import {RangeVertex} from './RangeVertex' import {Vertex} from './Vertex' diff --git a/src/DependencyGraph/index.ts b/src/DependencyGraph/index.ts index 05e4a2f86..9b5a1d003 100644 --- a/src/DependencyGraph/index.ts +++ b/src/DependencyGraph/index.ts @@ -9,13 +9,14 @@ export {Graph} from './Graph' export {TopSort} from './TopSort' export {RangeMapping} from './RangeMapping' export {SheetMapping} from './SheetMapping' +export {SheetReferenceRegistrar} from './SheetReferenceRegistrar' export {ArrayMapping} from './ArrayMapping' export {CellVertex, Vertex} from './Vertex' -export {FormulaCellVertex} from './FormulaCellVertex' +export {ScalarFormulaVertex} from './FormulaVertex' export {EmptyCellVertex} from './EmptyCellVertex' export {ValueCellVertex} from './ValueCellVertex' export {ParsingErrorVertex} from './ParsingErrorVertex' export {RangeVertex, CriterionCache} from './RangeVertex' export {SparseStrategy} from './AddressMapping/SparseStrategy' export {DenseStrategy} from './AddressMapping/DenseStrategy' -export {ArrayVertex} from './FormulaCellVertex' +export {ArrayFormulaVertex} from './FormulaVertex' diff --git a/src/Evaluator.ts b/src/Evaluator.ts index f47272b9b..f810bee36 100644 --- a/src/Evaluator.ts +++ b/src/Evaluator.ts @@ -8,8 +8,8 @@ import {absolutizeDependencies} from './absolutizeDependencies' import {CellError, ErrorType, SimpleCellAddress} from './Cell' import {Config} from './Config' import {ContentChanges} from './ContentChanges' -import {ArrayVertex, DependencyGraph, RangeVertex, Vertex} from './DependencyGraph' -import {FormulaVertex} from './DependencyGraph/FormulaCellVertex' +import {ArrayFormulaVertex, DependencyGraph, RangeVertex, Vertex} from './DependencyGraph' +import {FormulaVertex} from './DependencyGraph/FormulaVertex' import {Interpreter} from './interpreter/Interpreter' import {InterpreterState} from './interpreter/InterpreterState' import {EmptyValue, getRawValue, InterpreterValue} from './interpreter/InterpreterValue' @@ -46,35 +46,8 @@ export class Evaluator { this.stats.measure(StatType.EVALUATION, () => { this.dependencyGraph.graph.getTopSortedWithSccSubgraphFrom(vertices, - (vertex: Vertex) => { - if (vertex instanceof FormulaVertex) { - const currentValue = vertex.isComputed() ? vertex.getCellValue() : undefined - const newCellValue = this.recomputeFormulaVertexValue(vertex) - if (newCellValue !== currentValue) { - const address = vertex.getAddress(this.lazilyTransformingAstService) - changes.addChange(newCellValue, address) - this.columnSearch.change(getRawValue(currentValue), getRawValue(newCellValue), address) - return true - } - return false - } else if (vertex instanceof RangeVertex) { - vertex.clearCache() - return true - } else { - return true - } - }, - (vertex: Vertex) => { - if (vertex instanceof RangeVertex) { - vertex.clearCache() - } else if (vertex instanceof FormulaVertex) { - const address = vertex.getAddress(this.lazilyTransformingAstService) - this.columnSearch.remove(getRawValue(vertex.valueOrUndef()), address) - const error = new CellError(ErrorType.CYCLE, undefined, vertex) - vertex.setCellValue(error) - changes.addChange(error, address) - } - }, + (vertex: Vertex) => this.recomputeVertex(vertex, changes), + (vertex: Vertex) => this.processVertexOnCycle(vertex, changes), ) }) return changes @@ -87,7 +60,7 @@ export class Evaluator { const range = dep if (this.dependencyGraph.getRange(range.start, range.end) === undefined) { const rangeVertex = new RangeVertex(range) - this.dependencyGraph.rangeMapping.setRange(rangeVertex) + this.dependencyGraph.rangeMapping.addOrUpdateVertex(rangeVertex) tmpRanges.push(rangeVertex) } } @@ -95,12 +68,49 @@ export class Evaluator { const ret = this.evaluateAstToCellValue(ast, new InterpreterState(address, this.config.useArrayArithmetic)) tmpRanges.forEach((rangeVertex) => { - this.dependencyGraph.rangeMapping.removeRange(rangeVertex) + this.dependencyGraph.rangeMapping.removeVertexIfExists(rangeVertex) }) return ret } + /** + * Recalculates the value of a single vertex assuming its dependencies have already been recalculated + */ + private recomputeVertex(vertex: Vertex, changes: ContentChanges): boolean { + if (vertex instanceof FormulaVertex) { + const currentValue = vertex.isComputed() ? vertex.getCellValue() : undefined + const newCellValue = this.recomputeFormulaVertexValue(vertex) + if (newCellValue !== currentValue) { + const address = vertex.getAddress(this.lazilyTransformingAstService) + changes.addChange(newCellValue, address) + this.columnSearch.change(getRawValue(currentValue), getRawValue(newCellValue), address) + return true + } + return false + } else if (vertex instanceof RangeVertex) { + vertex.clearCache() + return true + } else { + return true + } + } + + /** + * Processes a vertex that is part of a cycle in dependency graph + */ + private processVertexOnCycle(vertex: Vertex, changes: ContentChanges): void { + if (vertex instanceof RangeVertex) { + vertex.clearCache() + } else if (vertex instanceof FormulaVertex) { + const address = vertex.getAddress(this.lazilyTransformingAstService) + this.columnSearch.remove(getRawValue(vertex.valueOrUndef()), address) + const error = new CellError(ErrorType.CYCLE, undefined, vertex) + vertex.setCellValue(error) + changes.addChange(error, address) + } + } + /** * Recalculates formulas in the topological sort order */ @@ -123,7 +133,7 @@ export class Evaluator { private recomputeFormulaVertexValue(vertex: FormulaVertex): InterpreterValue { const address = vertex.getAddress(this.lazilyTransformingAstService) - if (vertex instanceof ArrayVertex && (vertex.array.size.isRef || !this.dependencyGraph.isThereSpaceForArray(vertex))) { + if (vertex instanceof ArrayFormulaVertex && (vertex.array.size.isRef || !this.dependencyGraph.isThereSpaceForArray(vertex))) { return vertex.setNoSpace() } else { const formula = vertex.getFormula(this.lazilyTransformingAstService) diff --git a/src/Exporter.ts b/src/Exporter.ts index 0400e1689..c5ff24f73 100644 --- a/src/Exporter.ts +++ b/src/Exporter.ts @@ -12,7 +12,8 @@ import {EmptyValue, getRawValue, InterpreterValue, isExtendedNumber} from './int import {SimpleRangeValue} from './SimpleRangeValue' import {LazilyTransformingAstService} from './LazilyTransformingAstService' import {NamedExpressions} from './NamedExpressions' -import {SheetIndexMappingFn, simpleCellAddressToString} from './parser/addressRepresentationConverters' +import {simpleCellAddressToString} from './parser/addressRepresentationConverters' +import { SheetMapping } from './DependencyGraph/SheetMapping' export type ExportedChange = ExportedCellChange | ExportedNamedExpressionChange @@ -55,7 +56,7 @@ export class Exporter implements ChangeExporter { constructor( private readonly config: Config, private readonly namedExpressions: NamedExpressions, - private readonly sheetIndexMapping: SheetIndexMappingFn, + private readonly sheetMapping: SheetMapping, private readonly lazilyTransformingService: LazilyTransformingAstService, ) { } @@ -119,7 +120,7 @@ export class Exporter implements ChangeExporter { if (originAddress.sheet === NamedExpressions.SHEET_FOR_WORKBOOK_EXPRESSIONS) { address = this.namedExpressions.namedExpressionInAddress(originAddress.row)?.displayName } else { - address = simpleCellAddressToString(this.sheetIndexMapping, originAddress, -1) + address = simpleCellAddressToString(this.sheetMapping.getSheetNameOrThrowError.bind(this.sheetMapping), originAddress, -1) } } return new DetailedCellError(error, this.config.translationPackage.getErrorTranslation(error.type), address) diff --git a/src/GraphBuilder.ts b/src/GraphBuilder.ts index 695a6c321..6d54d0e54 100644 --- a/src/GraphBuilder.ts +++ b/src/GraphBuilder.ts @@ -9,9 +9,9 @@ import {SimpleCellAddress, simpleCellAddress} from './Cell' import {CellContent, CellContentParser} from './CellContentParser' import {CellDependency} from './CellDependency' import { - ArrayVertex, + ArrayFormulaVertex, DependencyGraph, - FormulaCellVertex, + ScalarFormulaVertex, ParsingErrorVertex, ValueCellVertex, Vertex @@ -99,7 +99,7 @@ export class SimpleStrategy implements GraphBuilderStrategy { this.shrinkArrayIfNeeded(address) const size = this.arraySizePredictor.checkArraySize(parseResult.ast, address) if (size.isScalar()) { - const vertex = new FormulaCellVertex(parseResult.ast, address, 0) + const vertex = new ScalarFormulaVertex(parseResult.ast, address, 0) dependencies.set(vertex, absolutizeDependencies(parseResult.dependencies, address)) this.dependencyGraph.addVertex(address, vertex) if (parseResult.hasVolatileFunction) { @@ -109,7 +109,7 @@ export class SimpleStrategy implements GraphBuilderStrategy { this.dependencyGraph.markAsDependentOnStructureChange(vertex) } } else { - const vertex = new ArrayVertex(parseResult.ast, address, new ArraySize(size.width, size.height)) + const vertex = new ArrayFormulaVertex(parseResult.ast, address, new ArraySize(size.width, size.height)) dependencies.set(vertex, absolutizeDependencies(parseResult.dependencies, address)) this.dependencyGraph.addArrayVertex(address, vertex) } @@ -131,7 +131,7 @@ export class SimpleStrategy implements GraphBuilderStrategy { private shrinkArrayIfNeeded(address: SimpleCellAddress) { const vertex = this.dependencyGraph.getCell(address) - if (vertex instanceof ArrayVertex) { + if (vertex instanceof ArrayFormulaVertex) { this.dependencyGraph.shrinkArrayToCorner(vertex) } } diff --git a/src/HyperFormula.ts b/src/HyperFormula.ts index 8de0f4a6d..e7c4b5150 100644 --- a/src/HyperFormula.ts +++ b/src/HyperFormula.ts @@ -649,6 +649,9 @@ export class HyperFormula implements TypedEmitter { return FunctionRegistry.getPlugins() } + /** + * @internal + */ private static buildFromEngineState(engine: EngineState): HyperFormula { return new HyperFormula( engine.config, @@ -2603,6 +2606,8 @@ export class HyperFormula implements TypedEmitter { /** * Adds a new sheet to the HyperFormula instance. Returns given or autogenerated name of a new sheet. * + * Note that this method may trigger dependency graph recalculation. + * * @param {string} [sheetName] - if not specified, name is autogenerated * * @fires [[sheetAdded]] after the sheet was added @@ -2632,6 +2637,7 @@ export class HyperFormula implements TypedEmitter { validateArgToType(sheetName, 'string', 'sheetName') } const addedSheetName = this._crudOperations.addSheet(sheetName) + this.recomputeIfDependencyGraphNeedsIt() this._emitter.emit(Events.SheetAdded, addedSheetName) return addedSheetName } @@ -2703,7 +2709,7 @@ export class HyperFormula implements TypedEmitter { */ public removeSheet(sheetId: number): ExportedChange[] { validateArgToType(sheetId, 'number', 'sheetId') - const displayName = this.sheetMapping.getDisplayName(sheetId) as string + const displayName = this.sheetMapping.getSheetName(sheetId) as string this._crudOperations.removeSheet(sheetId) const changes = this.recomputeIfDependencyGraphNeedsIt() this._emitter.emit(Events.SheetRemoved, displayName, changes) @@ -2882,7 +2888,7 @@ export class HyperFormula implements TypedEmitter { public simpleCellAddressFromString(cellAddress: string, contextSheetId: number): SimpleCellAddress | undefined { validateArgToType(cellAddress, 'string', 'cellAddress') validateArgToType(contextSheetId, 'number', 'sheetId') - return simpleCellAddressFromString(this.sheetMapping.get, cellAddress, contextSheetId) + return simpleCellAddressFromString(this.sheetMapping.getSheetId.bind(this.sheetMapping), cellAddress, contextSheetId) } /** @@ -2911,7 +2917,7 @@ export class HyperFormula implements TypedEmitter { public simpleCellRangeFromString(cellRange: string, contextSheetId: number): SimpleCellRange | undefined { validateArgToType(cellRange, 'string', 'cellRange') validateArgToType(contextSheetId, 'number', 'sheetId') - return simpleCellRangeFromString(this.sheetMapping.get, cellRange, contextSheetId) + return simpleCellRangeFromString(this.sheetMapping.getSheetId.bind(this.sheetMapping), cellRange, contextSheetId) } /** @@ -2957,7 +2963,7 @@ export class HyperFormula implements TypedEmitter { ? optionsOrContextSheetId : optionsOrContextSheetId.includeSheetName ? cellAddress.sheet+1 : cellAddress.sheet - return simpleCellAddressToString(this.sheetMapping.fetchDisplayName, cellAddress, contextSheetId) + return simpleCellAddressToString(this.sheetMapping.getSheetNameOrThrowError.bind(this.sheetMapping), cellAddress, contextSheetId) } /** @@ -3010,7 +3016,7 @@ export class HyperFormula implements TypedEmitter { ? optionsOrContextSheetId : optionsOrContextSheetId.includeSheetName ? cellRange.start.sheet+cellRange.end.sheet+1 : cellRange.start.sheet - return simpleCellRangeToString(this.sheetMapping.fetchDisplayName, cellRange, contextSheetId) + return simpleCellRangeToString(this.sheetMapping.getSheetNameOrThrowError.bind(this.sheetMapping), cellRange, contextSheetId) } /** @@ -3043,7 +3049,7 @@ export class HyperFormula implements TypedEmitter { if (isSimpleCellAddress(address)) { vertex = this._dependencyGraph.addressMapping.getCell(address) } else if (isSimpleCellRange(address)) { - vertex = this._dependencyGraph.rangeMapping.getRange(address.start, address.end) + vertex = this._dependencyGraph.rangeMapping.getRangeVertex(address.start, address.end) } else { throw new ExpectedValueOfTypeError('SimpleCellAddress | SimpleCellRange', address) } @@ -3081,7 +3087,7 @@ export class HyperFormula implements TypedEmitter { if (isSimpleCellAddress(address)) { vertex = this._dependencyGraph.addressMapping.getCell(address) } else if (isSimpleCellRange(address)) { - vertex = this._dependencyGraph.rangeMapping.getRange(address.start, address.end) + vertex = this._dependencyGraph.rangeMapping.getRangeVertex(address.start, address.end) } else { throw new ExpectedValueOfTypeError('SimpleCellAddress | SimpleCellRange', address) } @@ -3113,7 +3119,7 @@ export class HyperFormula implements TypedEmitter { */ public getSheetName(sheetId: number): string | undefined { validateArgToType(sheetId, 'number', 'sheetId') - return this.sheetMapping.getDisplayName(sheetId) + return this.sheetMapping.getSheetName(sheetId) } /** @@ -3134,7 +3140,7 @@ export class HyperFormula implements TypedEmitter { * @category Sheets */ public getSheetNames(): string[] { - return this.sheetMapping.sheetNames() + return this.sheetMapping.getSheetNames() } /** @@ -3159,7 +3165,7 @@ export class HyperFormula implements TypedEmitter { */ public getSheetId(sheetName: string): number | undefined { validateArgToType(sheetName, 'string', 'sheetName') - return this.sheetMapping.get(sheetName) + return this.sheetMapping.getSheetId(sheetName) } /** @@ -3504,6 +3510,8 @@ export class HyperFormula implements TypedEmitter { /** * Renames a specified sheet. * + * Note that this method may trigger dependency graph recalculation. + * * @param {number} sheetId - a sheet ID * @param {string} newName - a name of the sheet to be given, if is the same as the old one the method does nothing * @@ -3530,6 +3538,7 @@ export class HyperFormula implements TypedEmitter { validateArgToType(sheetId, 'number', 'sheetId') validateArgToType(newName, 'string', 'newName') const oldName = this._crudOperations.renameSheet(sheetId, newName) + this.recomputeIfDependencyGraphNeedsIt() if (oldName !== undefined) { this._emitter.emit(Events.SheetRenamed, oldName, newName) } @@ -4527,12 +4536,22 @@ export class HyperFormula implements TypedEmitter { objectDestroy(this) } + /** + * Throws an error if evaluation is suspended. + * + * @internal + */ private ensureEvaluationIsNotSuspended() { if (this._evaluationSuspended) { throw new EvaluationSuspendedError() } } + /** + * Parses a formula string and extracts its AST and dependencies. + * + * @internal + */ private extractTemporaryFormula(formulaString: string, sheetId: number = 1): { ast?: Ast, address: SimpleCellAddress, dependencies: RelativeDependency[] } { const parsedCellContent = this._cellContentParser.parse(formulaString) const address = {sheet: sheetId, col: 0, row: 0} diff --git a/src/Operations.ts b/src/Operations.ts index bd6b51cf9..d3b4d8e5b 100644 --- a/src/Operations.ts +++ b/src/Operations.ts @@ -6,7 +6,7 @@ import { AbsoluteCellRange } from './AbsoluteCellRange' import { absolutizeDependencies, filterDependenciesOutOfScope } from './absolutizeDependencies' import { ArraySize, ArraySizePredictor } from './ArraySize' -import { equalSimpleCellAddress, invalidSimpleCellAddress, simpleCellAddress, SimpleCellAddress } from './Cell' +import { equalSimpleCellAddress, isColOrRowInvalid, simpleCellAddress, SimpleCellAddress } from './Cell' import { CellContent, CellContentParser, RawCellContent } from './CellContentParser' import { ClipboardCell, ClipboardCellType } from './ClipboardOperations' import { Config } from './Config' @@ -14,25 +14,25 @@ import { ContentChanges } from './ContentChanges' import { ColumnRowIndex } from './CrudOperations' import { AddressMapping, - ArrayVertex, + ArrayFormulaVertex, CellVertex, DependencyGraph, EmptyCellVertex, - FormulaCellVertex, + ScalarFormulaVertex, ParsingErrorVertex, SheetMapping, SparseStrategy, - ValueCellVertex + ValueCellVertex, } from './DependencyGraph' -import { FormulaVertex } from './DependencyGraph/FormulaCellVertex' -import { RawAndParsedValue } from './DependencyGraph/ValueCellVertex' +import { FormulaVertex } from './DependencyGraph/FormulaVertex' +import { RawAndParsedValue, ValueCellVertexValue } from './DependencyGraph/ValueCellVertex' import { AddColumnsTransformer } from './dependencyTransformers/AddColumnsTransformer' import { AddRowsTransformer } from './dependencyTransformers/AddRowsTransformer' import { CleanOutOfScopeDependenciesTransformer } from './dependencyTransformers/CleanOutOfScopeDependenciesTransformer' import { MoveCellsTransformer } from './dependencyTransformers/MoveCellsTransformer' import { RemoveColumnsTransformer } from './dependencyTransformers/RemoveColumnsTransformer' import { RemoveRowsTransformer } from './dependencyTransformers/RemoveRowsTransformer' -import { RemoveSheetTransformer } from './dependencyTransformers/RemoveSheetTransformer' +import { RenameSheetTransformer } from './dependencyTransformers/RenameSheetTransformer' import { InvalidArgumentsError, NamedExpressionDoesNotExistError, @@ -44,6 +44,7 @@ import { import { EmptyValue, getRawValue } from './interpreter/InterpreterValue' import { LazilyTransformingAstService } from './LazilyTransformingAstService' import { ColumnSearchStrategy } from './Lookup/SearchStrategy' +import { Maybe } from './Maybe' import { doesContainRelativeReferences, InternalNamedExpression, @@ -53,7 +54,6 @@ import { import { NamedExpressionDependency, ParserWithCaching, ParsingErrorType, RelativeDependency } from './parser' import { ParsingError } from './parser/Ast' import { ParsingResult } from './parser/ParserWithCaching' -import { findBoundaries, Sheet } from './Sheet' import { ColumnsSpan, RowsSpan } from './Span' import { Statistics, StatType } from './statistics' @@ -217,43 +217,89 @@ export class Operations { return columnsRemovals } - public removeSheet(sheetId: number) { - this.dependencyGraph.removeSheet(sheetId) + /** + * Clears the sheet content. + */ + public clearSheet(sheetId: number) { + this.dependencyGraph.clearSheet(sheetId) + this.columnSearch.removeSheet(sheetId) + } - let version = 0 - this.stats.measure(StatType.TRANSFORM_ASTS, () => { - const transformation = new RemoveSheetTransformer(sheetId) - transformation.performEagerTransformations(this.dependencyGraph, this.parser) - version = this.lazilyTransformingAstService.addTransformation(transformation) - }) + /** + * Adds a new sheet to the workbook. + */ + public addSheet(name?: string): { sheetName: string, sheetId: number } { + const sheetId = this.sheetMapping.addSheet(name) + this.dependencyGraph.addSheet(sheetId) + return { sheetName: this.sheetMapping.getSheetNameOrThrowError(sheetId), sheetId } + } + + /** + * Adds a sheet with a specific ID for redo operations. + */ + public addSheetWithId(sheetId: number, name: string): void { + this.sheetMapping.addSheetWithId(sheetId, name) + this.dependencyGraph.addSheet(sheetId) + } + + /** + * Adds a placeholder sheet with a specific ID for undo operations. + * Used to restore previously merged placeholder sheets. + * + * Note: Unlike `addSheetWithId`, this does NOT call `dependencyGraph.addSheet()` + * because placeholders don't need dirty marking or strategy changes - they only + * need to exist in the mappings so formulas can reference them again. + */ + public addPlaceholderSheetWithId(sheetId: number, name: string): void { + this.sheetMapping.addPlaceholderWithId(sheetId, name) + this.addressMapping.addSheetStrategyPlaceholderIfNotExists(sheetId) + } - this.sheetMapping.removeSheet(sheetId) + /** + * Removes a sheet from the workbook. + */ + public removeSheet(sheetId: number): [InternalNamedExpression, ClipboardCell][] { + this.dependencyGraph.removeSheet(sheetId) this.columnSearch.removeSheet(sheetId) const scopedNamedExpressions = this.namedExpressions.getAllNamedExpressionsForScope(sheetId).map( (namedExpression) => this.removeNamedExpression(namedExpression.normalizeExpressionName(), sheetId) ) - return { version: version, scopedNamedExpressions } + return scopedNamedExpressions } + /** + * Removes a sheet from the workbook by name. + */ public removeSheetByName(sheetName: string) { - const sheetId = this.sheetMapping.fetch(sheetName) + const sheetId = this.sheetMapping.getSheetIdOrThrowError(sheetName) return this.removeSheet(sheetId) } - public clearSheet(sheetId: number) { - this.dependencyGraph.clearSheet(sheetId) - this.columnSearch.removeSheet(sheetId) - } - - public addSheet(name?: string) { - const sheetId = this.sheetMapping.addSheet(name) - const sheet: Sheet = [] - this.dependencyGraph.addressMapping.autoAddSheet(sheetId, findBoundaries(sheet)) - return this.sheetMapping.fetchDisplayName(sheetId) - } + /** + * Renames a sheet in the workbook. + */ + public renameSheet(sheetId: number, newName: string): { + previousDisplayName: Maybe, + version?: number, + mergedPlaceholderSheetId?: number, + } { + const { previousDisplayName, mergedWithPlaceholderSheet } = this.sheetMapping.renameSheet(sheetId, newName) + + let version: number | undefined + if (mergedWithPlaceholderSheet !== undefined) { + this.dependencyGraph.mergeSheets(sheetId, mergedWithPlaceholderSheet) + this.stats.measure(StatType.TRANSFORM_ASTS, () => { + const transformation = new RenameSheetTransformer(sheetId, mergedWithPlaceholderSheet) + transformation.performEagerTransformations(this.dependencyGraph, this.parser) + version = this.lazilyTransformingAstService.addTransformation(transformation) + }) + } - public renameSheet(sheetId: number, newName: string) { - return this.sheetMapping.renameSheet(sheetId, newName) + return { + previousDisplayName, + version, + mergedPlaceholderSheetId: mergedWithPlaceholderSheet, + } } public moveRows(sheet: number, startRow: number, numberOfRows: number, targetRow: number): number { @@ -419,9 +465,9 @@ export class Operations { public ensureItIsPossibleToMoveCells(sourceLeftCorner: SimpleCellAddress, width: number, height: number, destinationLeftCorner: SimpleCellAddress): void { if ( - invalidSimpleCellAddress(sourceLeftCorner) || + isColOrRowInvalid(sourceLeftCorner) || !((isPositiveInteger(width) && isPositiveInteger(height)) || isRowOrColumnRange(sourceLeftCorner, width, height)) || - invalidSimpleCellAddress(destinationLeftCorner) || + isColOrRowInvalid(destinationLeftCorner) || !this.sheetMapping.hasSheetWithId(sourceLeftCorner.sheet) || !this.sheetMapping.hasSheetWithId(destinationLeftCorner.sheet) ) { @@ -459,8 +505,6 @@ export class Operations { /** * Restores a single cell. - * @param {SimpleCellAddress} address - * @param {ClipboardCell} clipboardCell */ public restoreCell(address: SimpleCellAddress, clipboardCell: ClipboardCell): void { switch (clipboardCell.type) { @@ -509,13 +553,13 @@ export class Operations { return { type: ClipboardCellType.EMPTY } } else if (vertex instanceof ValueCellVertex) { return { type: ClipboardCellType.VALUE, ...vertex.getValues() } - } else if (vertex instanceof ArrayVertex) { + } else if (vertex instanceof ArrayFormulaVertex) { const val = vertex.getArrayCellValue(address) if (val === EmptyValue) { return { type: ClipboardCellType.EMPTY } } return { type: ClipboardCellType.VALUE, parsedValue: val, rawValue: vertex.getArrayCellRawValue(address) } - } else if (vertex instanceof FormulaCellVertex) { + } else if (vertex instanceof ScalarFormulaVertex) { return { type: ClipboardCellType.FORMULA, hash: this.parser.computeHashFromAst(vertex.getFormula(this.lazilyTransformingAstService)) @@ -570,7 +614,6 @@ export class Operations { this.setFormulaToCell(address, size, parserResult) } catch (error) { - if (!(error as Error).message) { throw error } @@ -598,45 +641,66 @@ export class Operations { } } + /** + * Sets cell content to an instance of parsing error. + * Creates a ParsingErrorVertex and updates the dependency graph and column search index. + */ public setParsingErrorToCell(rawInput: string, errors: ParsingError[], address: SimpleCellAddress) { - const oldValue = this.dependencyGraph.getCellValue(address) + this.removeCellValueFromColumnSearch(address) + const vertex = new ParsingErrorVertex(errors, rawInput) const arrayChanges = this.dependencyGraph.setParsingErrorToCell(address, vertex) - this.columnSearch.remove(getRawValue(oldValue), address) + this.columnSearch.applyChanges(arrayChanges.getChanges()) this.changes.addAll(arrayChanges) this.changes.addChange(vertex.getCellValue(), address) } + /** + * Sets cell content to a formula. + * Creates a ScalarFormulaVertex and updates the dependency graph and column search index. + */ public setFormulaToCell(address: SimpleCellAddress, size: ArraySize, { ast, hasVolatileFunction, hasStructuralChangeFunction, dependencies }: ParsingResult) { - const oldValue = this.dependencyGraph.getCellValue(address) + this.removeCellValueFromColumnSearch(address) + const arrayChanges = this.dependencyGraph.setFormulaToCell(address, ast, absolutizeDependencies(dependencies, address), size, hasVolatileFunction, hasStructuralChangeFunction) - this.columnSearch.remove(getRawValue(oldValue), address) + this.columnSearch.applyChanges(arrayChanges.getChanges()) this.changes.addAll(arrayChanges) } + /** + * Sets cell content to a value. + * Creates a ValueCellVertex and updates the dependency graph and column search index. + */ public setValueToCell(value: RawAndParsedValue, address: SimpleCellAddress) { - const oldValue = this.dependencyGraph.getCellValue(address) + this.changeCellValueInColumnSearch(address, value.parsedValue) + const arrayChanges = this.dependencyGraph.setValueToCell(address, value) - this.columnSearch.change(getRawValue(oldValue), getRawValue(value.parsedValue), address) + this.columnSearch.applyChanges(arrayChanges.getChanges().filter(change => !equalSimpleCellAddress(change.address, address))) this.changes.addAll(arrayChanges) this.changes.addChange(value.parsedValue, address) } + /** + * Sets cell content to an empty value. + * Creates an EmptyCellVertex and updates the dependency graph and column search index. + */ public setCellEmpty(address: SimpleCellAddress) { if (this.dependencyGraph.isArrayInternalCell(address)) { return } - const oldValue = this.dependencyGraph.getCellValue(address) + + this.removeCellValueFromColumnSearch(address) + const arrayChanges = this.dependencyGraph.setCellEmpty(address) - this.columnSearch.remove(getRawValue(oldValue), address) + this.columnSearch.applyChanges(arrayChanges.getChanges()) this.changes.addAll(arrayChanges) this.changes.addChange(EmptyValue, address) @@ -663,7 +727,7 @@ export class Operations { * @param {number} sheet - sheet ID number */ public rowEffectivelyNotInSheet(row: number, sheet: number): boolean { - const height = this.dependencyGraph.addressMapping.getHeight(sheet) + const height = this.dependencyGraph.addressMapping.getSheetHeight(sheet) return row >= height } @@ -765,7 +829,7 @@ export class Operations { this.rewriteAffectedArrays(affectedArrays) } - private rewriteAffectedArrays(affectedArrays: Set) { + private rewriteAffectedArrays(affectedArrays: Set) { for (const arrayVertex of affectedArrays.values()) { if (arrayVertex.array.size.isRef) { continue @@ -806,7 +870,7 @@ export class Operations { * @param {number} sheet - sheet ID number */ private columnEffectivelyNotInSheet(column: number, sheet: number): boolean { - const width = this.dependencyGraph.addressMapping.getWidth(sheet) + const width = this.dependencyGraph.addressMapping.getSheetWidth(sheet) return column >= width } @@ -822,7 +886,7 @@ export class Operations { const globalVertexId = maybeGlobalVertexId ?? this.dependencyGraph.graph.getNodeId(globalVertex) for (const adjacentNode of this.dependencyGraph.graph.adjacentNodes(globalVertex)) { - if (adjacentNode instanceof FormulaCellVertex && adjacentNode.getAddress(this.lazilyTransformingAstService).sheet === sheetId) { + if (adjacentNode instanceof ScalarFormulaVertex && adjacentNode.getAddress(this.lazilyTransformingAstService).sheet === sheetId) { const ast = adjacentNode.getFormula(this.lazilyTransformingAstService) const formulaAddress = adjacentNode.getAddress(this.lazilyTransformingAstService) const { dependencies } = this.parser.fetchCachedResultForAst(ast) @@ -861,8 +925,8 @@ export class Operations { const targetRange = AbsoluteCellRange.spanFrom(destinationLeftCorner, width, height) for (const formulaAddress of targetRange.addresses(this.dependencyGraph)) { - const vertex = this.addressMapping.fetchCell(formulaAddress) - if (vertex instanceof FormulaCellVertex && formulaAddress.sheet !== sourceLeftCorner.sheet) { + const vertex = this.addressMapping.getCell(formulaAddress, { throwIfCellNotExists: true }) + if (vertex instanceof ScalarFormulaVertex && formulaAddress.sheet !== sourceLeftCorner.sheet) { const ast = vertex.getFormula(this.lazilyTransformingAstService) const { dependencies } = this.parser.fetchCachedResultForAst(ast) addedGlobalNamedExpressions.push(...this.updateNamedExpressionsForTargetAddress(sourceLeftCorner.sheet, formulaAddress, dependencies)) @@ -878,7 +942,7 @@ export class Operations { } const addedGlobalNamedExpressions: string[] = [] - const vertex = this.addressMapping.fetchCell(targetAddress) + const vertex = this.addressMapping.getCellOrThrow(targetAddress) for (const namedExpressionDependency of absolutizeDependencies(dependencies, targetAddress)) { if (!(namedExpressionDependency instanceof NamedExpressionDependency)) { @@ -903,7 +967,7 @@ export class Operations { } private allocateNamedExpressionAddressSpace() { - this.dependencyGraph.addressMapping.addSheet(NamedExpressions.SHEET_FOR_WORKBOOK_EXPRESSIONS, new SparseStrategy(0, 0)) + this.dependencyGraph.addressMapping.addSheetWithStrategy(NamedExpressions.SHEET_FOR_WORKBOOK_EXPRESSIONS, new SparseStrategy(0, 0)) } private copyOrFetchGlobalNamedExpressionVertex(expressionName: string, sourceVertex: CellVertex, addedNamedExpressions: string[]): CellVertex { @@ -911,7 +975,7 @@ export class Operations { if (expression === undefined) { expression = this.namedExpressions.addNamedExpression(expressionName) addedNamedExpressions.push(expression.normalizeExpressionName()) - if (sourceVertex instanceof FormulaCellVertex) { + if (sourceVertex instanceof ScalarFormulaVertex) { const parsingResult = this.parser.fetchCachedResultForAst(sourceVertex.getFormula(this.lazilyTransformingAstService)) const { ast, hasVolatileFunction, hasStructuralChangeFunction, dependencies } = parsingResult this.dependencyGraph.setFormulaToCell(expression.address, ast, absolutizeDependencies(dependencies, expression.address), ArraySize.scalar(), hasVolatileFunction, hasStructuralChangeFunction) @@ -923,6 +987,45 @@ export class Operations { } return this.dependencyGraph.fetchCellOrCreateEmpty(expression.address).vertex } + + /** + * Removes a cell value from the columnSearch index. + * Ignores the non-computed formula vertices. + */ + private removeCellValueFromColumnSearch(address: SimpleCellAddress): void { + if (this.isNotComputed(address)) { + return + } + + const oldValue = this.dependencyGraph.getCellValue(address) + this.columnSearch.remove(getRawValue(oldValue), address) + } + + /** + * Changes a cell value in the columnSearch index. + * Ignores the non-computed formula vertices. + */ + private changeCellValueInColumnSearch(address: SimpleCellAddress, newValue: ValueCellVertexValue): void { + if (this.isNotComputed(address)) { + return + } + + const oldValue = this.dependencyGraph.getCellValue(address) + this.columnSearch.change(getRawValue(oldValue), getRawValue(newValue), address) + } + + /** + * Checks if the ScalarFormulaVertex or ArrayFormulaVertex at the given address is not computed. + */ + private isNotComputed(address: SimpleCellAddress): boolean { + const vertex = this.dependencyGraph.getCell(address) + + if (!vertex) { + return false + } + + return 'isComputed' in vertex && !vertex.isComputed() + } } export function normalizeRemovedIndexes(indexes: ColumnRowIndex[]): ColumnRowIndex[] { diff --git a/src/Serialization.ts b/src/Serialization.ts index 71a256fda..e33474ec5 100644 --- a/src/Serialization.ts +++ b/src/Serialization.ts @@ -7,11 +7,11 @@ import {simpleCellAddress, SimpleCellAddress} from './Cell' import {RawCellContent} from './CellContentParser' import {CellValue} from './CellValue' import {Config} from './Config' -import {ArrayVertex, DependencyGraph, FormulaCellVertex, ParsingErrorVertex} from './DependencyGraph' +import {ArrayFormulaVertex, DependencyGraph, ScalarFormulaVertex, ParsingErrorVertex} from './DependencyGraph' import {Exporter} from './Exporter' import {Maybe} from './Maybe' import {NamedExpressionOptions, NamedExpressions} from './NamedExpressions' -import {buildLexerConfig, ProcedureAst, Unparser} from './parser' +import {ProcedureAst, Unparser} from './parser' export interface SerializedNamedExpression { name: string, @@ -30,7 +30,7 @@ export class Serialization { public getCellHyperlink(address: SimpleCellAddress): Maybe { const formulaVertex = this.dependencyGraph.getCell(address) - if (formulaVertex instanceof FormulaCellVertex) { + if (formulaVertex instanceof ScalarFormulaVertex) { const formula = formulaVertex.getFormula(this.dependencyGraph.lazilyTransformingAstService) as ProcedureAst if ('HYPERLINK' === formula.procedureName) { return formula.hyperlink @@ -41,11 +41,11 @@ export class Serialization { public getCellFormula(address: SimpleCellAddress, targetAddress?: SimpleCellAddress): Maybe { const formulaVertex = this.dependencyGraph.getCell(address) - if (formulaVertex instanceof FormulaCellVertex) { + if (formulaVertex instanceof ScalarFormulaVertex) { const formula = formulaVertex.getFormula(this.dependencyGraph.lazilyTransformingAstService) targetAddress = targetAddress ?? address return this.unparser.unparse(formula, targetAddress) - } else if (formulaVertex instanceof ArrayVertex) { + } else if (formulaVertex instanceof ArrayFormulaVertex) { const arrayVertexAddress = formulaVertex.getAddress(this.dependencyGraph.lazilyTransformingAstService) if (arrayVertexAddress.row !== address.row || arrayVertexAddress.col !== address.col || arrayVertexAddress.sheet !== address.sheet) { return undefined @@ -114,8 +114,8 @@ export class Serialization { public genericAllSheetsGetter(sheetGetter: (sheet: number) => T): Record { const result: Record = {} - for (const sheetName of this.dependencyGraph.sheetMapping.displayNames()) { - const sheetId = this.dependencyGraph.sheetMapping.fetch(sheetName) + for (const sheetName of this.dependencyGraph.sheetMapping.iterateSheetNames()) { + const sheetId = this.dependencyGraph.sheetMapping.getSheetIdOrThrowError(sheetName) result[sheetName] = sheetGetter(sheetId) } return result @@ -140,8 +140,8 @@ export class Serialization { public getAllNamedExpressionsSerialized(): SerializedNamedExpression[] { const idMap: number[] = [] let id = 0 - for (const sheetName of this.dependencyGraph.sheetMapping.displayNames()) { - const sheetId = this.dependencyGraph.sheetMapping.fetch(sheetName) + for (const sheetName of this.dependencyGraph.sheetMapping.iterateSheetNames()) { + const sheetId = this.dependencyGraph.sheetMapping.getSheetIdOrThrowError(sheetName) idMap[sheetId] = id id++ } @@ -156,7 +156,7 @@ export class Serialization { } public withNewConfig(newConfig: Config, namedExpressions: NamedExpressions): Serialization { - const newUnparser = new Unparser(newConfig, buildLexerConfig(newConfig), this.dependencyGraph.sheetMapping.fetchDisplayName, namedExpressions) + const newUnparser = new Unparser(newConfig, this.dependencyGraph.sheetMapping, namedExpressions) return new Serialization(this.dependencyGraph, newUnparser, this.exporter) } } diff --git a/src/UndoRedo.ts b/src/UndoRedo.ts index cea071117..5413a79d5 100644 --- a/src/UndoRedo.ts +++ b/src/UndoRedo.ts @@ -225,6 +225,7 @@ export class RemoveColumnsUndoEntry extends BaseUndoEntry { export class AddSheetUndoEntry extends BaseUndoEntry { constructor( public readonly sheetName: string, + public readonly sheetId: number, ) { super() } @@ -244,7 +245,6 @@ export class RemoveSheetUndoEntry extends BaseUndoEntry { public readonly sheetId: number, public readonly oldSheetContent: ClipboardCell[][], public readonly scopedNamedExpressions: [InternalNamedExpression, ClipboardCell][], - public readonly version: number, ) { super() } @@ -258,11 +258,23 @@ export class RemoveSheetUndoEntry extends BaseUndoEntry { } } +/** + * Undo entry for renaming a sheet. + * + * When renaming a sheet to a name that was previously referenced (but didn't exist), + * a placeholder sheet gets merged into the renamed sheet. In this case: + * - `version` contains the transformation version for restoring formulas during undo + * - `mergedPlaceholderSheetId` contains the ID of the placeholder sheet that was merged + * + * When renaming to a name not previously referenced, both optional params are undefined. + */ export class RenameSheetUndoEntry extends BaseUndoEntry { constructor( public readonly sheetId: number, public readonly oldName: string, public readonly newName: string, + public readonly version?: number, + public readonly mergedPlaceholderSheetId?: number, ) { super() } @@ -583,8 +595,8 @@ export class UndoRedo { public undoRemoveSheet(operation: RemoveSheetUndoEntry) { this.operations.forceApplyPostponedTransformations() - const {oldSheetContent, sheetId} = operation - this.operations.addSheet(operation.sheetName) + const {oldSheetContent, sheetId, scopedNamedExpressions, sheetName} = operation + this.operations.addSheetWithId(sheetId, sheetName) for (let rowIndex = 0; rowIndex < oldSheetContent.length; rowIndex++) { const row = oldSheetContent[rowIndex] for (let col = 0; col < row.length; col++) { @@ -594,15 +606,19 @@ export class UndoRedo { } } - for (const [namedexpression, content] of operation.scopedNamedExpressions) { + for (const [namedexpression, content] of scopedNamedExpressions) { this.operations.restoreNamedExpression(namedexpression, content, sheetId) } - - this.restoreOldDataFromVersion(operation.version - 1) } public undoRenameSheet(operation: RenameSheetUndoEntry) { + this.operations.forceApplyPostponedTransformations() this.operations.renameSheet(operation.sheetId, operation.oldName) + + if (operation.mergedPlaceholderSheetId !== undefined && operation.version !== undefined) { + this.operations.addPlaceholderSheetWithId(operation.mergedPlaceholderSheetId, operation.newName) + this.restoreOldDataFromVersion(operation.version - 1) + } } public undoClearSheet(operation: ClearSheetUndoEntry) { @@ -711,7 +727,7 @@ export class UndoRedo { } public redoAddSheet(operation: AddSheetUndoEntry) { - this.operations.addSheet(operation.sheetName) + this.operations.addSheetWithId(operation.sheetId, operation.sheetName) } public redoRenameSheet(operation: RenameSheetUndoEntry) { diff --git a/src/absolutizeDependencies.ts b/src/absolutizeDependencies.ts index 200890ef4..80c5b16a7 100644 --- a/src/absolutizeDependencies.ts +++ b/src/absolutizeDependencies.ts @@ -4,7 +4,7 @@ */ import {AbsoluteCellRange} from './AbsoluteCellRange' -import {invalidSimpleCellAddress, SimpleCellAddress} from './Cell' +import {isColOrRowInvalid, SimpleCellAddress} from './Cell' import {CellDependency} from './CellDependency' import {NamedExpressionDependency, RelativeDependency} from './parser' @@ -24,9 +24,9 @@ export const filterDependenciesOutOfScope = (deps: CellDependency[]) => { return true } if (dep instanceof AbsoluteCellRange) { - return !(invalidSimpleCellAddress(dep.start) || invalidSimpleCellAddress(dep.end)) + return !(isColOrRowInvalid(dep.start) || isColOrRowInvalid(dep.end)) } else { - return !invalidSimpleCellAddress(dep) + return !isColOrRowInvalid(dep) } }) } diff --git a/src/dependencyTransformers/RemoveSheetTransformer.ts b/src/dependencyTransformers/RemoveSheetTransformer.ts deleted file mode 100644 index 97566a81c..000000000 --- a/src/dependencyTransformers/RemoveSheetTransformer.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * @license - * Copyright (c) 2025 Handsoncode. All rights reserved. - */ - -import {ErrorType, SimpleCellAddress} from '../Cell' -import {DependencyGraph} from '../DependencyGraph' -import {CellAddress, ParserWithCaching} from '../parser' -import {ColumnAddress} from '../parser/ColumnAddress' -import {RowAddress} from '../parser/RowAddress' -import {Transformer} from './Transformer' - -export class RemoveSheetTransformer extends Transformer { - constructor( - public readonly sheet: number - ) { - super() - } - - public isIrreversible() { - return true - } - - public performEagerTransformations(graph: DependencyGraph, _parser: ParserWithCaching): void { - for (const node of graph.arrayFormulaNodes()) { - const [newAst] = this.transformSingleAst(node.getFormula(graph.lazilyTransformingAstService), node.getAddress(graph.lazilyTransformingAstService)) - node.setFormula(newAst) - } - } - - protected fixNodeAddress(address: SimpleCellAddress): SimpleCellAddress { - return address - } - - protected transformCellAddress(dependencyAddress: T, _formulaAddress: SimpleCellAddress): ErrorType.REF | false | T { - return this.transformAddress(dependencyAddress) - } - - protected transformCellRange(start: CellAddress, _end: CellAddress, _formulaAddress: SimpleCellAddress): ErrorType.REF | false { - return this.transformAddress(start) - } - - protected transformColumnRange(start: ColumnAddress, _end: ColumnAddress, _formulaAddress: SimpleCellAddress): ErrorType.REF | false { - return this.transformAddress(start) - } - - protected transformRowRange(start: RowAddress, _end: RowAddress, _formulaAddress: SimpleCellAddress): ErrorType.REF | false { - return this.transformAddress(start) - } - - private transformAddress(address: T): ErrorType.REF | false { - if (address.sheet === this.sheet) { - return ErrorType.REF - } - return false - } -} diff --git a/src/dependencyTransformers/RenameSheetTransformer.ts b/src/dependencyTransformers/RenameSheetTransformer.ts new file mode 100644 index 000000000..d51c4faa2 --- /dev/null +++ b/src/dependencyTransformers/RenameSheetTransformer.ts @@ -0,0 +1,131 @@ +/** + * @license + * Copyright (c) 2025 Handsoncode. All rights reserved. + */ + +import {SimpleCellAddress} from '../Cell' +import {CellAddress} from '../parser' +import {ColumnAddress} from '../parser/ColumnAddress' +import {RowAddress} from '../parser/RowAddress' +import {Transformer} from './Transformer' + +type SheetAwareAddress = CellAddress | RowAddress | ColumnAddress + +/** + * Transformer that reassigns references from a merged sheet into the surviving sheet. + */ +export class RenameSheetTransformer extends Transformer { + constructor( + public readonly sheetIdToKeep: number, + public readonly sheetBeingMerged: number, + ) { + super() + } + + /** + * Returns id of sheet that survives merge operation. + * + * @returns {number} sheet identifier. + */ + public get sheet(): number { + return this.sheetIdToKeep + } + + /** + * Sheet merge cannot be undone because original sheet id is lost. + * + * @returns {boolean} always true to indicate transformation irreversibility. + */ + public isIrreversible(): boolean { + return true + } + + /** + * Updates cell address sheet when it points to merged sheet. + * + * @param {T} dependencyAddress - dependency address needing sheet update. + * @param {SimpleCellAddress} _formulaAddress - location of formula (unused but required by base class). + * @returns {T | false} updated address or false when nothing changes. + */ + protected transformCellAddress(dependencyAddress: T, _formulaAddress: SimpleCellAddress): T | false { + return this.updateSheetInAddress(dependencyAddress) + } + + /** + * Updates sheet for both ends of cell range. + * + * @param {CellAddress} start - start address of range. + * @param {CellAddress} end - end address of range. + * @param {SimpleCellAddress} _formulaAddress - formula location (unused). + * @returns {[CellAddress, CellAddress] | false} updated range tuple or false when unchanged. + */ + protected transformCellRange(start: CellAddress, end: CellAddress, _formulaAddress: SimpleCellAddress): [CellAddress, CellAddress] | false { + return this.transformRange(start, end) + } + + /** + * Updates sheet for both ends of column range. + * + * @param {ColumnAddress} start - beginning column of range. + * @param {ColumnAddress} end - ending column of range. + * @param {SimpleCellAddress} _formulaAddress - formula location (unused). + * @returns {[ColumnAddress, ColumnAddress] | false} updated column range or false. + */ + protected transformColumnRange(start: ColumnAddress, end: ColumnAddress, _formulaAddress: SimpleCellAddress): [ColumnAddress, ColumnAddress] | false { + return this.transformRange(start, end) + } + + /** + * Updates sheet for both ends of row range. + * + * @param {RowAddress} start - beginning row address. + * @param {RowAddress} end - ending row address. + * @param {SimpleCellAddress} _formulaAddress - formula location (unused). + * @returns {[RowAddress, RowAddress] | false} updated row range or false. + */ + protected transformRowRange(start: RowAddress, end: RowAddress, _formulaAddress: SimpleCellAddress): [RowAddress, RowAddress] | false { + return this.transformRange(start, end) + } + + /** + * Node addresses are already absolute, so no change is needed. + * + * @param {SimpleCellAddress} address - node address to inspect. + * @returns {SimpleCellAddress} original address unchanged. + */ + protected fixNodeAddress(address: SimpleCellAddress): SimpleCellAddress { + return address + } + + /** + * Updates sheet identifier for both range ends if needed. + * + * @param {T} start - range start address. + * @param {T} end - range end address. + * @returns {[T, T] | false} tuple with updated addresses or false when no updates happen. + */ + private transformRange(start: T, end: T): [T, T] | false { + const newStart = this.updateSheetInAddress(start) + const newEnd = this.updateSheetInAddress(end) + + if (newStart || newEnd) { + return [(newStart || start), (newEnd || end)] + } + + return false + } + + /** + * Replaces sheet id in address when it points to merged sheet. + * + * @param {T} address - address to update. + * @returns {T | false} address with new sheet id or false when no change occurs. + */ + private updateSheetInAddress(address: T): T | false { + if (address.sheet === this.sheetBeingMerged) { + return address.withSheet(this.sheetIdToKeep) as T + } + + return false + } +} diff --git a/src/interpreter/Interpreter.ts b/src/interpreter/Interpreter.ts index b5c12f430..8edf08f0a 100644 --- a/src/interpreter/Interpreter.ts +++ b/src/interpreter/Interpreter.ts @@ -6,11 +6,11 @@ import {AbsoluteCellRange, AbsoluteColumnRange, AbsoluteRowRange} from '../AbsoluteCellRange' import {ArraySizePredictor} from '../ArraySize' import {ArrayValue, NotComputedArray} from '../ArrayValue' -import {CellError, ErrorType, invalidSimpleCellAddress} from '../Cell' +import {CellError, ErrorType, isColOrRowInvalid} from '../Cell' import {Config} from '../Config' import {DateTimeHelper} from '../DateTimeHelper' import {DependencyGraph} from '../DependencyGraph' -import {FormulaVertex} from '../DependencyGraph/FormulaCellVertex' +import {FormulaVertex} from '../DependencyGraph/FormulaVertex' import {ErrorMessage} from '../error-message' import {LicenseKeyValidityState} from '../helpers/licenseKeyValidator' import {ColumnSearchStrategy} from '../Lookup/SearchStrategy' @@ -40,6 +40,7 @@ import { isExtendedNumber, } from './InterpreterValue' import {SimpleRangeValue} from '../SimpleRangeValue' +import { AddressWithSheet } from '../parser/Address' export class Interpreter { public readonly criterionBuilder: CriterionBuilder @@ -78,8 +79,8 @@ export class Interpreter { /** * Calculates cell value from formula abstract syntax tree * - * @param formula - abstract syntax tree of formula - * @param formulaAddress - address of the cell in which formula is located + * @param {Ast} ast - abstract syntax tree of formula + * @param {InterpreterState} state - interpreter state */ private evaluateAstWithoutPostprocessing(ast: Ast, state: InterpreterState): InterpreterValue { switch (ast.type) { @@ -88,9 +89,15 @@ export class Interpreter { } case AstNodeType.CELL_REFERENCE: { const address = ast.reference.toSimpleCellAddress(state.formulaAddress) - if (invalidSimpleCellAddress(address)) { + + if (isColOrRowInvalid(address)) { return new CellError(ErrorType.REF, ErrorMessage.BadRef) } + + if (!this.isSheetValid(ast.reference)) { + return new CellError(ErrorType.REF, ErrorMessage.SheetRef) + } + return this.dependencyGraph.getCellValue(address) } case AstNodeType.NUMBER: @@ -189,11 +196,17 @@ export class Interpreter { } } case AstNodeType.CELL_RANGE: { + if (!this.isSheetValid(ast.start) || !this.isSheetValid(ast.end)) { + return new CellError(ErrorType.REF, ErrorMessage.SheetRef) + } + if (!this.rangeSpansOneSheet(ast)) { return new CellError(ErrorType.REF, ErrorMessage.RangeManySheets) } + const range = AbsoluteCellRange.fromCellRange(ast, state.formulaAddress) const arrayVertex = this.dependencyGraph.getArray(range) + if (arrayVertex) { const array = arrayVertex.array if (array instanceof NotComputedArray) { @@ -205,11 +218,15 @@ export class Interpreter { } else { throw new Error('Unknown array') } - } else { - return SimpleRangeValue.onlyRange(range, this.dependencyGraph) } + + return SimpleRangeValue.onlyRange(range, this.dependencyGraph) } case AstNodeType.COLUMN_RANGE: { + if (!this.isSheetValid(ast.start) || !this.isSheetValid(ast.end)) { + return new CellError(ErrorType.REF, ErrorMessage.SheetRef) + } + if (!this.rangeSpansOneSheet(ast)) { return new CellError(ErrorType.REF, ErrorMessage.RangeManySheets) } @@ -217,6 +234,10 @@ export class Interpreter { return SimpleRangeValue.onlyRange(range, this.dependencyGraph) } case AstNodeType.ROW_RANGE: { + if (!this.isSheetValid(ast.start) || !this.isSheetValid(ast.end)) { + return new CellError(ErrorType.REF, ErrorMessage.SheetRef) + } + if (!this.rangeSpansOneSheet(ast)) { return new CellError(ErrorType.REF, ErrorMessage.RangeManySheets) } @@ -265,6 +286,17 @@ export class Interpreter { } } + /** + * Sheet is valid if: + * - sheet is undefined OR + * - sheet is a named expressions store OR + * - sheet exists in sheet mapping + * - sheet is not a placeholder + */ + private isSheetValid(address: AddressWithSheet): boolean { + return address.sheet === undefined || address.sheet === NamedExpressions.SHEET_FOR_WORKBOOK_EXPRESSIONS || this.dependencyGraph.sheetMapping.hasSheetWithId(address.sheet, { includePlaceholders: false }) + } + private rangeSpansOneSheet(ast: CellRangeAst | ColumnRangeAst | RowRangeAst): boolean { return ast.start.sheet === ast.end.sheet } @@ -465,4 +497,3 @@ function wrapperForRootVertex(val: InterpreterValue, vertex?: FormulaVertex): In } return val } - diff --git a/src/interpreter/InterpreterState.ts b/src/interpreter/InterpreterState.ts index fdf5cb900..be48aa6a0 100644 --- a/src/interpreter/InterpreterState.ts +++ b/src/interpreter/InterpreterState.ts @@ -4,7 +4,7 @@ */ import {SimpleCellAddress} from '../Cell' -import {FormulaVertex} from '../DependencyGraph/FormulaCellVertex' +import {FormulaVertex} from '../DependencyGraph/FormulaVertex' export class InterpreterState { constructor( @@ -14,4 +14,3 @@ export class InterpreterState { ) { } } - diff --git a/src/interpreter/plugin/InformationPlugin.ts b/src/interpreter/plugin/InformationPlugin.ts index 625453ecb..481522107 100644 --- a/src/interpreter/plugin/InformationPlugin.ts +++ b/src/interpreter/plugin/InformationPlugin.ts @@ -4,7 +4,7 @@ */ import {CellError, ErrorType, SimpleCellAddress} from '../../Cell' -import {FormulaVertex} from '../../DependencyGraph/FormulaCellVertex' +import {FormulaVertex} from '../../DependencyGraph/FormulaVertex' import {ErrorMessage} from '../../error-message' import {AstNodeType, ProcedureAst} from '../../parser' import {InterpreterState} from '../InterpreterState' @@ -457,7 +457,7 @@ export class InformationPlugin extends FunctionPlugin implements FunctionPluginT () => state.formulaAddress.sheet + 1, (reference: SimpleCellAddress) => reference.sheet + 1, (value: string) => { - const sheetNumber = this.dependencyGraph.sheetMapping.get(value) + const sheetNumber = this.dependencyGraph.sheetMapping.getSheetId(value) if (sheetNumber !== undefined) { return sheetNumber + 1 } else { diff --git a/src/interpreter/plugin/NumericAggregationPlugin.ts b/src/interpreter/plugin/NumericAggregationPlugin.ts index 1b0abf9fe..5a675e936 100644 --- a/src/interpreter/plugin/NumericAggregationPlugin.ts +++ b/src/interpreter/plugin/NumericAggregationPlugin.ts @@ -609,13 +609,13 @@ export class NumericAggregationPlugin extends FunctionPlugin implements Function /** * Performs range operation on given range * - * @param ast - cell range ast - * @param state - * @param initialAccValue - initial accumulator value for reducing function - * @param functionName - function name to use as cache key - * @param reducingFunction - reducing function - * @param mapFunction - * @param coercionFunction + * @param {CellRangeAst | ColumnRangeAst | RowRangeAst} ast - cell range ast + * @param {InterpreterState} state - interpreter state + * @param {T} initialAccValue - initial accumulator value for reducing function + * @param {string} functionName - function name to use as cache key + * @param {BinaryOperation} reducingFunction - reducing function + * @param {MapOperation} mapFunction - mapper transforming coerced scalar + * @param {coercionOperation} coercionFunction - scalar-to-number coercer */ private evaluateRange(ast: CellRangeAst | ColumnRangeAst | RowRangeAst, state: InterpreterState, initialAccValue: T, functionName: string, reducingFunction: BinaryOperation, mapFunction: MapOperation, coercionFunction: coercionOperation): T | CellError { let range @@ -629,6 +629,10 @@ export class NumericAggregationPlugin extends FunctionPlugin implements Function } } + if (!this.isSheetValid(range)) { + return new CellError(ErrorType.REF, ErrorMessage.SheetRef) + } + const rangeVertex = this.dependencyGraph.getRange(range.start, range.end) if (rangeVertex === undefined) { @@ -653,6 +657,16 @@ export class NumericAggregationPlugin extends FunctionPlugin implements Function return value } + /** + * Checks whether both ends of a range point to existing sheets (placeholders excluded). + */ + private isSheetValid(range: AbsoluteCellRange): boolean { + return ( + this.dependencyGraph.sheetMapping.hasSheetWithId(range.start.sheet, {includePlaceholders: false}) && + this.dependencyGraph.sheetMapping.hasSheetWithId(range.end.sheet, {includePlaceholders: false}) + ) + } + /** * Returns list of values for given range and function name * @@ -687,6 +701,7 @@ export class NumericAggregationPlugin extends FunctionPlugin implements Function } else { actualRange = range } + for (const cellFromRange of actualRange.addresses(this.dependencyGraph)) { const val = coercionFunction(this.dependencyGraph.getScalarValue(cellFromRange)) if (val instanceof CellError) { diff --git a/src/parser/CellAddress.ts b/src/parser/CellAddress.ts index e1243d069..7013349d2 100644 --- a/src/parser/CellAddress.ts +++ b/src/parser/CellAddress.ts @@ -5,7 +5,7 @@ import { absoluteSheetReference, - invalidSimpleCellAddress, + isColOrRowInvalid, simpleCellAddress, SimpleCellAddress, simpleColumnAddress, @@ -155,7 +155,7 @@ export class CellAddress implements AddressWithColumn, AddressWithRow { } public isInvalid(baseAddress: SimpleCellAddress): boolean { - return invalidSimpleCellAddress(this.toSimpleCellAddress(baseAddress)) + return isColOrRowInvalid(this.toSimpleCellAddress(baseAddress)) } public shiftRelativeDimensions(toRight: number, toBottom: number): CellAddress { @@ -190,7 +190,7 @@ export class CellAddress implements AddressWithColumn, AddressWithRow { public unparse(baseAddress: SimpleCellAddress): Maybe { const simpleAddress = this.toSimpleCellAddress(baseAddress) - if (invalidSimpleCellAddress(simpleAddress)) { + if (isColOrRowInvalid(simpleAddress)) { return undefined } const column = columnIndexToLabel(simpleAddress.col) diff --git a/src/parser/FormulaParser.ts b/src/parser/FormulaParser.ts index d22af1780..82c70e1c1 100644 --- a/src/parser/FormulaParser.ts +++ b/src/parser/FormulaParser.ts @@ -21,7 +21,7 @@ import { cellAddressFromString, columnAddressFromString, rowAddressFromString, - SheetMappingFn, + ResolveSheetReferenceFn, } from './addressRepresentationConverters' import { ArrayAst, @@ -130,12 +130,21 @@ export class FormulaParser extends EmbeddedActionsParser { private customParsingError?: ParsingError - private readonly sheetMapping: SheetMappingFn - /** * Cache for positiveAtomicExpression alternatives */ private atomicExpCache: Maybe + + constructor( + lexerConfig: LexerConfig, + private readonly resolveSheetReference: ResolveSheetReferenceFn, + ) { + super(lexerConfig.allTokens, {outputCst: false, maxLookahead: 7}) + this.lexerConfig = lexerConfig + this.formulaAddress = simpleCellAddress(0, 0, 0) + this.performSelfAnalysis() + } + private booleanExpressionOrEmpty: AstRule = this.RULE('booleanExpressionOrEmpty', () => { return this.OR([ {ALT: () => this.SUBRULE(this.booleanExpression)}, @@ -200,8 +209,8 @@ export class FormulaParser extends EmbeddedActionsParser { private columnRangeExpression: AstRule = this.RULE('columnRangeExpression', () => { const range = this.CONSUME(ColumnRange) as ExtendedToken const [startImage, endImage] = range.image.split(':') - const firstAddress = this.ACTION(() => columnAddressFromString(this.sheetMapping, startImage, this.formulaAddress)) - const secondAddress = this.ACTION(() => columnAddressFromString(this.sheetMapping, endImage, this.formulaAddress)) + const firstAddress = this.ACTION(() => columnAddressFromString(startImage, this.formulaAddress, this.resolveSheetReference)) + const secondAddress = this.ACTION(() => columnAddressFromString(endImage, this.formulaAddress, this.resolveSheetReference)) if (firstAddress === undefined || secondAddress === undefined) { return buildCellErrorAst(new CellError(ErrorType.REF)) @@ -226,8 +235,8 @@ export class FormulaParser extends EmbeddedActionsParser { private rowRangeExpression: AstRule = this.RULE('rowRangeExpression', () => { const range = this.CONSUME(RowRange) as ExtendedToken const [startImage, endImage] = range.image.split(':') - const firstAddress = this.ACTION(() => rowAddressFromString(this.sheetMapping, startImage, this.formulaAddress)) - const secondAddress = this.ACTION(() => rowAddressFromString(this.sheetMapping, endImage, this.formulaAddress)) + const firstAddress = this.ACTION(() => rowAddressFromString(startImage, this.formulaAddress, this.resolveSheetReference)) + const secondAddress = this.ACTION(() => rowAddressFromString(endImage, this.formulaAddress, this.resolveSheetReference)) if (firstAddress === undefined || secondAddress === undefined) { return buildCellErrorAst(new CellError(ErrorType.REF)) @@ -252,8 +261,9 @@ export class FormulaParser extends EmbeddedActionsParser { private cellReference: AstRule = this.RULE('cellReference', () => { const cell = this.CONSUME(CellReference) as ExtendedToken const address = this.ACTION(() => { - return cellAddressFromString(this.sheetMapping, cell.image, this.formulaAddress) + return cellAddressFromString(cell.image, this.formulaAddress, this.resolveSheetReference) }) + if (address === undefined) { return buildErrorWithRawInputAst(cell.image, new CellError(ErrorType.REF), cell.leadingWhitespace) } else if (address.exceedsSheetSizeLimits(this.lexerConfig.maxColumns, this.lexerConfig.maxRows)) { @@ -270,10 +280,10 @@ export class FormulaParser extends EmbeddedActionsParser { const end = this.CONSUME(CellReference) as ExtendedToken const startAddress = this.ACTION(() => { - return cellAddressFromString(this.sheetMapping, start.image, this.formulaAddress) + return cellAddressFromString(start.image, this.formulaAddress, this.resolveSheetReference) }) const endAddress = this.ACTION(() => { - return cellAddressFromString(this.sheetMapping, end.image, this.formulaAddress) + return cellAddressFromString(end.image, this.formulaAddress, this.resolveSheetReference) }) if (startAddress === undefined || endAddress === undefined) { @@ -306,7 +316,7 @@ export class FormulaParser extends EmbeddedActionsParser { ALT: () => { const offsetProcedure = this.SUBRULE(this.offsetProcedureExpression) const startAddress = this.ACTION(() => { - return cellAddressFromString(this.sheetMapping, start.image, this.formulaAddress) + return cellAddressFromString(start.image, this.formulaAddress, this.resolveSheetReference) }) if (startAddress === undefined) { return buildCellErrorAst(new CellError(ErrorType.REF)) @@ -337,7 +347,7 @@ export class FormulaParser extends EmbeddedActionsParser { const end = this.CONSUME(CellReference) as ExtendedToken const endAddress = this.ACTION(() => { - return cellAddressFromString(this.sheetMapping, end.image, this.formulaAddress) + return cellAddressFromString(end.image, this.formulaAddress, this.resolveSheetReference) }) if (endAddress === undefined) { @@ -451,14 +461,6 @@ export class FormulaParser extends EmbeddedActionsParser { ]) as Ast }) - constructor(lexerConfig: LexerConfig, sheetMapping: SheetMappingFn) { - super(lexerConfig.allTokens, {outputCst: false, maxLookahead: 7}) - this.lexerConfig = lexerConfig - this.sheetMapping = sheetMapping - this.formulaAddress = simpleCellAddress(0, 0, 0) - this.performSelfAnalysis() - } - /** * Parses tokenized formula and builds abstract syntax tree * @@ -829,7 +831,6 @@ export class FormulaParser extends EmbeddedActionsParser { } if (cellArg.reference.type === CellReferenceType.CELL_REFERENCE_RELATIVE || cellArg.reference.type === CellReferenceType.CELL_REFERENCE_ABSOLUTE_ROW) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion absoluteCol = absoluteCol + this.formulaAddress.col } @@ -843,8 +844,10 @@ export class FormulaParser extends EmbeddedActionsParser { topLeftCorner.col + width - 1, topLeftCorner.row + height - 1, topLeftCorner.type, + topLeftCorner.sheet, ) - return buildCellRangeAst(topLeftCorner, bottomRightCorner, RangeSheetReferenceType.RELATIVE) + const rangeSheetReferenceType = cellArg.reference.sheet == null ? RangeSheetReferenceType.RELATIVE : RangeSheetReferenceType.BOTH_ABSOLUTE + return buildCellRangeAst(topLeftCorner, bottomRightCorner, rangeSheetReferenceType) } } diff --git a/src/parser/ParserWithCaching.ts b/src/parser/ParserWithCaching.ts index 84681e216..a116aa8e5 100644 --- a/src/parser/ParserWithCaching.ts +++ b/src/parser/ParserWithCaching.ts @@ -11,7 +11,7 @@ import { cellAddressFromString, columnAddressFromString, rowAddressFromString, - SheetMappingFn, + ResolveSheetReferenceFn, } from './addressRepresentationConverters' import {Ast, imageWithWhitespace, ParsingError, ParsingErrorType, RangeSheetReferenceType} from './Ast' import {binaryOpTokenMap} from './binaryOpTokenMap' @@ -52,11 +52,11 @@ export class ParserWithCaching { constructor( private readonly config: ParserConfig, private readonly functionRegistry: FunctionRegistry, - private readonly sheetMapping: SheetMappingFn, + private readonly resolveSheetReference: ResolveSheetReferenceFn, ) { this.lexerConfig = buildLexerConfig(config) this.lexer = new FormulaLexer(this.lexerConfig) - this.formulaParser = new FormulaParser(this.lexerConfig, this.sheetMapping) + this.formulaParser = new FormulaParser(this.lexerConfig, this.resolveSheetReference) this.cache = new Cache(this.functionRegistry) } @@ -232,7 +232,7 @@ export class ParserWithCaching { while (idx < tokens.length) { const token = tokens[idx] if (tokenMatcher(token, CellReference)) { - const cellAddress = cellAddressFromString(this.sheetMapping, token.image, baseAddress) + const cellAddress = cellAddressFromString(token.image, baseAddress, this.resolveSheetReference) if (cellAddress === undefined) { hash = hash.concat(token.image) } else { @@ -244,8 +244,8 @@ export class ParserWithCaching { hash = hash.concat(canonicalProcedureName, '(') } else if (tokenMatcher(token, ColumnRange)) { const [start, end] = token.image.split(':') - const startAddress = columnAddressFromString(this.sheetMapping, start, baseAddress) - const endAddress = columnAddressFromString(this.sheetMapping, end, baseAddress) + const startAddress = columnAddressFromString(start, baseAddress, this.resolveSheetReference) + const endAddress = columnAddressFromString(end, baseAddress, this.resolveSheetReference) if (startAddress === undefined || endAddress === undefined) { hash = hash.concat('!REF') } else { @@ -253,8 +253,8 @@ export class ParserWithCaching { } } else if (tokenMatcher(token, RowRange)) { const [start, end] = token.image.split(':') - const startAddress = rowAddressFromString(this.sheetMapping, start, baseAddress) - const endAddress = rowAddressFromString(this.sheetMapping, end, baseAddress) + const startAddress = rowAddressFromString(start, baseAddress, this.resolveSheetReference) + const endAddress = rowAddressFromString(end, baseAddress, this.resolveSheetReference) if (startAddress === undefined || endAddress === undefined) { hash = hash.concat('!REF') } else { diff --git a/src/parser/Unparser.ts b/src/parser/Unparser.ts index 425ed689c..d2154477d 100644 --- a/src/parser/Unparser.ts +++ b/src/parser/Unparser.ts @@ -4,9 +4,10 @@ */ import {ErrorType, SimpleCellAddress} from '../Cell' +import {SheetMapping} from '../DependencyGraph/SheetMapping' import {NoSheetWithIdError} from '../index' import {NamedExpressions} from '../NamedExpressions' -import {SheetIndexMappingFn, sheetIndexToString} from './addressRepresentationConverters' +import {sheetIndexToString} from './addressRepresentationConverters' import { Ast, AstNodeType, @@ -17,14 +18,12 @@ import { RowRangeAst, } from './Ast' import {binaryOpTokenMap} from './binaryOpTokenMap' -import {LexerConfig} from './LexerConfig' import {ParserConfig} from './ParserConfig' export class Unparser { constructor( private readonly config: ParserConfig, - private readonly lexerConfig: LexerConfig, - private readonly sheetMappingFn: SheetIndexMappingFn, + private readonly sheetMapping: SheetMapping, private readonly namedExpressions: NamedExpressions, ) { } @@ -109,8 +108,13 @@ export class Unparser { } } + /** + * Unparses a sheet name. + * @param {number} sheetId - the ID of the sheet + * @returns {string} the unparsed sheet name + */ private unparseSheetName(sheetId: number): string { - const sheetName = sheetIndexToString(sheetId, this.sheetMappingFn) + const sheetName = sheetIndexToString(sheetId, id => this.sheetMapping.getSheetNameOrThrowError(id, { includePlaceholders: true })) if (sheetName === undefined) { throw new NoSheetWithIdError(sheetId) } diff --git a/src/parser/addressRepresentationConverters.ts b/src/parser/addressRepresentationConverters.ts index b27188387..06f7cb6dd 100644 --- a/src/parser/addressRepresentationConverters.ts +++ b/src/parser/addressRepresentationConverters.ts @@ -11,8 +11,8 @@ import {ColumnAddress} from './ColumnAddress' import {ABSOLUTE_OPERATOR, RANGE_OPERATOR, SHEET_NAME_PATTERN, UNQUOTED_SHEET_NAME_PATTERN} from './parser-consts' import {RowAddress} from './RowAddress' -export type SheetMappingFn = (sheetName: string) => Maybe export type SheetIndexMappingFn = (sheetIndex: number) => Maybe +export type ResolveSheetReferenceFn = (sheetName: string) => Maybe const addressRegex = new RegExp(`^(${SHEET_NAME_PATTERN})?(\\${ABSOLUTE_OPERATOR}?)([A-Za-z]+)(\\${ABSOLUTE_OPERATOR}?)([0-9]+)$`) const columnRegex = new RegExp(`^(${SHEET_NAME_PATTERN})?(\\${ABSOLUTE_OPERATOR}?)([A-Za-z]+)$`) @@ -22,26 +22,27 @@ const simpleSheetNameRegex = new RegExp(`^${UNQUOTED_SHEET_NAME_PATTERN}$`) /** * Computes R0C0 representation of cell address based on it's string representation and base address. * - * @param sheetMapping - mapping function needed to change name of a sheet to index - * @param stringAddress - string representation of cell address, e.g., 'C64' - * @param baseAddress - base address for R0C0 conversion - * @returns object representation of address + * @param {string} stringAddress - string representation of cell address, e.g., 'C64' + * @param {SimpleCellAddress} baseAddress - base address for R0C0 conversion + * @param {ResolveSheetReferenceFn} resolveSheetReference - mapping function needed to change name of a sheet to index + * @returns {Maybe} object representation of address or `undefined` if the sheet cannot be resolved */ -export const cellAddressFromString = (sheetMapping: SheetMappingFn, stringAddress: string, baseAddress: SimpleCellAddress): Maybe => { - const result = addressRegex.exec(stringAddress)! +export const cellAddressFromString = (stringAddress: string, baseAddress: SimpleCellAddress, resolveSheetReference: ResolveSheetReferenceFn): Maybe => { + const result = addressRegex.exec(stringAddress) - const col = columnLabelToIndex(result[6]) - - let sheet = extractSheetNumber(result, sheetMapping) - if (sheet === undefined) { + if (!result) { return undefined } + const col = columnLabelToIndex(result[6]) + const row = Number(result[8]) - 1 + const sheetName = extractSheetName(result) + const sheet = sheetNameToId(sheetName, resolveSheetReference) + if (sheet === null) { - sheet = undefined + return undefined } - const row = Number(result[8]) - 1 if (result[5] === ABSOLUTE_OPERATOR && result[7] === ABSOLUTE_OPERATOR) { return CellAddress.absolute(col, row, sheet) } else if (result[5] === ABSOLUTE_OPERATOR) { @@ -53,20 +54,21 @@ export const cellAddressFromString = (sheetMapping: SheetMappingFn, stringAddres } } -export const columnAddressFromString = (sheetMapping: SheetMappingFn, stringAddress: string, baseAddress: SimpleCellAddress): Maybe => { - const result = columnRegex.exec(stringAddress)! +export const columnAddressFromString = (stringAddress: string, baseAddress: SimpleCellAddress, resolveSheetReference: ResolveSheetReferenceFn): Maybe => { + const result = columnRegex.exec(stringAddress) - let sheet = extractSheetNumber(result, sheetMapping) - if (sheet === undefined) { + if (!result) { return undefined } + const col = columnLabelToIndex(result[6]) + const sheetName = extractSheetName(result) + const sheet = sheetNameToId(sheetName, resolveSheetReference) + if (sheet === null) { - sheet = undefined + return undefined } - const col = columnLabelToIndex(result[6]) - if (result[5] === ABSOLUTE_OPERATOR) { return ColumnAddress.absolute(col, sheet) } else { @@ -74,20 +76,21 @@ export const columnAddressFromString = (sheetMapping: SheetMappingFn, stringAddr } } -export const rowAddressFromString = (sheetMapping: SheetMappingFn, stringAddress: string, baseAddress: SimpleCellAddress): Maybe => { - const result = rowRegex.exec(stringAddress)! +export const rowAddressFromString = (stringAddress: string, baseAddress: SimpleCellAddress, resolveSheetReference: ResolveSheetReferenceFn): Maybe => { + const result = rowRegex.exec(stringAddress) - let sheet = extractSheetNumber(result, sheetMapping) - if (sheet === undefined) { + if (!result) { return undefined } + const row = Number(result[6]) - 1 + const sheetName = extractSheetName(result) + const sheet = sheetNameToId(sheetName, resolveSheetReference) + if (sheet === null) { - sheet = undefined + return undefined } - const row = Number(result[6]) - 1 - if (result[5] === ABSOLUTE_OPERATOR) { return RowAddress.absolute(row, sheet) } else { @@ -100,44 +103,43 @@ export const rowAddressFromString = (sheetMapping: SheetMappingFn, stringAddress * - If sheet name is present in the string representation but is not present in sheet mapping, returns `undefined`. * - If sheet name is not present in the string representation, returns {@param contextSheetId} as sheet number. * - * @param sheetMapping - mapping function needed to change name of a sheet to index - * @param stringAddress - string representation of cell address, e.g., 'C64' - * @param contextSheetId - sheet in context of which we should parse the address - * @returns absolute representation of address, e.g., { sheet: 0, col: 1, row: 1 } + * @param {ResolveSheetReferenceFn} resolveSheetReference - mapping function needed to change name of a sheet to index + * @param {string} stringAddress - string representation of cell address, e.g., 'C64' + * @param {number} contextSheetId - sheet in context of which we should parse the address + * @returns {Maybe} absolute representation of address, e.g., { sheet: 0, col: 1, row: 1 } */ -export const simpleCellAddressFromString = (sheetMapping: SheetMappingFn, stringAddress: string, contextSheetId: number): Maybe => { - const regExpExecArray = addressRegex.exec(stringAddress)! +export const simpleCellAddressFromString = (resolveSheetReference: ResolveSheetReferenceFn, stringAddress: string, contextSheetId: number): Maybe => { + const regExpExecArray = addressRegex.exec(stringAddress) if (!regExpExecArray) { return undefined } const col = columnLabelToIndex(regExpExecArray[6]) + const row = Number(regExpExecArray[8]) - 1 + const sheetName = extractSheetName(regExpExecArray) + const sheet = sheetNameToId(sheetName, resolveSheetReference) - let sheet = extractSheetNumber(regExpExecArray, sheetMapping) - if (sheet === undefined) { + if (sheet === null) { return undefined } - if (sheet === null) { - sheet = contextSheetId - } + const effectiveSheet = sheet === undefined ? contextSheetId : sheet - const row = Number(regExpExecArray[8]) - 1 - return simpleCellAddress(sheet, col, row) + return simpleCellAddress(effectiveSheet, col, row) } -export const simpleCellRangeFromString = (sheetMapping: SheetMappingFn, stringAddress: string, contextSheetId: number): Maybe => { +export const simpleCellRangeFromString = (resolveSheetReference: ResolveSheetReferenceFn, stringAddress: string, contextSheetId: number): Maybe => { const split = stringAddress.split(RANGE_OPERATOR) if (split.length !== 2) { return undefined } const [startString, endString] = split - const start = simpleCellAddressFromString(sheetMapping, startString, contextSheetId) + const start = simpleCellAddressFromString(resolveSheetReference, startString, contextSheetId) if (start === undefined) { return undefined } - const end = simpleCellAddressFromString(sheetMapping, endString, start.sheet) + const end = simpleCellAddressFromString(resolveSheetReference, endString, start.sheet) if (end === undefined) { return undefined } @@ -150,10 +152,6 @@ export const simpleCellRangeFromString = (sheetMapping: SheetMappingFn, stringAd /** * Returns string representation of absolute address * If sheet index is not present in sheet mapping, returns undefined - * - * @param sheetIndexMapping - mapping function needed to change sheet index to sheet name - * @param address - object representation of absolute address - * @param sheetIndex - if is not equal with address sheet index, string representation will contain sheet name */ export const simpleCellAddressToString = (sheetIndexMapping: SheetIndexMappingFn, address: SimpleCellAddress, sheetIndex: number): Maybe => { const column = columnIndexToLabel(address.col) @@ -227,13 +225,25 @@ export function sheetIndexToString(sheetId: number, sheetMappingFn: SheetIndexMa } } -function extractSheetNumber(regexResult: RegExpExecArray, sheetMapping: SheetMappingFn): number | null | undefined { - let maybeSheetName = regexResult[3] ?? regexResult[2] +function extractSheetName(regexResult: RegExpExecArray): string | null { + const maybeSheetName = regexResult[3] ?? regexResult[2] - if (maybeSheetName) { - maybeSheetName = maybeSheetName.replace(/''/g, "'") - return sheetMapping(maybeSheetName) - } else { - return null + return maybeSheetName ? maybeSheetName.replace(/''/g, "'") : null +} + +/** + * Resolves sheet name to sheet id. + * + * @param sheetName - extracted sheet name or null when not provided. + * @param resolveSheetReference - mapping function resolving sheet name to id. + * @returns sheet id, undefined when sheet name absent, null when resolution fails. + */ +function sheetNameToId(sheetName: string | null, resolveSheetReference: ResolveSheetReferenceFn): Maybe | null { + if (!sheetName) { + return undefined } + + const sheetId = resolveSheetReference(sheetName) + + return sheetId === undefined ? null : sheetId } diff --git a/test/unit/AbsoluteCellRange.spec.ts b/test/unit/AbsoluteCellRange.spec.ts index 4253a1c73..e698b237a 100644 --- a/test/unit/AbsoluteCellRange.spec.ts +++ b/test/unit/AbsoluteCellRange.spec.ts @@ -7,13 +7,13 @@ describe('AbsoluteCellRange', () => { it('true in simplest case', () => { const range = AbsoluteCellRange.fromCoordinates(0, 0, 0, 0, 0) - expect(range.addressInRange(adr('A1'))) + expect(range.addressInRange(adr('A1'))).toBe(true) }) it('false if different sheets', () => { const range = AbsoluteCellRange.fromCoordinates(1, 0, 0, 0, 0) - expect(range.addressInRange(adr('A1'))) + expect(range.addressInRange(adr('A1'))).toBe(false) }) }) diff --git a/test/unit/CellValueExporter.spec.ts b/test/unit/CellValueExporter.spec.ts index 8f7df4f06..ca1daf8cd 100644 --- a/test/unit/CellValueExporter.spec.ts +++ b/test/unit/CellValueExporter.spec.ts @@ -1,24 +1,24 @@ import {DetailedCellError, ErrorType, HyperFormula} from '../../src' import {CellError} from '../../src/Cell' import {Config} from '../../src/Config' +import { SheetMapping } from '../../src/DependencyGraph' import {ErrorMessage} from '../../src/error-message' import {Exporter} from '../../src/Exporter' import {plPL} from '../../src/i18n/languages' import {EmptyValue} from '../../src/interpreter/InterpreterValue' import {LazilyTransformingAstService} from '../../src/LazilyTransformingAstService' import {NamedExpressions} from '../../src/NamedExpressions' -import {SheetIndexMappingFn} from '../../src/parser/addressRepresentationConverters' import {EmptyStatistics} from '../../src/statistics' import {detailedError} from './testUtils' const namedExpressionsMock = {} as NamedExpressions -const sheetIndexMock = {} as SheetIndexMappingFn +const sheetMappingMock = {} as SheetMapping const lazilyTransforminService = new LazilyTransformingAstService(new EmptyStatistics()) describe('rounding', () => { it('no rounding', () => { const config = new Config({smartRounding: false}) - const cellValueExporter = new Exporter(config, namedExpressionsMock, sheetIndexMock, lazilyTransforminService) + const cellValueExporter = new Exporter(config, namedExpressionsMock, sheetMappingMock, lazilyTransforminService) expect(cellValueExporter.exportValue(1.000000000000001)).toBe(1.000000000000001) expect(cellValueExporter.exportValue(-1.000000000000001)).toBe(-1.000000000000001) expect(cellValueExporter.exportValue(0.000000000000001)).toBe(0.000000000000001) @@ -32,7 +32,7 @@ describe('rounding', () => { it('with rounding', () => { const config = new Config() - const cellValueExporter = new Exporter(config, namedExpressionsMock, sheetIndexMock, lazilyTransforminService) + const cellValueExporter = new Exporter(config, namedExpressionsMock, sheetMappingMock, lazilyTransforminService) expect(cellValueExporter.exportValue(1.000000001)).toBe(1.000000001) expect(cellValueExporter.exportValue(-1.000000001)).toBe(-1.000000001) expect(cellValueExporter.exportValue(1.00000000001)).toBe(1) @@ -50,7 +50,7 @@ describe('rounding', () => { describe('detailed error', () => { it('should return detailed errors', () => { const config = new Config({language: 'enGB'}) - const cellValueExporter = new Exporter(config, namedExpressionsMock, sheetIndexMock, lazilyTransforminService) + const cellValueExporter = new Exporter(config, namedExpressionsMock, sheetMappingMock, lazilyTransforminService) const error = cellValueExporter.exportValue(new CellError(ErrorType.VALUE)) as DetailedCellError expect(error).toEqualError(detailedError(ErrorType.VALUE)) @@ -60,7 +60,7 @@ describe('detailed error', () => { it('should return detailed errors with translation', () => { HyperFormula.registerLanguage('plPL', plPL) const config = new Config({language: 'plPL'}) - const cellValueExporter = new Exporter(config, namedExpressionsMock, sheetIndexMock, lazilyTransforminService) + const cellValueExporter = new Exporter(config, namedExpressionsMock, sheetMappingMock, lazilyTransforminService) const error = cellValueExporter.exportValue(new CellError(ErrorType.VALUE)) as DetailedCellError expect(error).toEqualError(detailedError(ErrorType.VALUE, undefined, config)) diff --git a/test/unit/_setupFiles/jest/bootstrap.ts b/test/unit/_setupFiles/jest/bootstrap.ts index 3e8225cb4..f8f8f9d29 100644 --- a/test/unit/_setupFiles/jest/bootstrap.ts +++ b/test/unit/_setupFiles/jest/bootstrap.ts @@ -3,6 +3,7 @@ import {toEqualError} from './toEqualError' beforeAll(() => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore + // eslint-disable-next-line no-global-assign spyOn = jest.spyOn expect.extend(toEqualError) }) diff --git a/test/unit/address-mapping.spec.ts b/test/unit/address-mapping.spec.ts index 02014a6ce..496c714d5 100644 --- a/test/unit/address-mapping.spec.ts +++ b/test/unit/address-mapping.spec.ts @@ -16,7 +16,7 @@ const sharedExamples = (builder: (width: number, height: number) => AddressMappi mapping.setCell(address, vertex) - expect(mapping.fetchCell(address)).toBe(vertex) + expect(mapping.getCell(address)).toBe(vertex) }) it('set and using different reference when get', () => { @@ -25,7 +25,7 @@ const sharedExamples = (builder: (width: number, height: number) => AddressMappi mapping.setCell(adr('A1'), vertex) - expect(mapping.fetchCell(adr('A1'))).toBe(vertex) + expect(mapping.getCell(adr('A1'))).toBe(vertex) }) it("get when there's even no column", () => { @@ -141,8 +141,8 @@ const sharedExamples = (builder: (width: number, height: number) => AddressMappi mapping.setCell(adr('A2'), vertex1) - expect(mapping.fetchCell(adr('A1'))).toBe(vertex0) - expect(mapping.fetchCell(adr('A2'))).toBe(vertex1) + expect(mapping.getCell(adr('A1'))).toBe(vertex0) + expect(mapping.getCell(adr('A2'))).toBe(vertex1) }) it('set overrides old value', () => { @@ -153,7 +153,7 @@ const sharedExamples = (builder: (width: number, height: number) => AddressMappi mapping.setCell(adr('A1'), vertex1) - expect(mapping.fetchCell(adr('A1'))).toBe(vertex1) + expect(mapping.getCell(adr('A1'))).toBe(vertex1) }) it("has when there's even no column", () => { @@ -192,8 +192,8 @@ const sharedExamples = (builder: (width: number, height: number) => AddressMappi mapping.addRows(0, 0, 1) expect(mapping.getCell(adr('A1'))).toBe(undefined) - expect(mapping.fetchCell(adr('A2'))).toEqual(new ValueCellVertex(42, 42)) - expect(mapping.getHeight(0)).toEqual(2) + expect(mapping.getCell(adr('A2'))).toEqual(new ValueCellVertex(42, 42)) + expect(mapping.getSheetHeight(0)).toEqual(2) }) it('addRows in the middle of a mapping', () => { @@ -204,10 +204,10 @@ const sharedExamples = (builder: (width: number, height: number) => AddressMappi mapping.addRows(0, 1, 1) - expect(mapping.fetchCell(adr('A1'))).toEqual(new ValueCellVertex(42, 42)) + expect(mapping.getCell(adr('A1'))).toEqual(new ValueCellVertex(42, 42)) expect(mapping.getCell(adr('A2'))).toBe(undefined) - expect(mapping.fetchCell(adr('A3'))).toEqual(new ValueCellVertex(43, 43)) - expect(mapping.getHeight(0)).toEqual(3) + expect(mapping.getCell(adr('A3'))).toEqual(new ValueCellVertex(43, 43)) + expect(mapping.getSheetHeight(0)).toEqual(3) }) it('addRows in the end of a mapping', () => { @@ -217,9 +217,9 @@ const sharedExamples = (builder: (width: number, height: number) => AddressMappi mapping.addRows(0, 1, 1) - expect(mapping.fetchCell(adr('A1'))).toEqual(new ValueCellVertex(42, 42)) + expect(mapping.getCell(adr('A1'))).toEqual(new ValueCellVertex(42, 42)) expect(mapping.getCell(adr('A2'))).toBe(undefined) - expect(mapping.getHeight(0)).toEqual(2) + expect(mapping.getSheetHeight(0)).toEqual(2) }) it('addRows more than one row', () => { @@ -230,12 +230,12 @@ const sharedExamples = (builder: (width: number, height: number) => AddressMappi mapping.addRows(0, 1, 3) - expect(mapping.fetchCell(adr('A1'))).toEqual(new ValueCellVertex(42, 42)) + expect(mapping.getCell(adr('A1'))).toEqual(new ValueCellVertex(42, 42)) expect(mapping.getCell(adr('A2'))).toBe(undefined) expect(mapping.getCell(adr('A3'))).toBe(undefined) expect(mapping.getCell(adr('A4'))).toBe(undefined) - expect(mapping.fetchCell(adr('A5'))).toEqual(new ValueCellVertex(43, 43)) - expect(mapping.getHeight(0)).toEqual(5) + expect(mapping.getCell(adr('A5'))).toEqual(new ValueCellVertex(43, 43)) + expect(mapping.getSheetHeight(0)).toEqual(5) }) it('addRows when more than one column present', () => { @@ -248,13 +248,13 @@ const sharedExamples = (builder: (width: number, height: number) => AddressMappi mapping.addRows(0, 1, 1) - expect(mapping.fetchCell(adr('A1'))).toEqual(new ValueCellVertex(11, 11)) - expect(mapping.fetchCell(adr('B1'))).toEqual(new ValueCellVertex(12, 12)) + expect(mapping.getCell(adr('A1'))).toEqual(new ValueCellVertex(11, 11)) + expect(mapping.getCell(adr('B1'))).toEqual(new ValueCellVertex(12, 12)) expect(mapping.getCell(adr('A2'))).toBe(undefined) expect(mapping.getCell(adr('B2'))).toBe(undefined) - expect(mapping.fetchCell(adr('A3'))).toEqual(new ValueCellVertex(21, 21)) - expect(mapping.fetchCell(adr('B3'))).toEqual(new ValueCellVertex(22, 22)) - expect(mapping.getHeight(0)).toEqual(3) + expect(mapping.getCell(adr('A3'))).toEqual(new ValueCellVertex(21, 21)) + expect(mapping.getCell(adr('B3'))).toEqual(new ValueCellVertex(22, 22)) + expect(mapping.getSheetHeight(0)).toEqual(3) }) it('removeRows - one row', () => { @@ -264,9 +264,9 @@ const sharedExamples = (builder: (width: number, height: number) => AddressMappi mapping.setCell(adr('A2'), new ValueCellVertex(21, 21)) mapping.setCell(adr('B2'), new ValueCellVertex(22, 22)) - expect(mapping.getHeight(0)).toBe(2) + expect(mapping.getSheetHeight(0)).toBe(2) mapping.removeRows(new RowsSpan(0, 0, 0)) - expect(mapping.getHeight(0)).toBe(1) + expect(mapping.getSheetHeight(0)).toBe(1) expect(mapping.getCellValue(adr('A1'))).toBe(21) expect(mapping.getCellValue(adr('B1'))).toBe(22) }) @@ -282,9 +282,9 @@ const sharedExamples = (builder: (width: number, height: number) => AddressMappi mapping.setCell(adr('A4'), new ValueCellVertex(41, 41)) mapping.setCell(adr('B4'), new ValueCellVertex(42, 42)) - expect(mapping.getHeight(0)).toBe(4) + expect(mapping.getSheetHeight(0)).toBe(4) mapping.removeRows(new RowsSpan(0, 1, 2)) - expect(mapping.getHeight(0)).toBe(2) + expect(mapping.getSheetHeight(0)).toBe(2) expect(mapping.getCellValue(adr('A1'))).toBe(11) expect(mapping.getCellValue(adr('A2'))).toBe(41) }) @@ -296,9 +296,9 @@ const sharedExamples = (builder: (width: number, height: number) => AddressMappi mapping.setCell(adr('A2'), new ValueCellVertex(21, 21)) mapping.setCell(adr('B2'), new ValueCellVertex(22, 22)) - expect(mapping.getHeight(0)).toBe(2) + expect(mapping.getSheetHeight(0)).toBe(2) mapping.removeRows(new RowsSpan(0, 0, 5)) - expect(mapping.getHeight(0)).toBe(0) + expect(mapping.getSheetHeight(0)).toBe(0) expect(mapping.has(adr('A1'))).toBe(false) }) @@ -311,7 +311,7 @@ const sharedExamples = (builder: (width: number, height: number) => AddressMappi mapping.removeRows(new RowsSpan(0, 1, 5)) - expect(mapping.getHeight(0)).toBe(1) + expect(mapping.getSheetHeight(0)).toBe(1) expect(mapping.has(adr('A1'))).toBe(true) expect(mapping.has(adr('A2'))).toBe(false) }) @@ -325,7 +325,7 @@ const sharedExamples = (builder: (width: number, height: number) => AddressMappi mapping.removeRows(new RowsSpan(0, 2, 3)) - expect(mapping.getHeight(0)).toBe(2) + expect(mapping.getSheetHeight(0)).toBe(2) expect(mapping.has(adr('A1'))).toBe(true) expect(mapping.has(adr('A2'))).toBe(true) }) @@ -341,9 +341,9 @@ const sharedExamples = (builder: (width: number, height: number) => AddressMappi mapping.setCell(adr('D1'), new ValueCellVertex(41, 41)) mapping.setCell(adr('D2'), new ValueCellVertex(42, 42)) - expect(mapping.getWidth(0)).toBe(4) + expect(mapping.getSheetWidth(0)).toBe(4) mapping.removeColumns(new ColumnsSpan(0, 1, 2)) - expect(mapping.getWidth(0)).toBe(2) + expect(mapping.getSheetWidth(0)).toBe(2) expect(mapping.getCellValue(adr('A1'))).toBe(11) expect(mapping.getCellValue(adr('B1'))).toBe(41) }) @@ -355,9 +355,9 @@ const sharedExamples = (builder: (width: number, height: number) => AddressMappi mapping.setCell(adr('A2'), new ValueCellVertex(21, 21)) mapping.setCell(adr('B2'), new ValueCellVertex(22, 22)) - expect(mapping.getHeight(0)).toBe(2) + expect(mapping.getSheetHeight(0)).toBe(2) mapping.removeColumns(new ColumnsSpan(0, 0, 5)) - expect(mapping.getWidth(0)).toBe(0) + expect(mapping.getSheetWidth(0)).toBe(0) expect(mapping.has(adr('A1'))).toBe(false) }) @@ -370,7 +370,7 @@ const sharedExamples = (builder: (width: number, height: number) => AddressMappi mapping.removeColumns(new ColumnsSpan(0, 1, 5)) - expect(mapping.getWidth(0)).toBe(1) + expect(mapping.getSheetWidth(0)).toBe(1) expect(mapping.has(adr('A1'))).toBe(true) expect(mapping.has(adr('B1'))).toBe(false) }) @@ -384,7 +384,7 @@ const sharedExamples = (builder: (width: number, height: number) => AddressMappi mapping.removeColumns(new ColumnsSpan(0, 2, 3)) - expect(mapping.getWidth(0)).toBe(2) + expect(mapping.getSheetWidth(0)).toBe(2) expect(mapping.has(adr('A1'))).toBe(true) expect(mapping.has(adr('B1'))).toBe(true) }) @@ -392,13 +392,13 @@ const sharedExamples = (builder: (width: number, height: number) => AddressMappi it('should expand columns when adding cell', () => { const mapping = builder(2, 2) mapping.setCell(adr('C1'), new EmptyCellVertex()) - expect(mapping.getWidth(0)).toBe(3) + expect(mapping.getSheetWidth(0)).toBe(3) }) it('should expand rows when adding cell', () => { const mapping = builder(2, 2) mapping.setCell(adr('A3'), new EmptyCellVertex()) - expect(mapping.getHeight(0)).toBe(3) + expect(mapping.getSheetHeight(0)).toBe(3) }) it('should move cell from source to destination', () => { @@ -425,11 +425,14 @@ const sharedExamples = (builder: (width: number, height: number) => AddressMappi expect(() => mapping.moveCell(adr('A1'), adr('A2'))).toThrowError('Cannot move cell. Destination already occupied.') }) - it('should throw error when trying to move vertices between sheets', () => { + it('should move vertices between sheets', () => { const mapping = builder(1, 2) mapping.setCell(adr('A1', 0), new ValueCellVertex(42, 42)) - expect(() => mapping.moveCell(adr('A1', 0), adr('A2', 1))).toThrowError('Cannot move cells between sheets.') + mapping.moveCell(adr('A1', 0), adr('A2', 1)) + + expect(mapping.has(adr('A1', 0))).toEqual(false) + expect(mapping.has(adr('A2', 1))).toEqual(true) }) it('should throw error when trying to move vertices in non-existing sheet', () => { @@ -441,10 +444,10 @@ const sharedExamples = (builder: (width: number, height: number) => AddressMappi it('entriesFromColumnsSpan returns the same result regardless of the strategy', () => { const denseMapping = new AddressMapping(new AlwaysDense()) - denseMapping.addSheet(0, new DenseStrategy(5, 5)) + denseMapping.addSheetWithStrategy(0, new DenseStrategy(5, 5)) const sparseMapping = new AddressMapping(new AlwaysSparse()) - sparseMapping.addSheet(0, new SparseStrategy(5, 5)) + sparseMapping.addSheetWithStrategy(0, new SparseStrategy(5, 5)) const mappingsAndResults: { mapping: AddressMapping, results: String[][] }[] = [ { @@ -487,25 +490,25 @@ const sharedExamples = (builder: (width: number, height: number) => AddressMappi describe('SparseStrategy', () => { sharedExamples((maxCol: number, maxRow: number) => { const mapping = new AddressMapping(new AlwaysSparse()) - mapping.addSheet(0, new SparseStrategy(maxCol, maxRow)) - mapping.addSheet(1, new SparseStrategy(maxCol, maxRow)) + mapping.addSheetWithStrategy(0, new SparseStrategy(maxCol, maxRow)) + mapping.addSheetWithStrategy(1, new SparseStrategy(maxCol, maxRow)) return mapping }) it('returns maximum row/col for simplest case', () => { const mapping = new AddressMapping(new AlwaysSparse()) - mapping.addSheet(0, new SparseStrategy(4, 16)) + mapping.addSheetWithStrategy(0, new SparseStrategy(4, 16)) mapping.setCell(adr('D16'), new ValueCellVertex(42, 42)) - expect(mapping.getHeight(0)).toEqual(16) - expect(mapping.getWidth(0)).toEqual(4) + expect(mapping.getSheetHeight(0)).toEqual(16) + expect(mapping.getSheetWidth(0)).toEqual(4) }) it('get all vertices', () => { const mapping = new AddressMapping(new AlwaysSparse()) const sparseStrategy = new SparseStrategy(3, 3) - mapping.addSheet(0, sparseStrategy) + mapping.addSheetWithStrategy(0, sparseStrategy) mapping.setCell(adr('A1', 0), new ValueCellVertex(42, 42)) mapping.setCell(adr('A2', 0), new ValueCellVertex(43, 43)) @@ -530,7 +533,7 @@ describe('SparseStrategy', () => { it('get all vertices - from column', () => { const mapping = new AddressMapping(new AlwaysSparse()) const sparseStrategy = new SparseStrategy(3, 3) - mapping.addSheet(0, sparseStrategy) + mapping.addSheetWithStrategy(0, sparseStrategy) mapping.setCell(adr('A1', 0), new ValueCellVertex(42, 42)) mapping.setCell(adr('A2', 0), new ValueCellVertex(43, 43)) @@ -559,7 +562,7 @@ describe('SparseStrategy', () => { it('get all vertices - from row', () => { const mapping = new AddressMapping(new AlwaysSparse()) const sparseStrategy = new SparseStrategy(3, 3) - mapping.addSheet(0, sparseStrategy) + mapping.addSheetWithStrategy(0, sparseStrategy) mapping.setCell(adr('A1', 0), new ValueCellVertex(42, 42)) mapping.setCell(adr('A2', 0), new ValueCellVertex(43, 43)) @@ -589,23 +592,23 @@ describe('SparseStrategy', () => { describe('DenseStrategy', () => { sharedExamples((maxCol, maxRow) => { const mapping = new AddressMapping(new AlwaysDense()) - mapping.addSheet(0, new DenseStrategy(maxCol, maxRow)) - mapping.addSheet(1, new DenseStrategy(maxCol, maxRow)) + mapping.addSheetWithStrategy(0, new DenseStrategy(maxCol, maxRow)) + mapping.addSheetWithStrategy(1, new DenseStrategy(maxCol, maxRow)) return mapping }) it('returns maximum row/col for simplest case', () => { const mapping = new AddressMapping(new AlwaysDense()) - mapping.addSheet(0, new DenseStrategy(1, 2)) + mapping.addSheetWithStrategy(0, new DenseStrategy(1, 2)) - expect(mapping.getHeight(0)).toEqual(2) - expect(mapping.getWidth(0)).toEqual(1) + expect(mapping.getSheetHeight(0)).toEqual(2) + expect(mapping.getSheetWidth(0)).toEqual(1) }) it('get all vertices', () => { const mapping = new AddressMapping(new AlwaysDense()) const denseStratgey = new DenseStrategy(3, 3) - mapping.addSheet(0, denseStratgey) + mapping.addSheetWithStrategy(0, denseStratgey) mapping.setCell(adr('A1', 0), new ValueCellVertex(42, 42)) mapping.setCell(adr('A2', 0), new ValueCellVertex(43, 43)) @@ -630,7 +633,7 @@ describe('DenseStrategy', () => { it('get all vertices - from column', () => { const mapping = new AddressMapping(new AlwaysDense()) const denseStratgey = new DenseStrategy(3, 3) - mapping.addSheet(0, denseStratgey) + mapping.addSheetWithStrategy(0, denseStratgey) mapping.setCell(adr('A1', 0), new ValueCellVertex(42, 42)) mapping.setCell(adr('A2', 0), new ValueCellVertex(43, 43)) @@ -659,7 +662,7 @@ describe('DenseStrategy', () => { it('get all vertices - from row', () => { const mapping = new AddressMapping(new AlwaysDense()) const denseStratgey = new DenseStrategy(3, 3) - mapping.addSheet(0, denseStratgey) + mapping.addSheetWithStrategy(0, denseStratgey) mapping.setCell(adr('A1', 0), new ValueCellVertex(42, 42)) mapping.setCell(adr('A2', 0), new ValueCellVertex(43, 43)) @@ -693,9 +696,9 @@ describe('AddressMapping', () => { [null, null, null], [null, null, '1'], ] - addressMapping.autoAddSheet(0, findBoundaries(sheet)) + addressMapping.addSheetAndSetStrategyBasedOnBoundaries(0, findBoundaries(sheet)) - expect(addressMapping.strategyFor(0)).toBeInstanceOf(SparseStrategy) + expect(addressMapping.getStrategyForSheetOrThrow(0)).toBeInstanceOf(SparseStrategy) }) it('#buildAddresMapping - when dense matrix', () => { @@ -704,8 +707,8 @@ describe('AddressMapping', () => { ['1', '1'], ['1', '1'], ] - addressMapping.autoAddSheet(0, findBoundaries(sheet)) + addressMapping.addSheetAndSetStrategyBasedOnBoundaries(0, findBoundaries(sheet)) - expect(addressMapping.strategyFor(0)).toBeInstanceOf(DenseStrategy) + expect(addressMapping.getStrategyForSheetOrThrow(0)).toBeInstanceOf(DenseStrategy) }) }) diff --git a/test/unit/arrays.spec.ts b/test/unit/arrays.spec.ts index 2b2af2461..71d29cc94 100644 --- a/test/unit/arrays.spec.ts +++ b/test/unit/arrays.spec.ts @@ -1,6 +1,6 @@ import {ErrorType, HyperFormula} from '../../src' import {ArraySize} from '../../src/ArraySize' -import {ArrayVertex, ValueCellVertex} from '../../src/DependencyGraph' +import {ArrayFormulaVertex, ValueCellVertex} from '../../src/DependencyGraph' import {ErrorMessage} from '../../src/error-message' import {adr, detailedError, detailedErrorWithOrigin, expectVerticesOfTypes, noSpace} from './testUtils' @@ -363,8 +363,8 @@ describe('build from array', () => { ], {useArrayArithmetic: true}) expectVerticesOfTypes(engine, [ - [ArrayVertex, ArrayVertex], - [ArrayVertex, ArrayVertex], + [ArrayFormulaVertex, ArrayFormulaVertex], + [ArrayFormulaVertex, ArrayFormulaVertex], ]) }) @@ -375,12 +375,11 @@ describe('build from array', () => { ], {useArrayArithmetic: true}) expectVerticesOfTypes(engine, [ - [ArrayVertex, ArrayVertex, undefined], - [ArrayVertex, ArrayVertex, ArrayVertex], - [undefined, ArrayVertex, ArrayVertex], + [ArrayFormulaVertex, ArrayFormulaVertex, undefined], + [ArrayFormulaVertex, ArrayFormulaVertex, ArrayFormulaVertex], + [undefined, ArrayFormulaVertex, ArrayFormulaVertex], ]) expect(engine.arrayMapping.arrayMapping.size).toEqual(4) - expect(engine.getSheetValues(0)) }) it('should REF last array', () => { @@ -390,8 +389,8 @@ describe('build from array', () => { ], {useArrayArithmetic: true}) expectVerticesOfTypes(engine, [ - [ArrayVertex, ArrayVertex, ArrayVertex], - [ArrayVertex, ArrayVertex, ArrayVertex], + [ArrayFormulaVertex, ArrayFormulaVertex, ArrayFormulaVertex], + [ArrayFormulaVertex, ArrayFormulaVertex, ArrayFormulaVertex], [undefined, undefined], ]) expect(engine.getSheetValues(0)).toEqual([ @@ -399,7 +398,6 @@ describe('build from array', () => { [noSpace(), 2, 2, 1, 2], ]) expect(engine.arrayMapping.arrayMapping.size).toEqual(3) - expect(engine.getSheetValues(0)) }) it('array should work with different types of data', () => { @@ -433,8 +431,8 @@ describe('build from array', () => { ], {useArrayArithmetic: true}) expectVerticesOfTypes(engine, [ - [ArrayVertex, ArrayVertex], - [ArrayVertex, ArrayVertex], + [ArrayFormulaVertex, ArrayFormulaVertex], + [ArrayFormulaVertex, ArrayFormulaVertex], ]) }) @@ -447,7 +445,7 @@ describe('build from array', () => { expect(engine.arrayMapping.getArrayByCorner(adr('A1'))?.array.size).toEqual(ArraySize.error()) expectVerticesOfTypes(engine, [ - [ArrayVertex, undefined], + [ArrayFormulaVertex, undefined], [ValueCellVertex, undefined], ]) }) @@ -490,15 +488,6 @@ describe('column ranges', () => { expect(engine.getCellValue(adr('A2'))).toEqualError(detailedError(ErrorType.SPILL, ErrorMessage.NoSpaceForArrayResult)) }) - it('arithmetic should work for row range', () => { - const engine = HyperFormula.buildFromArray([ - ['=2*(2:2)', null], - [1, 2], - ], {useArrayArithmetic: true}) - - expect(engine.getSheetValues(0)).toEqual([[2, 4], [1, 2]]) - }) - it('arithmetic for shifted row range -- error', () => { const engine = HyperFormula.buildFromArray([ [null, '=2*(2:2)'], @@ -518,4 +507,31 @@ describe('column ranges', () => { expect(engine.getCellValue(adr('D1'))).toEqual(3) }) + + it('should handle array shrinking when dependent is a value cell', () => { + const engine = HyperFormula.buildFromArray([ + [1, 2], + [3, 4], + ['=TRANSPOSE(A1:B2)'], + ], {useArrayArithmetic: true}) + + expect(engine.getCellValue(adr('A3'))).toBe(1) + expect(engine.getCellValue(adr('A4'))).toBe(2) + expect(engine.getCellValue(adr('B3'))).toBe(3) + expect(engine.getCellValue(adr('B4'))).toBe(4) + + engine.setCellContents(adr('B3'), 'obstructing value') + + expect(engine.getCellValue(adr('A3'))).toEqualError(detailedError(ErrorType.SPILL, ErrorMessage.NoSpaceForArrayResult)) + }) + + it('should correctly set address mapping for scalar formula', () => { + const engine = HyperFormula.buildFromArray([ + ['=1+1'], + ]) + + expect(engine.getCellValue(adr('A1'))).toBe(2) + expect(engine.addressMapping.getCell(adr('A1'))).toBeDefined() + expect(engine.addressMapping.getCell(adr('B1'))).toBeUndefined() + }) }) diff --git a/test/unit/build-engine.spec.ts b/test/unit/build-engine.spec.ts index cf688447f..0e16c9448 100644 --- a/test/unit/build-engine.spec.ts +++ b/test/unit/build-engine.spec.ts @@ -56,14 +56,16 @@ describe('Building engine from arrays', () => { }) it('should allow to create sheets with a delay', () => { - const engine1 = HyperFormula.buildFromArray([['=Sheet2!A1']]) + const sheetName = 'Sheet2' + const engine = HyperFormula.buildFromArray([[`=${sheetName}!A1`]]) - engine1.addSheet('Sheet2') - engine1.setSheetContent(1, [['1']]) - engine1.rebuildAndRecalculate() + engine.addSheet(sheetName) + const sheetId = engine.getSheetId(sheetName)! + engine.setSheetContent(sheetId, [['1']]) + engine.rebuildAndRecalculate() - expect(engine1.getCellValue(adr('A1', 1))).toBe(1) - expect(engine1.getCellValue(adr('A1', 0))).toBe(1) + expect(engine.getCellValue(adr('A1', sheetId))).toBe(1) + expect(engine.getCellValue(adr('A1', 0))).toBe(1) }) it('corrupted sheet definition', () => { diff --git a/test/unit/column-index.spec.ts b/test/unit/column-index.spec.ts index a48d6136e..b82f81fe5 100644 --- a/test/unit/column-index.spec.ts +++ b/test/unit/column-index.spec.ts @@ -14,7 +14,8 @@ import {ColumnIndex} from '../../src/Lookup/ColumnIndex' import {NamedExpressions} from '../../src/NamedExpressions' import {ColumnsSpan, RowsSpan} from '../../src/Span' import {Statistics} from '../../src/statistics' -import {adr, expectColumnIndexToMatchSheet} from './testUtils' +import {adr, detailedError, expectColumnIndexToMatchSheet} from './testUtils' +import { ErrorMessage } from '../../src/error-message' function buildEmptyIndex(transformingService: LazilyTransformingAstService, config: Config, statistics: Statistics): ColumnIndex { const functionRegistry = new FunctionRegistry(config) @@ -34,7 +35,7 @@ describe('ColumnIndex#add', () => { const columnMap = index.getColumnMap(0, 1) expect(columnMap.size).toBe(1) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + expect(columnMap.get(1)!.index[0]).toBe(4) }) @@ -47,7 +48,7 @@ describe('ColumnIndex#add', () => { const columnMap = index.getColumnMap(0, 0) expect(columnMap.size).toBe(1) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + expect(columnMap.get(1)!.index[0]).toBe(0) }) @@ -62,7 +63,7 @@ describe('ColumnIndex#add', () => { const columnMap = index.getColumnMap(0, 0) expect(columnMap.size).toBe(1) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + expect(columnMap.get(1)!.index.length).toBe(2) }) @@ -107,13 +108,11 @@ describe('ColumnIndex#add', () => { const columnMap = index.getColumnMap(0, 0) - // eslint-disable @typescript-eslint/no-non-null-assertion expect(columnMap.get('a')!.index.length).toBe(3) expect(columnMap.get('l')!.index.length).toBe(1) expect(columnMap.get('ł')!.index.length).toBe(1) expect(columnMap.get('t')!.index.length).toBe(1) expect(columnMap.get('ŧ')!.index.length).toBe(1) - // eslint-enable @typescript-eslint/no-non-null-assertion }) it('should ignore EmptyValue', () => { @@ -135,7 +134,7 @@ describe('ColumnIndex change/remove', () => { index.remove(1, adr('A2')) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const valueIndex = index.getColumnMap(0, 0).get(1)! expect(valueIndex.index.length).toBe(2) expect(valueIndex.index).toContain(0) @@ -150,7 +149,7 @@ describe('ColumnIndex change/remove', () => { index.remove(undefined, adr('A2')) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const valueIndex = index.getColumnMap(0, 0).get(1)! expect(valueIndex.index.length).toBe(3) expect(valueIndex.index).toContain(0) @@ -165,7 +164,7 @@ describe('ColumnIndex change/remove', () => { index.change(1, 2, adr('A1')) expect(index.getColumnMap(0, 0).keys()).not.toContain(1) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const valueIndex = index.getColumnMap(0, 0).get(2)! expect(valueIndex.index).toContain(0) }) @@ -296,7 +295,7 @@ describe('ColumnIndex#removeColumns', () => { expect(index.getValueIndex(0, 0, 1).index).toEqual([]) }) - it('should remove multiple columns in the middle ', () => { + it('should remove multiple columns in the middle', () => { const index = buildEmptyIndex(transformingService, new Config(), statistics) index.add(1, adr('A1')) index.add(2, adr('B1')) @@ -311,7 +310,7 @@ describe('ColumnIndex#removeColumns', () => { expect(index.getValueIndex(0, 3, 4).index).toEqual([]) }) - it('should remove columns only in one sheet ', () => { + it('should remove columns only in one sheet', () => { const index = buildEmptyIndex(transformingService, new Config(), statistics) index.add(1, adr('A1', 0)) index.add(1, adr('A1', 1)) @@ -455,7 +454,7 @@ describe('ColumnIndex#removeRows', () => { expect(index.getValueIndex(0, 0, 1).index).toEqual([]) }) - it('should remove rows in the middle ', () => { + it('should remove rows in the middle', () => { const statistics = new Statistics() const transformingService = new LazilyTransformingAstService(statistics) const index = buildEmptyIndex(transformingService, new Config(), statistics) @@ -600,6 +599,8 @@ describe('Arrays', () => { engine.setCellContents(adr('D1'), [['foo']]) + expect(engine.getCellValue(adr('C1'))).toEqualError(detailedError(ErrorType.SPILL, ErrorMessage.NoSpaceForArrayResult)) + expectColumnIndexToMatchSheet([ [1, 2, null, 'foo'] ], engine) diff --git a/test/unit/column-range.spec.ts b/test/unit/column-range.spec.ts index 7848035f1..a538063c5 100644 --- a/test/unit/column-range.spec.ts +++ b/test/unit/column-range.spec.ts @@ -17,7 +17,7 @@ describe('Column ranges', () => { ['=SUM(C:D)', '=SUM(C5:D6)'], ]) - const cd = engine.rangeMapping.getRange(colStart('C'), colEnd('D')) as RangeVertex + const cd = engine.rangeMapping.getRangeVertex(colStart('C'), colEnd('D')) as RangeVertex const c5 = engine.dependencyGraph.fetchCell(adr('C5')) const c6 = engine.dependencyGraph.fetchCell(adr('C6')) @@ -38,8 +38,8 @@ describe('Column ranges', () => { engine.setCellContents(adr('B1'), '=SUM(D42:H42)') - const ce = engine.rangeMapping.getRange(colStart('C'), colEnd('E')) as RangeVertex - const dg = engine.rangeMapping.getRange(colStart('D'), colEnd('G')) as RangeVertex + const ce = engine.rangeMapping.getRangeVertex(colStart('C'), colEnd('E')) as RangeVertex + const dg = engine.rangeMapping.getRangeVertex(colStart('D'), colEnd('G')) as RangeVertex const d42 = engine.dependencyGraph.fetchCell(adr('D42')) const e42 = engine.dependencyGraph.fetchCell(adr('E42')) @@ -81,4 +81,17 @@ describe('Column ranges', () => { expect(range.start).toEqual(colStart('A')) expect(range.end).toEqual(colEnd('B')) }) + + it('should correctly handle infinite column ranges when setting cell values (line 890)', () => { + const engine = HyperFormula.buildFromArray([ + ['=SUM(C:D)'], + ]) + + expect(engine.getCellValue(adr('A1'))).toBe(0) + + engine.setCellContents(adr('C5'), 10) + engine.setCellContents(adr('D5'), 20) + + expect(engine.getCellValue(adr('A1'))).toBe(30) + }) }) diff --git a/test/unit/computation-suspension.spec.ts b/test/unit/computation-suspension.spec.ts index ebb94030e..836e79137 100644 --- a/test/unit/computation-suspension.spec.ts +++ b/test/unit/computation-suspension.spec.ts @@ -206,4 +206,22 @@ describe('Evaluation suspension', () => { expect(changes).toContainEqual(new ExportedCellChange(adr('B1'), 4)) expect(changes).toContainEqual(new ExportedCellChange(adr('C1'), 5)) }) + + it('allows to update cell content of a not computed formula cell (#1194)', () => { + const hf = HyperFormula.buildFromArray([], { + licenseKey: 'gpl-v3' + }) + + hf.suspendEvaluation() + hf.setCellContents(adr('A1'), '=42') + hf.setCellContents(adr('A1'), 42) + hf.setCellContents(adr('A1'), '=42') + hf.setCellContents(adr('A1'), null) + hf.setCellContents(adr('A1'), '=42') + hf.setCellContents(adr('A1'), '==') + hf.setCellContents(adr('A1'), '=42') + hf.setCellContents(adr('A1'), '=42') + hf.resumeEvaluation() + expect(hf.getCellValue(adr('A1'))).toBe(42) + }) }) diff --git a/test/unit/crud-random.spec.ts b/test/unit/crud-random.spec.ts index 94eb9d091..cdea9913c 100644 --- a/test/unit/crud-random.spec.ts +++ b/test/unit/crud-random.spec.ts @@ -272,6 +272,6 @@ describe('large psuedo-random test', () => { } randomCleanup(engine, rectangleFromCorner({x: 0, y: 0}, 2 * (n + 1) * sideX, 2 * sideY)) expect(engine.dependencyGraph.graph.getNodes().length).toBe(0) - expect(engine.dependencyGraph.rangeMapping.getMappingSize(0)).toBe(0) + expect(engine.dependencyGraph.rangeMapping.getNumberOfRangesInSheet(0)).toBe(0) }) }) diff --git a/test/unit/cruds/adding-columns-dependencies.spec.ts b/test/unit/cruds/adding-columns-dependencies.spec.ts index 0d7739fe1..17a243405 100644 --- a/test/unit/cruds/adding-columns-dependencies.spec.ts +++ b/test/unit/cruds/adding-columns-dependencies.spec.ts @@ -251,12 +251,12 @@ describe('Adding column, fixing ranges', () => { ['=SUM(A1:C1)'], ]) - expect(engine.rangeMapping.getRange(adr('A1'), adr('C1'))).not.toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(adr('A1'), adr('C1'))).not.toBe(undefined) engine.addColumns(0, [1, 1]) - expect(engine.rangeMapping.getRange(adr('A1'), adr('C1'))).toBe(undefined) - expect(engine.rangeMapping.getRange(adr('A1'), adr('D1'))).not.toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(adr('A1'), adr('C1'))).toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(adr('A1'), adr('D1'))).not.toBe(undefined) expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([ [null, null, null, null], @@ -270,12 +270,12 @@ describe('Adding column, fixing ranges', () => { ['=SUM(A1:C1)'], ]) - expect(engine.rangeMapping.getRange(adr('A1'), adr('C1'))).not.toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(adr('A1'), adr('C1'))).not.toBe(undefined) engine.addColumns(0, [1, 1]) - expect(engine.rangeMapping.getRange(adr('A1'), adr('C1'))).toBe(undefined) - expect(engine.rangeMapping.getRange(adr('A1'), adr('D1'))).not.toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(adr('A1'), adr('C1'))).toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(adr('A1'), adr('D1'))).not.toBe(undefined) expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([ ['1', null, '2', '3'], @@ -289,10 +289,10 @@ describe('Adding column, fixing ranges', () => { ['=SUM(A1:C1)'], ]) - expect(engine.rangeMapping.getRange(adr('A1'), adr('C1'))).not.toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(adr('A1'), adr('C1'))).not.toBe(undefined) engine.addColumns(0, [0, 1]) - expect(engine.rangeMapping.getRange(adr('A1'), adr('C1'))).toBe(undefined) - expect(engine.rangeMapping.getRange(adr('B1'), adr('D1'))).not.toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(adr('A1'), adr('C1'))).toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(adr('B1'), adr('D1'))).not.toBe(undefined) expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([ [null, '1', '2', '3'], @@ -306,9 +306,9 @@ describe('Adding column, fixing ranges', () => { ['=SUM(A1:C1)'], ]) - expect(engine.rangeMapping.getRange(adr('A1'), adr('C1'))).not.toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(adr('A1'), adr('C1'))).not.toBe(undefined) engine.addColumns(0, [3, 1]) - expect(engine.rangeMapping.getRange(adr('A1'), adr('C1'))).not.toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(adr('A1'), adr('C1'))).not.toBe(undefined) expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([ ['1', '2', '3', null], @@ -316,7 +316,7 @@ describe('Adding column, fixing ranges', () => { ])) }) - it('it should insert new cell with edge to only one range at right', () => { + it('should insert new cell with edge to only one range at right', () => { const engine = HyperFormula.buildFromArray([ ['1', '2', /* */ '3', '4'], ['=SUM(A1:A1)', '=SUM(A1:B1)', /* */ '=SUM(A1:C1)', '=SUM(A1:D1)'], @@ -324,13 +324,13 @@ describe('Adding column, fixing ranges', () => { engine.addColumns(0, [2, 1]) - const c1 = engine.addressMapping.fetchCell(adr('C1')) - const a1d1 = engine.rangeMapping.fetchRange(adr('A1'), adr('D1')) - const a1e1 = engine.rangeMapping.fetchRange(adr('A1'), adr('E1')) + const c1 = engine.addressMapping.getCell(adr('C1')) + const a1d1 = engine.rangeMapping.getVertexOrThrow(adr('A1'), adr('D1')) + const a1e1 = engine.rangeMapping.getVertexOrThrow(adr('A1'), adr('E1')) - expect(engine.graph.existsEdge(c1, a1d1)).toBe(true) - expect(engine.graph.existsEdge(c1, a1e1)).toBe(true) - expect(engine.graph.adjacentNodesCount(c1)).toBe(2) + expect(engine.graph.existsEdge(c1!, a1d1)).toBe(true) + expect(engine.graph.existsEdge(c1!, a1e1)).toBe(true) + expect(engine.graph.adjacentNodesCount(c1!)).toBe(2) }) it('range start in column', () => { @@ -358,10 +358,10 @@ describe('Adding column, fixing ranges', () => { engine.addColumns(0, [1, 1]) - const b1 = engine.addressMapping.fetchCell(adr('B1')) - const range = engine.rangeMapping.fetchRange(adr('A1'), adr('E1')) + const b1 = engine.addressMapping.getCell(adr('B1')) + const range = engine.rangeMapping.getVertexOrThrow(adr('A1'), adr('E1')) expect(b1).toBeInstanceOf(EmptyCellVertex) - expect(engine.graph.existsEdge(b1, range)).toBe(true) + expect(engine.graph.existsEdge(b1!, range)).toBe(true) expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([ ['1', null, '2', '3', '4'], @@ -411,11 +411,11 @@ describe('Adding column, fixing ranges', () => { engine.addColumns(0, [1, 1]) - const b1 = engine.addressMapping.fetchCell(adr('B1')) + const b1 = engine.addressMapping.getCell(adr('B1')) - const range = engine.rangeMapping.fetchRange(adr('A1'), adr('C1')) + const range = engine.rangeMapping.getVertexOrThrow(adr('A1'), adr('C1')) expect(b1).toBeInstanceOf(EmptyCellVertex) - expect(engine.graph.existsEdge(b1, range)).toBe(true) + expect(engine.graph.existsEdge(b1!, range)).toBe(true) expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([ ['1', null, '2', '3', '4'], @@ -431,11 +431,11 @@ describe('Adding column, fixing ranges', () => { engine.addColumns(0, [1, 1]) - const b1 = engine.addressMapping.fetchCell(adr('B1')) + const b1 = engine.addressMapping.getCell(adr('B1')) - const range = engine.rangeMapping.fetchRange(adr('A1'), adr('D1')) + const range = engine.rangeMapping.getVertexOrThrow(adr('A1'), adr('D1')) expect(b1).toBeInstanceOf(EmptyCellVertex) - expect(engine.graph.existsEdge(b1, range)).toBe(true) + expect(engine.graph.existsEdge(b1!, range)).toBe(true) expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([ ['1', null, '2', '3', '4'], @@ -467,12 +467,12 @@ describe('Adding column, fixing column ranges', () => { ['1', /* new col */ '2', '3', '=SUM(A:C)'], ]) - expect(engine.rangeMapping.getRange(colStart('A'), colEnd('C'))).not.toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(colStart('A'), colEnd('C'))).not.toBe(undefined) engine.addColumns(0, [1, 1]) - expect(engine.rangeMapping.getRange(colStart('A'), colEnd('C'))).toBe(undefined) - expect(engine.rangeMapping.getRange(colStart('A'), colEnd('D'))).not.toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(colStart('A'), colEnd('C'))).toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(colStart('A'), colEnd('D'))).not.toBe(undefined) expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([ ['1', null, '2', '3', '=SUM(A:D)'], @@ -484,10 +484,10 @@ describe('Adding column, fixing column ranges', () => { [/* new col */ '1', '2', '3', '=SUM(A:C)'], ]) - expect(engine.rangeMapping.getRange(colStart('A'), colEnd('C'))).not.toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(colStart('A'), colEnd('C'))).not.toBe(undefined) engine.addColumns(0, [0, 1]) - expect(engine.rangeMapping.getRange(colStart('A'), colEnd('C'))).toBe(undefined) - expect(engine.rangeMapping.getRange(colStart('B'), colEnd('D'))).not.toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(colStart('A'), colEnd('C'))).toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(colStart('B'), colEnd('D'))).not.toBe(undefined) expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([ [null, '1', '2', '3', '=SUM(B:D)'], @@ -499,9 +499,9 @@ describe('Adding column, fixing column ranges', () => { ['1', '2', '3' /* new col */, '=SUM(A:C)'], ]) - expect(engine.rangeMapping.getRange(colStart('A'), colEnd('C'))).not.toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(colStart('A'), colEnd('C'))).not.toBe(undefined) engine.addColumns(0, [3, 1]) - expect(engine.rangeMapping.getRange(colStart('A'), colEnd('C'))).not.toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(colStart('A'), colEnd('C'))).not.toBe(undefined) expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([ ['1', '2', '3', null, '=SUM(A:C)'], @@ -519,7 +519,7 @@ describe('Adding column, row range', () => { engine.addColumns(0, [1, 1]) - expect(engine.rangeMapping.getRange(rowStart(1), rowEnd(2))).not.toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(rowStart(1), rowEnd(2))).not.toBe(undefined) const rowRange = extractRowRange(engine, adr('D3')) expect(rowRange.start).toEqual(rowStart(1)) expect(rowRange.end).toEqual(rowEnd(2)) diff --git a/test/unit/cruds/adding-columns.spec.ts b/test/unit/cruds/adding-columns.spec.ts index 0a2f8a12a..feded1182 100644 --- a/test/unit/cruds/adding-columns.spec.ts +++ b/test/unit/cruds/adding-columns.spec.ts @@ -7,7 +7,7 @@ import { } from '../../../src' import {AbsoluteCellRange} from '../../../src/AbsoluteCellRange' import {Config} from '../../../src/Config' -import {ArrayVertex, FormulaCellVertex} from '../../../src/DependencyGraph' +import {ArrayFormulaVertex, ScalarFormulaVertex} from '../../../src/DependencyGraph' import {ColumnIndex} from '../../../src/Lookup/ColumnIndex' import { adr, @@ -233,7 +233,7 @@ describe('Adding column - reevaluation', () => { }) }) -describe('Adding column - FormulaCellVertex#address update', () => { +describe('Adding column - ScalarFormulaVertex#address update', () => { it('updates addresses in formulas', () => { const engine = HyperFormula.buildFromArray([ ['1', /* new col */ '=A1'], @@ -241,8 +241,8 @@ describe('Adding column - FormulaCellVertex#address update', () => { engine.addColumns(0, [1, 1]) - const c1 = engine.addressMapping.getCell(adr('C1')) as FormulaCellVertex - expect(c1).toBeInstanceOf(FormulaCellVertex) + const c1 = engine.addressMapping.getCell(adr('C1')) as ScalarFormulaVertex + expect(c1).toBeInstanceOf(ScalarFormulaVertex) expect(c1.getAddress(engine.lazilyTransformingAstService)).toEqual(adr('C1')) }) }) @@ -275,7 +275,7 @@ describe('different sheet', () => { engine.addColumns(0, [0, 1]) - const formulaVertex = engine.addressMapping.fetchCell(adr('A1', 1)) as FormulaCellVertex + const formulaVertex = engine.addressMapping.getCell(adr('A1', 1)) as ScalarFormulaVertex expect(formulaVertex.getAddress(engine.lazilyTransformingAstService)).toEqual(adr('A1', 1)) formulaVertex.getFormula(engine.lazilyTransformingAstService) // force transformations to be applied @@ -388,7 +388,7 @@ describe('Adding column - arrays', () => { ], {useArrayArithmetic: true})) }) - it('ArrayVertex#formula should be updated', () => { + it('ArrayFormulaVertex#formula should be updated', () => { const engine = HyperFormula.buildFromArray([ [1, 2, '=TRANSPOSE(A1:B2)'], [3, 4], @@ -399,7 +399,7 @@ describe('Adding column - arrays', () => { expect(extractMatrixRange(engine, adr('D1'))).toEqual(new AbsoluteCellRange(adr('A1'), adr('C2'))) }) - it('ArrayVertex#formula should be updated when different sheets', () => { + it('ArrayFormulaVertex#formula should be updated when different sheets', () => { const engine = HyperFormula.buildFromSheets({ Sheet1: [ ['1', '2'], @@ -415,7 +415,7 @@ describe('Adding column - arrays', () => { expect(extractMatrixRange(engine, adr('A1', 1))).toEqual(new AbsoluteCellRange(adr('A1'), adr('C2'))) }) - it('ArrayVertex#address should be updated', () => { + it('ArrayFormulaVertex#address should be updated', () => { const engine = HyperFormula.buildFromArray([ [1, 2, '=TRANSPOSE(A1:B2)'], [3, 4], @@ -423,7 +423,7 @@ describe('Adding column - arrays', () => { engine.addColumns(0, [1, 1]) - const matrixVertex = engine.addressMapping.fetchCell(adr('D1')) as ArrayVertex + const matrixVertex = engine.addressMapping.getCell(adr('D1')) as ArrayFormulaVertex expect(matrixVertex.getAddress(engine.lazilyTransformingAstService)).toEqual(adr('D1')) }) }) diff --git a/test/unit/cruds/adding-row-dependencies.spec.ts b/test/unit/cruds/adding-row-dependencies.spec.ts index cb8a109a5..812c53dc7 100644 --- a/test/unit/cruds/adding-row-dependencies.spec.ts +++ b/test/unit/cruds/adding-row-dependencies.spec.ts @@ -278,10 +278,10 @@ describe('Adding row, ranges', () => { ['3', null], ]) - expect(engine.rangeMapping.getRange(adr('A1'), adr('A3'))).not.toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(adr('A1'), adr('A3'))).not.toBe(undefined) engine.addRows(0, [1, 1]) - expect(engine.rangeMapping.getRange(adr('A1'), adr('A3'))).toBe(undefined) - expect(engine.rangeMapping.getRange(adr('A1'), adr('A4'))).not.toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(adr('A1'), adr('A3'))).toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(adr('A1'), adr('A4'))).not.toBe(undefined) expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([ ['1', '=SUM(A1:A4)'], @@ -299,10 +299,10 @@ describe('Adding row, ranges', () => { ['3', null], ]) - expect(engine.rangeMapping.getRange(adr('A1'), adr('A3'))).not.toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(adr('A1'), adr('A3'))).not.toBe(undefined) engine.addRows(0, [0, 1]) - expect(engine.rangeMapping.getRange(adr('A1'), adr('A3'))).toBe(undefined) - expect(engine.rangeMapping.getRange(adr('A2'), adr('A4'))).not.toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(adr('A1'), adr('A3'))).toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(adr('A2'), adr('A4'))).not.toBe(undefined) expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([ [null, null], @@ -320,9 +320,9 @@ describe('Adding row, ranges', () => { // new row ]) - expect(engine.rangeMapping.getRange(adr('A1'), adr('A3'))).not.toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(adr('A1'), adr('A3'))).not.toBe(undefined) engine.addRows(0, [3, 1]) - expect(engine.rangeMapping.getRange(adr('A1'), adr('A3'))).not.toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(adr('A1'), adr('A3'))).not.toBe(undefined) expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([ ['1', '=SUM(A1:A3)'], @@ -332,7 +332,7 @@ describe('Adding row, ranges', () => { ])) }) - it('it should insert new cell with edge to all ranges below', () => { + it('should insert new cell with edge to all ranges below', () => { const engine = HyperFormula.buildFromArray([ ['1', '=SUM(A1:A1)'], ['2', '=SUM(A1:A2)'], @@ -343,22 +343,22 @@ describe('Adding row, ranges', () => { engine.addRows(0, [2, 1]) - const a4 = engine.addressMapping.fetchCell(adr('A4')) - const a3 = engine.addressMapping.fetchCell(adr('A3')) - const a2 = engine.addressMapping.fetchCell(adr('A2')) - const a1a4 = engine.rangeMapping.fetchRange(adr('A1'), adr('A4')) // A1:A4 - const a1a3 = engine.rangeMapping.fetchRange(adr('A1'), adr('A3')) // A1:A4 - const a1a2 = engine.rangeMapping.fetchRange(adr('A1'), adr('A2')) // A1:A4 - - expect(engine.graph.existsEdge(a4, a1a4)).toBe(true) - expect(engine.graph.existsEdge(a3, a1a3)).toBe(true) - expect(engine.graph.existsEdge(a2, a1a2)).toBe(true) - expect(engine.graph.adjacentNodesCount(a4)).toBe(1) - expect(engine.graph.adjacentNodesCount(a3)).toBe(1) - expect(engine.graph.adjacentNodesCount(a2)).toBe(1) + const a4 = engine.addressMapping.getCell(adr('A4')) + const a3 = engine.addressMapping.getCell(adr('A3')) + const a2 = engine.addressMapping.getCell(adr('A2')) + const a1a4 = engine.rangeMapping.getVertexOrThrow(adr('A1'), adr('A4')) // A1:A4 + const a1a3 = engine.rangeMapping.getVertexOrThrow(adr('A1'), adr('A3')) // A1:A4 + const a1a2 = engine.rangeMapping.getVertexOrThrow(adr('A1'), adr('A2')) // A1:A4 + + expect(engine.graph.existsEdge(a4!, a1a4)).toBe(true) + expect(engine.graph.existsEdge(a3!, a1a3)).toBe(true) + expect(engine.graph.existsEdge(a2!, a1a2)).toBe(true) + expect(engine.graph.adjacentNodesCount(a4!)).toBe(1) + expect(engine.graph.adjacentNodesCount(a3!)).toBe(1) + expect(engine.graph.adjacentNodesCount(a2!)).toBe(1) }) - it('it should insert new cell with edge to only one range below, shifted by 1', () => { + it('should insert new cell with edge to only one range below, shifted by 1', () => { const engine = HyperFormula.buildFromArray([ ['1', null], ['2', '=SUM(A1:A1)'], @@ -415,10 +415,10 @@ describe('Adding row, ranges', () => { engine.addRows(0, [1, 1]) - const a2 = engine.addressMapping.fetchCell(adr('A2')) - const range = engine.rangeMapping.fetchRange(adr('A1'), adr('A5')) + const a2 = engine.addressMapping.getCell(adr('A2')) + const range = engine.rangeMapping.getVertexOrThrow(adr('A1'), adr('A5')) expect(a2).toBeInstanceOf(EmptyCellVertex) - expect(engine.graph.existsEdge(a2, range)).toBe(true) + expect(engine.graph.existsEdge(a2!, range)).toBe(true) expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([ ['1', null], @@ -486,11 +486,11 @@ describe('Adding row, ranges', () => { engine.addRows(0, [1, 1]) - const a2 = engine.addressMapping.fetchCell(adr('A2')) + const a2 = engine.addressMapping.getCell(adr('A2')) - const range = engine.rangeMapping.fetchRange(adr('A1'), adr('A3')) + const range = engine.rangeMapping.getVertexOrThrow(adr('A1'), adr('A3')) expect(a2).toBeInstanceOf(EmptyCellVertex) - expect(engine.graph.existsEdge(a2, range)).toBe(true) + expect(engine.graph.existsEdge(a2!, range)).toBe(true) expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([ ['1', null], @@ -512,11 +512,11 @@ describe('Adding row, ranges', () => { engine.addRows(0, [1, 1]) - const a2 = engine.addressMapping.fetchCell(adr('A2')) + const a2 = engine.addressMapping.getCell(adr('A2')) - const range = engine.rangeMapping.fetchRange(adr('A1'), adr('A4')) + const range = engine.rangeMapping.getVertexOrThrow(adr('A1'), adr('A4')) expect(a2).toBeInstanceOf(EmptyCellVertex) - expect(engine.graph.existsEdge(a2, range)).toBe(true) + expect(engine.graph.existsEdge(a2!, range)).toBe(true) expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([ ['1', null], @@ -562,7 +562,7 @@ describe('Adding row, column range', () => { engine.addRows(0, [1, 1]) - expect(engine.rangeMapping.getRange(colStart('A'), colEnd('B'))).not.toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(colStart('A'), colEnd('B'))).not.toBe(undefined) expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([ ['1', '1', '=SUM(A:B)'], @@ -583,12 +583,12 @@ describe('Adding row, fixing row ranges', () => { ['=SUM(1:3)'], ]) - expect(engine.rangeMapping.getRange(rowStart(1), rowEnd(3))).not.toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(rowStart(1), rowEnd(3))).not.toBe(undefined) engine.addRows(0, [1, 1]) - expect(engine.rangeMapping.getRange(rowStart(1), rowEnd(3))).toBe(undefined) - expect(engine.rangeMapping.getRange(rowStart(1), rowEnd(4))).not.toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(rowStart(1), rowEnd(3))).toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(rowStart(1), rowEnd(4))).not.toBe(undefined) expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([ ['1'], @@ -608,10 +608,10 @@ describe('Adding row, fixing row ranges', () => { ['=SUM(1:3)'], ]) - expect(engine.rangeMapping.getRange(rowStart(1), rowEnd(3))).not.toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(rowStart(1), rowEnd(3))).not.toBe(undefined) engine.addRows(0, [0, 1]) - expect(engine.rangeMapping.getRange(rowStart(1), rowEnd(3))).toBe(undefined) - expect(engine.rangeMapping.getRange(rowStart(2), rowEnd(4))).not.toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(rowStart(1), rowEnd(3))).toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(rowStart(2), rowEnd(4))).not.toBe(undefined) expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([ [null], @@ -631,9 +631,9 @@ describe('Adding row, fixing row ranges', () => { ['=SUM(1:3)'], ]) - expect(engine.rangeMapping.getRange(rowStart(1), rowEnd(3))).not.toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(rowStart(1), rowEnd(3))).not.toBe(undefined) engine.addRows(0, [3, 1]) - expect(engine.rangeMapping.getRange(rowStart(1), rowEnd(3))).not.toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(rowStart(1), rowEnd(3))).not.toBe(undefined) expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([ ['1'], diff --git a/test/unit/cruds/adding-row.spec.ts b/test/unit/cruds/adding-row.spec.ts index 129d35dc7..45d0a8651 100644 --- a/test/unit/cruds/adding-row.spec.ts +++ b/test/unit/cruds/adding-row.spec.ts @@ -1,7 +1,7 @@ import {ExportedCellChange, HyperFormula, SheetSizeLimitExceededError} from '../../../src' import {AbsoluteCellRange} from '../../../src/AbsoluteCellRange' import {Config} from '../../../src/Config' -import {ArrayVertex, FormulaCellVertex} from '../../../src/DependencyGraph' +import {ArrayFormulaVertex, ScalarFormulaVertex} from '../../../src/DependencyGraph' import {AlwaysDense} from '../../../src/DependencyGraph/AddressMapping/ChooseAddressMappingPolicy' import {ColumnIndex} from '../../../src/Lookup/ColumnIndex' import {adr, expectArrayWithSameContent, expectEngineToBeTheSameAs, extractMatrixRange} from '../testUtils' @@ -249,17 +249,17 @@ describe('Adding row - reevaluation', () => { }) }) -describe('Adding row - FormulaCellVertex#address update', () => { +describe('Adding row - ScalarFormulaVertex#address update', () => { it('insert row, formula vertex address shifted', () => { const engine = HyperFormula.buildFromArray([ // new row ['=SUM(1, 2)'], ]) - let vertex = engine.addressMapping.fetchCell(adr('A1')) as FormulaCellVertex + let vertex = engine.addressMapping.getCell(adr('A1')) as ScalarFormulaVertex expect(vertex.getAddress(engine.lazilyTransformingAstService)).toEqual(adr('A1')) engine.addRows(0, [0, 1]) - vertex = engine.addressMapping.fetchCell(adr('A2')) as FormulaCellVertex + vertex = engine.addressMapping.getCell(adr('A2')) as ScalarFormulaVertex expect(vertex.getAddress(engine.lazilyTransformingAstService)).toEqual(adr('A2')) }) @@ -276,7 +276,7 @@ describe('Adding row - FormulaCellVertex#address update', () => { engine.addRows(0, [0, 1]) - const formulaVertex = engine.addressMapping.fetchCell(adr('A1', 1)) as FormulaCellVertex + const formulaVertex = engine.addressMapping.getCell(adr('A1', 1)) as ScalarFormulaVertex expect(formulaVertex.getAddress(engine.lazilyTransformingAstService)).toEqual(adr('A1', 1)) formulaVertex.getFormula(engine.lazilyTransformingAstService) // force transformations to be applied @@ -438,7 +438,7 @@ describe('Adding row - arrays', () => { ], {useArrayArithmetic: true})) }) - it('ArrayVertex#formula should be updated', () => { + it('ArrayFormulaVertex#formula should be updated', () => { const engine = HyperFormula.buildFromArray([ ['1', '2'], ['3', '4'], @@ -450,7 +450,7 @@ describe('Adding row - arrays', () => { expect(extractMatrixRange(engine, adr('A4'))).toEqual(new AbsoluteCellRange(adr('A1'), adr('B3'))) }) - it('ArrayVertex#formula should be updated when different sheets', () => { + it('ArrayFormulaVertex#formula should be updated when different sheets', () => { const engine = HyperFormula.buildFromSheets({ Sheet1: [ ['1', '2'], @@ -466,7 +466,7 @@ describe('Adding row - arrays', () => { expect(extractMatrixRange(engine, adr('A1', 1))).toEqual(new AbsoluteCellRange(adr('A1'), adr('B3'))) }) - it('ArrayVertex#address should be updated', () => { + it('ArrayFormulaVertex#address should be updated', () => { const engine = HyperFormula.buildFromArray([ ['1', '2'], ['3', '4'], @@ -475,7 +475,7 @@ describe('Adding row - arrays', () => { engine.addRows(0, [1, 1]) - const matrixVertex = engine.addressMapping.fetchCell(adr('A4')) as ArrayVertex + const matrixVertex = engine.addressMapping.getCell(adr('A4')) as ArrayFormulaVertex expect(matrixVertex.getAddress(engine.lazilyTransformingAstService)).toEqual(adr('A4')) }) }) diff --git a/test/unit/cruds/adding-sheet.spec.ts b/test/unit/cruds/adding-sheet.spec.ts index dd4566a3b..71de7eff5 100644 --- a/test/unit/cruds/adding-sheet.spec.ts +++ b/test/unit/cruds/adding-sheet.spec.ts @@ -1,13 +1,13 @@ -import {HyperFormula, SheetNameAlreadyTakenError} from '../../../src' +import {AlwaysSparse, ErrorType, HyperFormula, SheetNameAlreadyTakenError} from '../../../src' import {plPL} from '../../../src/i18n/languages' -import {adr} from '../testUtils' +import {adr, detailedError} from '../testUtils' describe('Adding sheet - checking if its possible', () => { it('yes', () => { const engine = HyperFormula.buildEmpty() - expect(engine.isItPossibleToAddSheet('Sheet1')).toEqual(true) - expect(engine.isItPossibleToAddSheet('~`!@#$%^&*()_-+_=/|?{}[]\\"')).toEqual(true) + expect(engine.isItPossibleToAddSheet('Sheet1')).toBe(true) + expect(engine.isItPossibleToAddSheet('~`!@#$%^&*()_-+_=/|?{}[]\\"')).toBe(true) }) it('no', () => { @@ -16,8 +16,8 @@ describe('Adding sheet - checking if its possible', () => { Foo: [], }) - expect(engine.isItPossibleToAddSheet('Sheet1')).toEqual(false) - expect(engine.isItPossibleToAddSheet('Foo')).toEqual(false) + expect(engine.isItPossibleToAddSheet('Sheet1')).toBe(false) + expect(engine.isItPossibleToAddSheet('Foo')).toBe(false) }) }) @@ -27,8 +27,8 @@ describe('add sheet to engine', () => { engine.addSheet() - expect(engine.sheetMapping.numberOfSheets()).toEqual(1) - expect(Array.from(engine.sheetMapping.displayNames())).toEqual(['Sheet1']) + expect(engine.sheetMapping.numberOfSheets()).toBe(1) + expect(Array.from(engine.sheetMapping.iterateSheetNames())).toEqual(['Sheet1']) }) it('should add sheet to engine with one sheet', function() { @@ -38,8 +38,8 @@ describe('add sheet to engine', () => { engine.addSheet() - expect(engine.sheetMapping.numberOfSheets()).toEqual(2) - expect(Array.from(engine.sheetMapping.displayNames())).toEqual(['Sheet1', 'Sheet2']) + expect(engine.sheetMapping.numberOfSheets()).toBe(2) + expect(Array.from(engine.sheetMapping.iterateSheetNames())).toEqual(['Sheet1', 'Sheet2']) }) it('should be possible to fetch empty cell from newly added sheet', function() { @@ -47,7 +47,7 @@ describe('add sheet to engine', () => { engine.addSheet() - expect(engine.getCellValue(adr('A1', 0))).toBe(null) + expect(engine.getCellValue(adr('A1', 0))).toBeNull() }) it('should add sheet with translated sheet name', function() { @@ -56,8 +56,8 @@ describe('add sheet to engine', () => { engine.addSheet() - expect(engine.sheetMapping.numberOfSheets()).toEqual(1) - expect(Array.from(engine.sheetMapping.displayNames())).toEqual(['Arkusz1']) + expect(engine.sheetMapping.numberOfSheets()).toBe(1) + expect(Array.from(engine.sheetMapping.iterateSheetNames())).toEqual(['Arkusz1']) }) it('should add sheet with given name', function() { @@ -65,8 +65,8 @@ describe('add sheet to engine', () => { engine.addSheet('foo') - expect(engine.sheetMapping.numberOfSheets()).toEqual(1) - expect(Array.from(engine.sheetMapping.displayNames())).toEqual(['foo']) + expect(engine.sheetMapping.numberOfSheets()).toBe(1) + expect(Array.from(engine.sheetMapping.iterateSheetNames())).toEqual(['foo']) }) it('cannot add another sheet with same lowercased name', function() { @@ -76,8 +76,9 @@ describe('add sheet to engine', () => { expect(() => { engine.addSheet('FOO') }).toThrowError(/already exists/) - expect(engine.sheetMapping.numberOfSheets()).toEqual(1) - expect(Array.from(engine.sheetMapping.displayNames())).toEqual(['foo']) + + expect(engine.sheetMapping.numberOfSheets()).toBe(1) + expect(Array.from(engine.sheetMapping.iterateSheetNames())).toEqual(['foo']) }) it('should return given name', function() { @@ -85,7 +86,7 @@ describe('add sheet to engine', () => { const sheetName = engine.addSheet('foo') - expect(sheetName).toEqual('foo') + expect(sheetName).toBe('foo') }) @@ -94,7 +95,7 @@ describe('add sheet to engine', () => { const sheetName = engine.addSheet() - expect(sheetName).toEqual('Sheet1') + expect(sheetName).toBe('Sheet1') }) it('should throw error when sheet name is already taken', () => { @@ -106,3 +107,389 @@ describe('add sheet to engine', () => { }).toThrow(new SheetNameAlreadyTakenError('bar')) }) }) + +describe('recalculates formulas after adding new sheet (issue #1116)', () => { + it('recalculates single cell reference', () => { + const engine = HyperFormula.buildEmpty() + const table1Name = 'table1' + const table2Name = 'table2' + + engine.addSheet(table1Name) + engine.setCellContents(adr('A1', engine.getSheetId(table1Name)), `='${table2Name}'!A1`) + + expect(engine.getCellValue(adr('A1', engine.getSheetId(table1Name)))).toEqualError(detailedError(ErrorType.REF)) + + engine.addSheet(table2Name) + + expect(engine.getCellValue(adr('A1', engine.getSheetId(table2Name)))).toBeNull() + expect(engine.getCellValue(adr('A1', engine.getSheetId(table1Name)))).toBeNull() + + engine.setCellContents(adr('A1', engine.getSheetId(table2Name)), 10) + + expect(engine.getCellValue(adr('A1', engine.getSheetId(table1Name)))).toBe(10) + }) + + it('recalculates chained dependencies across multiple sheets', () => { + const engine = HyperFormula.buildEmpty() + const sheet1Name = 'Sheet1' + const sheet2Name = 'Sheet2' + const sheet3Name = 'Sheet3' + + engine.addSheet(sheet1Name) + engine.addSheet(sheet2Name) + engine.setCellContents(adr('A1', engine.getSheetId(sheet1Name)), `='${sheet2Name}'!A1+2`) + engine.setCellContents(adr('A1', engine.getSheetId(sheet2Name)), `='${sheet3Name}'!A1*2`) + + expect(engine.getCellValue(adr('A1', engine.getSheetId(sheet1Name)))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('A1', engine.getSheetId(sheet2Name)))).toEqualError(detailedError(ErrorType.REF)) + + engine.addSheet(sheet3Name) + + expect(engine.getCellValue(adr('A1', engine.getSheetId(sheet3Name)))).toBeNull() + expect(engine.getCellValue(adr('A1', engine.getSheetId(sheet2Name)))).toBe(0) + expect(engine.getCellValue(adr('A1', engine.getSheetId(sheet1Name)))).toBe(2) + + engine.setCellContents(adr('A1', engine.getSheetId(sheet3Name)), 42) + + expect(engine.getCellValue(adr('A1', engine.getSheetId(sheet2Name)))).toBe(84) + expect(engine.getCellValue(adr('A1', engine.getSheetId(sheet1Name)))).toBe(86) + }) + + it('recalculates nested dependencies within same sheet', () => { + const engine = HyperFormula.buildEmpty() + const sheet1Name = 'Sheet1' + const newSheetName = 'NewSheet' + + engine.addSheet(sheet1Name) + engine.setCellContents(adr('B1', engine.getSheetId(sheet1Name)), `='${newSheetName}'!A1`) + engine.setCellContents(adr('A1', engine.getSheetId(sheet1Name)), '=B1*2') + + expect(engine.getCellValue(adr('B1', engine.getSheetId(sheet1Name)))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('A1', engine.getSheetId(sheet1Name)))).toEqualError(detailedError(ErrorType.REF)) + + engine.addSheet(newSheetName) + + expect(engine.getCellValue(adr('B1', engine.getSheetId(newSheetName)))).toBeNull() + expect(engine.getCellValue(adr('B1', engine.getSheetId(sheet1Name)))).toBeNull() + expect(engine.getCellValue(adr('A1', engine.getSheetId(sheet1Name)))).toBe(0) + + engine.setCellContents(adr('A1', engine.getSheetId(newSheetName)), 15) + + expect(engine.getCellValue(adr('B1', engine.getSheetId(sheet1Name)))).toBe(15) + expect(engine.getCellValue(adr('A1', engine.getSheetId(sheet1Name)))).toBe(30) + }) + + it('recalculates multiple cells from different sheets', () => { + const engine = HyperFormula.buildEmpty() + const sheet1Name = 'Sheet1' + const sheet2Name = 'Sheet2' + const targetSheetName = 'TargetSheet' + + engine.addSheet(sheet1Name) + engine.addSheet(sheet2Name) + engine.setCellContents(adr('A1', engine.getSheetId(sheet1Name)), `='${targetSheetName}'!A1`) + engine.setCellContents(adr('B1', engine.getSheetId(sheet1Name)), `='${targetSheetName}'!B1`) + engine.setCellContents(adr('A1', engine.getSheetId(sheet2Name)), `='${targetSheetName}'!A1+10`) + engine.setCellContents(adr('B1', engine.getSheetId(sheet2Name)), `='${targetSheetName}'!B1+20`) + + expect(engine.getCellValue(adr('A1', engine.getSheetId(sheet1Name)))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('B1', engine.getSheetId(sheet1Name)))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('A1', engine.getSheetId(sheet2Name)))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('B1', engine.getSheetId(sheet2Name)))).toEqualError(detailedError(ErrorType.REF)) + + engine.addSheet(targetSheetName) + engine.setCellContents(adr('A1', engine.getSheetId(targetSheetName)), 5) + engine.setCellContents(adr('B1', engine.getSheetId(targetSheetName)), 7) + + expect(engine.getCellValue(adr('A1', engine.getSheetId(sheet1Name)))).toBe(5) + expect(engine.getCellValue(adr('B1', engine.getSheetId(sheet1Name)))).toBe(7) + expect(engine.getCellValue(adr('A1', engine.getSheetId(sheet2Name)))).toBe(15) + expect(engine.getCellValue(adr('B1', engine.getSheetId(sheet2Name)))).toBe(27) + }) + + it('recalculates formulas with mixed operations', () => { + const engine = HyperFormula.buildEmpty() + const sheet1Name = 'Sheet1' + const newSheetName = 'NewSheet' + + engine.addSheet(sheet1Name) + engine.setCellContents(adr('A1', engine.getSheetId(sheet1Name)), 100) + engine.setCellContents(adr('B1', engine.getSheetId(sheet1Name)), `='${newSheetName}'!A1 + A1`) + engine.setCellContents(adr('C1', engine.getSheetId(sheet1Name)), `='${newSheetName}'!B1 * 2`) + + expect(engine.getCellValue(adr('B1', engine.getSheetId(sheet1Name)))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('C1', engine.getSheetId(sheet1Name)))).toEqualError(detailedError(ErrorType.REF)) + + engine.addSheet(newSheetName) + engine.setCellContents(adr('A1', engine.getSheetId(newSheetName)), 50) + engine.setCellContents(adr('B1', engine.getSheetId(newSheetName)), 25) + + expect(engine.getCellValue(adr('B1', engine.getSheetId(sheet1Name)))).toBe(150) + expect(engine.getCellValue(adr('C1', engine.getSheetId(sheet1Name)))).toBe(50) + }) + + it('recalculates formulas with range references', () => { + const engine = HyperFormula.buildEmpty() + const sheet1Name = 'Sheet1' + const dataSheetName = 'DataSheet' + + engine.addSheet(sheet1Name) + engine.setCellContents(adr('A1', engine.getSheetId(sheet1Name)), `=SUM('${dataSheetName}'!A1:B5)`) + engine.setCellContents(adr('A2', engine.getSheetId(sheet1Name)), `=MEDIAN('${dataSheetName}'!A1:B5)`) + + expect(engine.getCellValue(adr('A1', engine.getSheetId(sheet1Name)))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('A2', engine.getSheetId(sheet1Name)))).toEqualError(detailedError(ErrorType.REF)) + + engine.addSheet(dataSheetName) + const dataSheetId = engine.getSheetId(dataSheetName) + engine.setCellContents(adr('A1', dataSheetId), 1) + engine.setCellContents(adr('B1', dataSheetId), 2) + engine.setCellContents(adr('A2', dataSheetId), 3) + engine.setCellContents(adr('B2', dataSheetId), 4) + engine.setCellContents(adr('A3', dataSheetId), 5) + engine.setCellContents(adr('B3', dataSheetId), 6) + engine.setCellContents(adr('A4', dataSheetId), 7) + engine.setCellContents(adr('B4', dataSheetId), 8) + engine.setCellContents(adr('A5', dataSheetId), 9) + engine.setCellContents(adr('B5', dataSheetId), 10) + + expect(engine.getCellValue(adr('A1', engine.getSheetId(sheet1Name)))).toBe(55) + expect(engine.getCellValue(adr('A2', engine.getSheetId(sheet1Name)))).toBe(5.5) + }) + + it('recalculates named expressions', () => { + const engine = HyperFormula.buildEmpty() + const sheet1Name = 'Sheet1' + const newSheetName = 'NewSheet' + + engine.addSheet(sheet1Name) + engine.addNamedExpression('MyValue', `='${newSheetName}'!$A$1`) + engine.setCellContents(adr('A1', engine.getSheetId(sheet1Name)), '=MyValue') + engine.setCellContents(adr('A2', engine.getSheetId(sheet1Name)), '=MyValue*2') + + expect(engine.getCellValue(adr('A1', engine.getSheetId(sheet1Name)))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('A2', engine.getSheetId(sheet1Name)))).toEqualError(detailedError(ErrorType.REF)) + + engine.addSheet(newSheetName) + + expect(engine.getCellValue(adr('A1', engine.getSheetId(sheet1Name)))).toBeNull() + expect(engine.getCellValue(adr('A2', engine.getSheetId(sheet1Name)))).toBe(0) + + engine.setCellContents(adr('A1', engine.getSheetId(newSheetName)), 99) + + expect(engine.getCellValue(adr('A1', engine.getSheetId(sheet1Name)))).toBe(99) + expect(engine.getCellValue(adr('A2', engine.getSheetId(sheet1Name)))).toBe(198) + }) + + it('setCellContents adds formula referencing existing sheet after it was added', () => { + const engine = HyperFormula.buildFromSheets({ + 'Main': [[1]], + }) + const mainId = engine.getSheetId('Main')! + + engine.addSheet('NewSheet') + const newSheetId = engine.getSheetId('NewSheet')! + engine.setCellContents(adr('A1', newSheetId), 42) + + engine.setCellContents(adr('B1', mainId), '=NewSheet!A1') + + expect(engine.getCellValue(adr('B1', mainId))).toBe(42) + + engine.setCellContents(adr('C1', mainId), '=FutureSheet!A1') + + expect(engine.getCellValue(adr('C1', mainId))).toEqualError(detailedError(ErrorType.REF)) + + engine.addSheet('FutureSheet') + engine.setCellContents(adr('A1', engine.getSheetId('FutureSheet')), 99) + + expect(engine.getCellValue(adr('C1', mainId))).toBe(99) + }) + + describe('when using ranges with', () => { + it('function using `runFunction`', () => { + const engine = HyperFormula.buildFromSheets({ + 'FirstSheet': [['=MEDIAN(NewSheet!A1:A1)', '=MEDIAN(NewSheet!A1:A2)', '=MEDIAN(NewSheet!A1:A3)', '=MEDIAN(NewSheet!A1:A4)']], + }) + const sheet1Id = engine.getSheetId('FirstSheet')! + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('B1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('C1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('D1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + + engine.addSheet('NewSheet') + engine.setSheetContent(engine.getSheetId('NewSheet')!, [[1], [2], [3], [4]]) + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(1) + expect(engine.getCellValue(adr('B1', sheet1Id))).toBe(1.5) + expect(engine.getCellValue(adr('C1', sheet1Id))).toBe(2) + expect(engine.getCellValue(adr('D1', sheet1Id))).toBe(2.5) + }) + + it('function not using `runFunction`', () => { + const engine = HyperFormula.buildFromSheets({ + 'FirstSheet': [['=SUM(NewSheet!A1:A1)', '=SUM(NewSheet!A1:A2)', '=SUM(NewSheet!A1:A3)', '=SUM(NewSheet!A1:A4)']], + }, {useArrayArithmetic: false}) + const sheet1Id = engine.getSheetId('FirstSheet')! + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('B1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('C1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('D1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + + engine.addSheet('NewSheet') + engine.setSheetContent(engine.getSheetId('NewSheet')!, [[1], [2], [3], [4]]) + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(1) + expect(engine.getCellValue(adr('B1', sheet1Id))).toBe(3) + expect(engine.getCellValue(adr('C1', sheet1Id))).toBe(6) + expect(engine.getCellValue(adr('D1', sheet1Id))).toBe(10) + }) + + it('function using `runFunction` referencing range indirectly', () => { + const engine = HyperFormula.buildFromSheets({ + 'FirstSheet': [ + ['=MEDIAN(A2)', '=MEDIAN(B2)', '=MEDIAN(C2)', '=MEDIAN(D2)'], + ['=\'NewSheet\'!A1:A1', '=\'NewSheet\'!A1:B2', '=\'NewSheet\'!A1:A3', '=\'NewSheet\'!A1:A4'], + ], + }, {useArrayArithmetic: false}) + const sheet1Id = engine.getSheetId('FirstSheet')! + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('B1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('C1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('D1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + + engine.addSheet('NewSheet') + engine.setSheetContent(engine.getSheetId('NewSheet')!, [[1], [2], [3], [4]]) + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(1) + expect(engine.getCellValue(adr('B1', sheet1Id))).toBe(1.5) + expect(engine.getCellValue(adr('C1', sheet1Id))).toBe(2) + expect(engine.getCellValue(adr('D1', sheet1Id))).toBe(2.5) + }) + + it('function not using `runFunction` referencing range indirectly', () => { + const engine = HyperFormula.buildFromSheets({ + 'FirstSheet': [ + ['=SUM(A2)', '=SUM(B2)', '=SUM(C2)', '=SUM(D2)'], + ['=\'NewSheet\'!A1:A1', '=\'NewSheet\'!A1:B2', '=\'NewSheet\'!A1:A3', '=\'NewSheet\'!A1:A4'], + ], + }, {useArrayArithmetic: false}) + const sheet1Id = engine.getSheetId('FirstSheet')! + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('B1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('C1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('D1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + + engine.addSheet('NewSheet') + engine.setSheetContent(engine.getSheetId('NewSheet')!, [[1], [2], [3], [4]]) + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(1) + expect(engine.getCellValue(adr('B1', sheet1Id))).toBe(3) + expect(engine.getCellValue(adr('C1', sheet1Id))).toBe(6) + expect(engine.getCellValue(adr('D1', sheet1Id))).toBe(10) + }) + + it('function calling a named expression', () => { + const engine = HyperFormula.buildFromSheets({ + 'FirstSheet': [['=\'NewSheet\'!A1:A4']], + }, {useArrayArithmetic: false}, [ + { name: 'ExprA', expression: '=MEDIAN(NewSheet!$A$1:$A$1)' }, + { name: 'ExprB', expression: '=MEDIAN(NewSheet!$A$1:$A$2)' }, + { name: 'ExprC', expression: '=MEDIAN(NewSheet!$A$1:$A$3)' }, + { name: 'ExprD', expression: '=MEDIAN(FirstSheet!$A$1)' }, + ]) + + expect(engine.getNamedExpressionValue('ExprA')).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getNamedExpressionValue('ExprB')).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getNamedExpressionValue('ExprC')).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getNamedExpressionValue('ExprD')).toEqualError(detailedError(ErrorType.REF)) + + engine.addSheet('NewSheet') + engine.setSheetContent(engine.getSheetId('NewSheet')!, [[1], [2], [3], [4]]) + + expect(engine.getNamedExpressionValue('ExprA')).toBe(1) + expect(engine.getNamedExpressionValue('ExprB')).toBe(1.5) + expect(engine.getNamedExpressionValue('ExprC')).toBe(2) + expect(engine.getNamedExpressionValue('ExprD')).toBe(2.5) + }) + }) + + it('should convert placeholder sheet strategy when adding referenced sheet', () => { + const engine = HyperFormula.buildFromSheets({ + 'MainSheet': [['=PlaceholderSheet!A1']], + }, { chooseAddressMappingPolicy: new AlwaysSparse()}) + + const mainId = engine.getSheetId('MainSheet')! + + expect(engine.getCellValue(adr('A1', mainId))).toEqualError(detailedError(ErrorType.REF)) + + engine.addSheet('PlaceholderSheet') + const placeholderId = engine.getSheetId('PlaceholderSheet')! + + engine.setCellContents(adr('A1', placeholderId), 42) + + expect(engine.getCellValue(adr('A1', mainId))).toBe(42) + }) + + it('should handle adding sheet that resolves references with ranges in placeholder', () => { + const engine = HyperFormula.buildFromSheets({ + Main: [['=SUM(Data!A1:A3)']], + }) + + const mainId = engine.getSheetId('Main')! + + expect(engine.getCellValue(adr('A1', mainId))).toEqualError(detailedError(ErrorType.REF)) + + engine.addSheet('Data') + const dataId = engine.getSheetId('Data')! + engine.setCellContents(adr('A1', dataId), 1) + engine.setCellContents(adr('A2', dataId), 2) + engine.setCellContents(adr('A3', dataId), 3) + + expect(engine.getCellValue(adr('A1', mainId))).toBe(6) + }) + + it('should handle remove and add sheet cycle with range references', () => { + const engine = HyperFormula.buildFromSheets({ + Main: [['=SUM(Data!A1:A3)']], + Data: [[1], [2], [3]], + }) + + const mainId = engine.getSheetId('Main')! + const dataId = engine.getSheetId('Data')! + + expect(engine.getCellValue(adr('A1', mainId))).toBe(6) + + engine.removeSheet(dataId) + + expect(engine.getCellValue(adr('A1', mainId))).toEqualError(detailedError(ErrorType.REF)) + + engine.addSheet('Data') + const newDataId = engine.getSheetId('Data')! + engine.setCellContents(adr('A1', newDataId), 10) + engine.setCellContents(adr('A2', newDataId), 20) + engine.setCellContents(adr('A3', newDataId), 30) + + expect(engine.getCellValue(adr('A1', mainId))).toBe(60) + }) + + it('should correctly merge sheets when adding sheet that was previously referenced', () => { + const engine = HyperFormula.buildFromSheets({ + Main: [['=NewSheet!A1 + NewSheet!B1']], + }) + + const mainId = engine.getSheetId('Main')! + + expect(engine.getCellValue(adr('A1', mainId))).toEqualError(detailedError(ErrorType.REF)) + + engine.addSheet('NewSheet') + const newSheetId = engine.getSheetId('NewSheet')! + engine.setCellContents(adr('A1', newSheetId), 100) + engine.setCellContents(adr('B1', newSheetId), 50) + + expect(engine.getCellValue(adr('A1', mainId))).toBe(150) + }) +}) diff --git a/test/unit/cruds/change-cell-content.spec.ts b/test/unit/cruds/change-cell-content.spec.ts index d34f09f32..cc083a0d8 100644 --- a/test/unit/cruds/change-cell-content.spec.ts +++ b/test/unit/cruds/change-cell-content.spec.ts @@ -13,7 +13,7 @@ import { import {AbsoluteCellRange} from '../../../src/AbsoluteCellRange' import {simpleCellAddress} from '../../../src/Cell' import {Config} from '../../../src/Config' -import {ArrayVertex, EmptyCellVertex, ValueCellVertex} from '../../../src/DependencyGraph' +import {ArrayFormulaVertex, EmptyCellVertex, ValueCellVertex} from '../../../src/DependencyGraph' import {ErrorMessage} from '../../../src/error-message' import {ColumnIndex} from '../../../src/Lookup/ColumnIndex' import { @@ -87,18 +87,18 @@ describe('changing cell content', () => { ['1', '2', '=A1'], ] const engine = HyperFormula.buildFromArray(sheet) - const a1 = engine.addressMapping.fetchCell(adr('A1')) - const b1 = engine.addressMapping.fetchCell(adr('B1')) - let c1 = engine.addressMapping.fetchCell(adr('C1')) + const a1 = engine.addressMapping.getCell(adr('A1')) + const b1 = engine.addressMapping.getCell(adr('B1')) + let c1 = engine.addressMapping.getCell(adr('C1')) - expect(engine.graph.existsEdge(a1, c1)).toBe(true) + expect(engine.graph.existsEdge(a1!, c1!)).toBe(true) expect(engine.getCellValue(adr('C1'))).toBe(1) engine.setCellContents(adr('C1'), [['=B1']]) - c1 = engine.addressMapping.fetchCell(adr('C1')) - expect(engine.graph.existsEdge(a1, c1)).toBe(false) - expect(engine.graph.existsEdge(b1, c1)).toBe(true) + c1 = engine.addressMapping.getCell(adr('C1')) + expect(engine.graph.existsEdge(a1!, c1!)).toBe(false) + expect(engine.graph.existsEdge(b1!, c1!)).toBe(true) expect(engine.getCellValue(adr('C1'))).toBe(2) }) @@ -108,14 +108,14 @@ describe('changing cell content', () => { ['1', '=A1'], ] const engine = HyperFormula.buildFromArray(sheet) - const a1 = engine.addressMapping.fetchCell(adr('A1')) - const b1 = engine.addressMapping.fetchCell(adr('B1')) + const a1 = engine.addressMapping.getCell(adr('A1')) + const b1 = engine.addressMapping.getCell(adr('B1')) - expect(engine.graph.existsEdge(a1, b1)).toBe(true) + expect(engine.graph.existsEdge(a1!, b1!)).toBe(true) expect(engine.getCellValue(adr('B1'))).toBe(1) engine.setCellContents(adr('B1'), [['7']]) expect(engine.getCellValue(adr('B1'))).toBe(7) - expect(engine.graph.existsEdge(a1, b1)).toBe(false) + expect(engine.graph.existsEdge(a1!, b1!)).toBe(false) }) it('update formula to plain text cell vertex', () => { @@ -123,14 +123,14 @@ describe('changing cell content', () => { ['1', '=A1'], ] const engine = HyperFormula.buildFromArray(sheet) - const a1 = engine.addressMapping.fetchCell(adr('A1')) - const b1 = engine.addressMapping.fetchCell(adr('B1')) + const a1 = engine.addressMapping.getCell(adr('A1')) + const b1 = engine.addressMapping.getCell(adr('B1')) - expect(engine.graph.existsEdge(a1, b1)).toBe(true) + expect(engine.graph.existsEdge(a1!, b1!)).toBe(true) expect(engine.getCellValue(adr('B1'))).toBe(1) engine.setCellContents(adr('B1'), [['foo']]) expect(engine.getCellValue(adr('B1'))).toBe('foo') - expect(engine.graph.existsEdge(a1, b1)).toBe(false) + expect(engine.graph.existsEdge(a1!, b1!)).toBe(false) }) it('set vertex with edge to empty cell', () => { @@ -140,10 +140,10 @@ describe('changing cell content', () => { engine.setCellContents(adr('A1'), [[null]]) - const a1 = engine.addressMapping.fetchCell(adr('A1')) - const a2 = engine.addressMapping.fetchCell(adr('B1')) + const a1 = engine.addressMapping.getCell(adr('A1')) + const a2 = engine.addressMapping.getCell(adr('B1')) expect(a1).toBeInstanceOf(EmptyCellVertex) - expect(engine.graph.existsEdge(a1, a2)).toBe(true) + expect(engine.graph.existsEdge(a1!, a2!)).toBe(true) expect(engine.getCellValue(adr('A1'))).toBe(null) }) @@ -152,8 +152,8 @@ describe('changing cell content', () => { ['1', '=A1'], ] const engine = HyperFormula.buildFromArray(sheet) - const a1 = engine.addressMapping.fetchCell(adr('A1')) - const b1 = engine.addressMapping.fetchCell(adr('B1')) + const a1 = engine.addressMapping.getCellOrThrow(adr('A1')) + const b1 = engine.addressMapping.getCellOrThrow(adr('B1')) expect(engine.graph.existsEdge(a1, b1)).toBe(true) expect(engine.getCellValue(adr('B1'))).toBe(1) @@ -168,15 +168,15 @@ describe('changing cell content', () => { ['1', '2'], ] const engine = HyperFormula.buildFromArray(sheet) - const a1 = engine.addressMapping.fetchCell(adr('A1')) - let b1 = engine.addressMapping.fetchCell(adr('B1')) + const a1 = engine.addressMapping.getCell(adr('A1')) + let b1 = engine.addressMapping.getCell(adr('B1')) - expect(engine.graph.existsEdge(a1, b1)).toBe(false) + expect(engine.graph.existsEdge(a1!, b1!)).toBe(false) expect(engine.getCellValue(adr('B1'))).toBe(2) engine.setCellContents(adr('B1'), [['=A1']]) - b1 = engine.addressMapping.fetchCell(adr('B1')) - expect(engine.graph.existsEdge(a1, b1)).toBe(true) + b1 = engine.addressMapping.getCell(adr('B1')) + expect(engine.graph.existsEdge(a1!, b1!)).toBe(true) expect(engine.getCellValue(adr('B1'))).toBe(1) }) @@ -290,13 +290,13 @@ describe('changing cell content', () => { engine.setCellContents(adr('B1'), '=A1') - const a1 = engine.addressMapping.fetchCell(adr('A1')) - const b1 = engine.addressMapping.fetchCell(adr('B1')) - expect(engine.graph.existsEdge(a1, b1)).toBe(true) + const a1 = engine.addressMapping.getCell(adr('A1')) + const b1 = engine.addressMapping.getCell(adr('B1')) + expect(engine.graph.existsEdge(a1!, b1!)).toBe(true) expect(engine.getCellValue(adr('B1'))).toBe(42) }) - it('set nothing again', () => { + it('set nothing again (2)', () => { const sheet = [ [null], ] @@ -338,11 +338,11 @@ describe('changing cell content', () => { engine.setCellContents(adr('B1'), '=A1') - const a1 = engine.addressMapping.fetchCell(adr('A1')) - const b1 = engine.addressMapping.fetchCell(adr('B1')) - const c1 = engine.addressMapping.fetchCell(adr('C1')) - expect(engine.graph.existsEdge(a1, b1)).toBe(true) - expect(engine.graph.existsEdge(b1, c1)).toBe(true) + const a1 = engine.addressMapping.getCell(adr('A1')) + const b1 = engine.addressMapping.getCell(adr('B1')) + const c1 = engine.addressMapping.getCell(adr('C1')) + expect(engine.graph.existsEdge(a1!, b1!)).toBe(true) + expect(engine.graph.existsEdge(b1!, c1!)).toBe(true) expect(engine.getCellValue(adr('B1'))).toBe(42) expect(engine.getCellValue(adr('C1'))).toBe(42) }) @@ -355,10 +355,10 @@ describe('changing cell content', () => { engine.setCellContents(adr('A1'), null) - const a1 = engine.addressMapping.fetchCell(adr('A1')) - const b1 = engine.addressMapping.fetchCell(adr('B1')) + const a1 = engine.addressMapping.getCell(adr('A1')) + const b1 = engine.addressMapping.getCell(adr('B1')) expect(a1).toBeInstanceOf(EmptyCellVertex) - expect(engine.graph.existsEdge(a1, b1)).toBe(true) + expect(engine.graph.existsEdge(a1!, b1!)).toBe(true) }) it('change EMPTY to NUMBER', () => { @@ -369,9 +369,9 @@ describe('changing cell content', () => { engine.setCellContents(adr('A1'), '7') - const a1 = engine.addressMapping.fetchCell(adr('A1')) - const b1 = engine.addressMapping.fetchCell(adr('B1')) - expect(engine.graph.existsEdge(a1, b1)).toBe(true) + const a1 = engine.addressMapping.getCell(adr('A1')) + const b1 = engine.addressMapping.getCell(adr('B1')) + expect(engine.graph.existsEdge(a1!, b1!)).toBe(true) expect(engine.getCellValue(adr('A1'))).toBe(7) }) @@ -383,9 +383,9 @@ describe('changing cell content', () => { engine.setCellContents(adr('A1'), 'foo') - const a1 = engine.addressMapping.fetchCell(adr('A1')) - const b1 = engine.addressMapping.fetchCell(adr('B1')) - expect(engine.graph.existsEdge(a1, b1)).toBe(true) + const a1 = engine.addressMapping.getCell(adr('A1')) + const b1 = engine.addressMapping.getCell(adr('B1')) + expect(engine.graph.existsEdge(a1!, b1!)).toBe(true) expect(engine.getCellValue(adr('A1'))).toBe('foo') }) @@ -486,7 +486,7 @@ describe('changing cell content', () => { expectArrayWithSameContent(changes.map((change) => change.newValue), [2, 10, 12, 18, 22]) }) - it('update empty cell to parsing error ', () => { + it('update empty cell to parsing error', () => { const engine = HyperFormula.buildFromArray([]) engine.setCellContents(adr('A1'), '=SUM(') @@ -494,7 +494,7 @@ describe('changing cell content', () => { expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.ERROR, ErrorMessage.ParseError)) }) - it('update dependency value cell to parsing error ', () => { + it('update dependency value cell to parsing error', () => { const sheet = [ ['1', '=SUM(A1)'], ] @@ -502,14 +502,14 @@ describe('changing cell content', () => { engine.setCellContents(adr('A1'), '=SUM(') - const a1 = engine.addressMapping.fetchCell(adr('A1')) - const b1 = engine.addressMapping.fetchCell(adr('B1')) - expect(engine.graph.existsEdge(a1, b1)).toBe(true) + const a1 = engine.addressMapping.getCell(adr('A1')) + const b1 = engine.addressMapping.getCell(adr('B1')) + expect(engine.graph.existsEdge(a1!, b1!)).toBe(true) expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.ERROR, ErrorMessage.ParseError)) expect(engine.getCellValue(adr('B1'))).toEqualError(detailedError(ErrorType.ERROR, ErrorMessage.ParseError)) }) - it('update formula cell to parsing error ', () => { + it('update formula cell to parsing error', () => { const sheet = [ ['1', '=SUM(A1)'], ] @@ -517,9 +517,9 @@ describe('changing cell content', () => { engine.setCellContents(adr('B1'), '=SUM(') - const a1 = engine.addressMapping.fetchCell(adr('A1')) - const b1 = engine.addressMapping.fetchCell(adr('B1')) - expect(engine.graph.existsEdge(a1, b1)).toBe(false) + const a1 = engine.addressMapping.getCell(adr('A1')) + const b1 = engine.addressMapping.getCell(adr('B1')) + expect(engine.graph.existsEdge(a1!, b1!)).toBe(false) expect(engine.getCellValue(adr('A1'))).toEqual(1) expect(engine.getCellValue(adr('B1'))).toEqualError(detailedError(ErrorType.ERROR, ErrorMessage.ParseError)) @@ -761,9 +761,9 @@ describe('column ranges', () => { engine.setCellContents(adr('A2'), '3') - const range = engine.rangeMapping.fetchRange(colStart('A'), colEnd('B')) - const a2 = engine.addressMapping.fetchCell(adr('A2')) - expect(engine.graph.existsEdge(a2, range)).toEqual(true) + const range = engine.rangeMapping.getVertexOrThrow(colStart('A'), colEnd('B')) + const a2 = engine.addressMapping.getCell(adr('A2')) + expect(engine.graph.existsEdge(a2!, range)).toEqual(true) expect(engine.getCellValue(adr('C1'))).toEqual(6) }) @@ -776,11 +776,11 @@ describe('column ranges', () => { engine.setCellContents(adr('B1'), '=TRANSPOSE(A2:A3)') - const range = engine.rangeMapping.fetchRange(colStart('B'), colEnd('C')) - const b1 = engine.addressMapping.fetchCell(adr('B1')) - const c1 = engine.addressMapping.fetchCell(adr('C1')) - expect(engine.graph.existsEdge(b1, range)).toEqual(true) - expect(engine.graph.existsEdge(c1, range)).toEqual(true) + const range = engine.rangeMapping.getVertexOrThrow(colStart('B'), colEnd('C')) + const b1 = engine.addressMapping.getCell(adr('B1')) + const c1 = engine.addressMapping.getCell(adr('C1')) + expect(engine.graph.existsEdge(b1!, range)).toEqual(true) + expect(engine.graph.existsEdge(c1!, range)).toEqual(true) expect(engine.getCellValue(adr('A1'))).toEqual(3) }) }) @@ -807,9 +807,9 @@ describe('row ranges', () => { engine.setCellContents(adr('B1'), '3') - const range = engine.rangeMapping.fetchRange(rowStart(1), rowEnd(2)) - const b1 = engine.addressMapping.fetchCell(adr('B1')) - expect(engine.graph.existsEdge(b1, range)).toEqual(true) + const range = engine.rangeMapping.getVertexOrThrow(rowStart(1), rowEnd(2)) + const b1 = engine.addressMapping.getCell(adr('B1')) + expect(engine.graph.existsEdge(b1!, range)).toEqual(true) expect(engine.getCellValue(adr('A3'))).toEqual(6) }) @@ -820,11 +820,11 @@ describe('row ranges', () => { engine.setCellContents(adr('A2'), '=TRANSPOSE(B1:C1)') - const range = engine.rangeMapping.fetchRange(rowStart(2), rowEnd(3)) - const a2 = engine.addressMapping.fetchCell(adr('A2')) - const a3 = engine.addressMapping.fetchCell(adr('A3')) - expect(engine.graph.existsEdge(a2, range)).toEqual(true) - expect(engine.graph.existsEdge(a3, range)).toEqual(true) + const range = engine.rangeMapping.getVertexOrThrow(rowStart(2), rowEnd(3)) + const a2 = engine.addressMapping.getCell(adr('A2')) + const a3 = engine.addressMapping.getCell(adr('A3')) + expect(engine.graph.existsEdge(a2!, range)).toEqual(true) + expect(engine.graph.existsEdge(a3!, range)).toEqual(true) expect(engine.getCellValue(adr('A1'))).toEqual(3) }) }) @@ -884,7 +884,7 @@ describe('arrays', () => { expect(engine.arrayMapping.getArrayByCorner(adr('A1'))?.array.size).toEqual(ArraySize.error()) expectVerticesOfTypes(engine, [ - [ArrayVertex, undefined], + [ArrayFormulaVertex, undefined], [ValueCellVertex, undefined], ]) expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([ @@ -902,12 +902,11 @@ describe('arrays', () => { ]) expectVerticesOfTypes(engine, [ - [ArrayVertex, ArrayVertex, undefined], - [ArrayVertex, ArrayVertex, ArrayVertex], - [undefined, ArrayVertex, ArrayVertex], + [ArrayFormulaVertex, ArrayFormulaVertex, undefined], + [ArrayFormulaVertex, ArrayFormulaVertex, ArrayFormulaVertex], + [undefined, ArrayFormulaVertex, ArrayFormulaVertex], ]) expect(engine.arrayMapping.arrayMapping.size).toEqual(4) - expect(engine.getSheetValues(0)) }) it('should REF last array', () => { @@ -922,8 +921,8 @@ describe('arrays', () => { ]) expectVerticesOfTypes(engine, [ - [ArrayVertex, ArrayVertex, ArrayVertex], - [ArrayVertex, ArrayVertex, ArrayVertex], + [ArrayFormulaVertex, ArrayFormulaVertex, ArrayFormulaVertex], + [ArrayFormulaVertex, ArrayFormulaVertex, ArrayFormulaVertex], [undefined, undefined], ]) expect(engine.getSheetValues(0)).toEqual([ @@ -931,7 +930,6 @@ describe('arrays', () => { [noSpace(), 2, 2, 1, 2], ]) expect(engine.arrayMapping.arrayMapping.size).toEqual(3) - expect(engine.getSheetValues(0)) }) it('should make existing array REF and change cell content to simple value', () => { @@ -1070,8 +1068,8 @@ describe('arrays', () => { const b1 = engine.dependencyGraph.getCell(adr('b1'))! const b2 = engine.dependencyGraph.getCell(adr('b2'))! const b3 = engine.dependencyGraph.getCell(adr('b3'))! - const b1b2 = engine.rangeMapping.getRange(adr('b1'), adr('b2'))! - const b1b3 = engine.rangeMapping.getRange(adr('b1'), adr('b3'))! + const b1b2 = engine.rangeMapping.getRangeVertex(adr('b1'), adr('b2'))! + const b1b3 = engine.rangeMapping.getRangeVertex(adr('b1'), adr('b3'))! expect(engine.graph.existsEdge(b1, b1b2)).toBe(true) expect(engine.graph.existsEdge(b2, b1b2)).toBe(true) diff --git a/test/unit/cruds/copy-paste.spec.ts b/test/unit/cruds/copy-paste.spec.ts index 3f6c968f0..44674cd86 100644 --- a/test/unit/cruds/copy-paste.spec.ts +++ b/test/unit/cruds/copy-paste.spec.ts @@ -323,11 +323,11 @@ describe('Copy - paste integration', () => { engine.copy(AbsoluteCellRange.spanFrom(adr('C1'), 1, 1)) engine.paste(adr('A3')) - const range = engine.rangeMapping.fetchRange(rowStart(2), rowEnd(3)) - const a2 = engine.addressMapping.fetchCell(adr('A2')) - const a3 = engine.addressMapping.fetchCell(adr('A3')) - expect(engine.graph.existsEdge(a2, range)) - expect(engine.graph.existsEdge(a3, range)) + const range = engine.rangeMapping.getVertexOrThrow(rowStart(2), rowEnd(3)) + const a2 = engine.addressMapping.getCell(adr('A2')) + const a3 = engine.addressMapping.getCell(adr('A3')) + expect(engine.graph.existsEdge(a2!, range)).toBe(true) + expect(engine.graph.existsEdge(a3!, range)).toBe(true) expect(engine.getCellValue(adr('A1'))).toEqual(4) }) @@ -446,11 +446,14 @@ describe('Copy - paste integration - actions at the Operations layer', () => { const lazilyTransformingAstService = new LazilyTransformingAstService(stats) const dependencyGraph = DependencyGraph.buildEmpty(lazilyTransformingAstService, config, functionRegistry, namedExpressions, stats) const columnSearch = buildColumnSearchStrategy(dependencyGraph, config, stats) - const sheetMapping = dependencyGraph.sheetMapping const dateTimeHelper = new DateTimeHelper(config) const numberLiteralHelper = new NumberLiteralHelper(config) const cellContentParser = new CellContentParser(config, dateTimeHelper, numberLiteralHelper) - const parser = new ParserWithCaching(config, functionRegistry, sheetMapping.get) + const parser = new ParserWithCaching( + config, + functionRegistry, + dependencyGraph.sheetReferenceRegistrar.ensureSheetRegistered.bind(dependencyGraph.sheetReferenceRegistrar) + ) const arraySizePredictor = new ArraySizePredictor(config, functionRegistry) operations = new Operations(config, dependencyGraph, columnSearch, cellContentParser, parser, stats, lazilyTransformingAstService, namedExpressions, arraySizePredictor) }) diff --git a/test/unit/cruds/cut-paste.spec.ts b/test/unit/cruds/cut-paste.spec.ts index 037f502c0..37a5e454b 100644 --- a/test/unit/cruds/cut-paste.spec.ts +++ b/test/unit/cruds/cut-paste.spec.ts @@ -255,10 +255,10 @@ describe('Move cells', () => { engine.cut(AbsoluteCellRange.spanFrom(adr('A1'), 1, 1)) engine.paste(adr('A2')) - const b1 = engine.addressMapping.fetchCell(adr('B1')) - const b2 = engine.addressMapping.fetchCell(adr('B2')) + const b1 = engine.addressMapping.getCell(adr('B1')) + const b2 = engine.addressMapping.getCell(adr('B2')) const source = engine.addressMapping.getCell(adr('A1')) - const target = engine.addressMapping.fetchCell(adr('A2')) + const target = engine.addressMapping.getCell(adr('A2')) expect(graphEdgesCount(engine.graph)).toBe( 2, // A2 -> B1, A2 -> B2 @@ -269,8 +269,8 @@ describe('Move cells', () => { ) expect(source).toBe(undefined) - expect(engine.graph.existsEdge(target, b2)).toBe(true) - expect(engine.graph.existsEdge(target, b1)).toBe(true) + expect(engine.graph.existsEdge(target!, b2!)).toBe(true) + expect(engine.graph.existsEdge(target!, b1!)).toBe(true) expect(engine.getCellValue(adr('A2'))).toBe(1) }) }) @@ -291,12 +291,12 @@ describe('moving ranges', () => { expect(range.end).toEqual(adr('A2')) expect(engine.getCellValue(adr('A3'))).toEqual(2) - const a1 = engine.addressMapping.fetchCell(adr('A1')) - const a2 = engine.addressMapping.fetchCell(adr('A2')) - const a1a2 = engine.rangeMapping.fetchRange(adr('A1'), adr('A2')) + const a1 = engine.addressMapping.getCell(adr('A1')) + const a2 = engine.addressMapping.getCell(adr('A2')) + const a1a2 = engine.rangeMapping.getVertexOrThrow(adr('A1'), adr('A2')) expect(a1).toBeInstanceOf(EmptyCellVertex) - expect(engine.graph.existsEdge(a1, a1a2)).toBe(true) - expect(engine.graph.existsEdge(a2, a1a2)).toBe(true) + expect(engine.graph.existsEdge(a1!, a1a2)).toBe(true) + expect(engine.graph.existsEdge(a2!, a1a2)).toBe(true) expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([ [null, '1'], @@ -315,7 +315,7 @@ describe('moving ranges', () => { engine.cut(AbsoluteCellRange.spanFrom(adr('A1'), 1, 2)) engine.paste(adr('B1')) - expect(engine.rangeMapping.getRange(adr('B1'), adr('B2'))).not.toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(adr('B1'), adr('B2'))).not.toBe(undefined) const range = extractRange(engine, adr('A3')) expect(range.start).toEqual(adr('B1')) @@ -372,14 +372,14 @@ describe('moving ranges', () => { engine.cut(AbsoluteCellRange.spanFrom(adr('A1'), 1, 1)) engine.paste(adr('A2')) - const b1 = engine.addressMapping.fetchCell(adr('B1')) - const b2 = engine.addressMapping.fetchCell(adr('B2')) - const source = engine.addressMapping.fetchCell(adr('A1')) - const target = engine.addressMapping.fetchCell(adr('A2')) - const range = engine.rangeMapping.fetchRange(adr('A1'), adr('A2')) + const b1 = engine.addressMapping.getCell(adr('B1')) + const b2 = engine.addressMapping.getCell(adr('B2')) + const source = engine.addressMapping.getCell(adr('A1')) + const target = engine.addressMapping.getCell(adr('A2')) + const range = engine.rangeMapping.getVertexOrThrow(adr('A1'), adr('A2')) expect(source).toBeInstanceOf(EmptyCellVertex) - expect(source.getCellValue()).toBe(EmptyValue) + expect(source!.getCellValue()).toBe(EmptyValue) expect(engine.graph.getNodes().length).toBe( +2 // formulas + 1 // A2 @@ -391,10 +391,10 @@ describe('moving ranges', () => { + 1 // A1:A2 -> B1 + 1, // A2 -> B2 ) - expect(engine.graph.existsEdge(target, b2)).toBe(true) - expect(engine.graph.existsEdge(source, range)).toBe(true) - expect(engine.graph.existsEdge(target, range)).toBe(true) - expect(engine.graph.existsEdge(range, b1)).toBe(true) + expect(engine.graph.existsEdge(target!, b2!)).toBe(true) + expect(engine.graph.existsEdge(source!, range)).toBe(true) + expect(engine.graph.existsEdge(target!, range)).toBe(true) + expect(engine.graph.existsEdge(range, b1!)).toBe(true) expect(engine.getCellValue(adr('A2'))).toBe(1) expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([ @@ -414,10 +414,10 @@ describe('moving ranges', () => { const a1 = engine.addressMapping.getCell(adr('A1')) const a2 = engine.addressMapping.getCell(adr('A2')) - const b1 = engine.addressMapping.fetchCell(adr('B1')) - const c1 = engine.addressMapping.fetchCell(adr('C1')) - const c2 = engine.addressMapping.fetchCell(adr('C2')) - const range = engine.rangeMapping.fetchRange(adr('C1'), adr('C2')) + const b1 = engine.addressMapping.getCell(adr('B1')) + const c1 = engine.addressMapping.getCell(adr('C1')) + const c2 = engine.addressMapping.getCell(adr('C2')) + const range = engine.rangeMapping.getVertexOrThrow(adr('C1'), adr('C2')) expect(a1).toBe(undefined) expect(a2).toBe(undefined) @@ -433,9 +433,9 @@ describe('moving ranges', () => { + 1, // C2 -> B2 ) - expect(engine.graph.existsEdge(c1, range)).toBe(true) - expect(engine.graph.existsEdge(c2, range)).toBe(true) - expect(engine.graph.existsEdge(range, b1)).toBe(true) + expect(engine.graph.existsEdge(c1!, range)).toBe(true) + expect(engine.graph.existsEdge(c2!, range)).toBe(true) + expect(engine.graph.existsEdge(range, b1!)).toBe(true) expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([ [null, '=SUM(C1:C2)', '1'], @@ -464,16 +464,16 @@ describe('moving ranges', () => { )) /* edges */ - const c1c2 = engine.rangeMapping.fetchRange(adr('C1'), adr('C2')) - const a1a3 = engine.rangeMapping.fetchRange(adr('A1'), adr('A3')) + const c1c2 = engine.rangeMapping.getVertexOrThrow(adr('C1'), adr('C2')) + const a1a3 = engine.rangeMapping.getVertexOrThrow(adr('A1'), adr('A3')) expect(engine.graph.existsEdge(c1c2, a1a3)).toBe(false) - expect(engine.graph.existsEdge(engine.addressMapping.fetchCell(adr('A1')), a1a3)).toBe(true) - expect(engine.graph.existsEdge(engine.addressMapping.fetchCell(adr('A2')), a1a3)).toBe(true) - expect(engine.graph.existsEdge(engine.addressMapping.fetchCell(adr('A3')), a1a3)).toBe(true) + expect(engine.graph.existsEdge(engine.addressMapping.getCell(adr('A1'))!, a1a3)).toBe(true) + expect(engine.graph.existsEdge(engine.addressMapping.getCell(adr('A2'))!, a1a3)).toBe(true) + expect(engine.graph.existsEdge(engine.addressMapping.getCell(adr('A3'))!, a1a3)).toBe(true) - expect(engine.graph.existsEdge(engine.addressMapping.fetchCell(adr('C1')), c1c2)).toBe(true) - expect(engine.graph.existsEdge(engine.addressMapping.fetchCell(adr('C2')), c1c2)).toBe(true) + expect(engine.graph.existsEdge(engine.addressMapping.getCell(adr('C1'))!, c1c2)).toBe(true) + expect(engine.graph.existsEdge(engine.addressMapping.getCell(adr('C2'))!, c1c2)).toBe(true) expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([ [null, null, '1'], @@ -494,26 +494,26 @@ describe('moving ranges', () => { engine.paste(adr('C1')) /* edges */ - const c1c2 = engine.rangeMapping.fetchRange(adr('C1'), adr('C2')) - const c1c3 = engine.rangeMapping.fetchRange(adr('C1'), adr('C3')) - const a1a4 = engine.rangeMapping.fetchRange(adr('A1'), adr('A4')) + const c1c2 = engine.rangeMapping.getVertexOrThrow(adr('C1'), adr('C2')) + const c1c3 = engine.rangeMapping.getVertexOrThrow(adr('C1'), adr('C3')) + const a1a4 = engine.rangeMapping.getVertexOrThrow(adr('A1'), adr('A4')) expect(engine.graph.existsEdge(c1c2, c1c3)).toBe(true) expect(engine.graph.existsEdge(c1c3, a1a4)).toBe(false) - expect(engine.graph.existsEdge(engine.addressMapping.fetchCell(adr('A1')), a1a4)).toBe(true) - expect(engine.graph.existsEdge(engine.addressMapping.fetchCell(adr('A2')), a1a4)).toBe(true) - expect(engine.graph.existsEdge(engine.addressMapping.fetchCell(adr('A3')), a1a4)).toBe(true) - expect(engine.graph.existsEdge(engine.addressMapping.fetchCell(adr('A4')), a1a4)).toBe(true) + expect(engine.graph.existsEdge(engine.addressMapping.getCell(adr('A1'))!, a1a4)).toBe(true) + expect(engine.graph.existsEdge(engine.addressMapping.getCell(adr('A2'))!, a1a4)).toBe(true) + expect(engine.graph.existsEdge(engine.addressMapping.getCell(adr('A3'))!, a1a4)).toBe(true) + expect(engine.graph.existsEdge(engine.addressMapping.getCell(adr('A4'))!, a1a4)).toBe(true) - const c1 = engine.addressMapping.fetchCell(adr('C1')) - const c2 = engine.addressMapping.fetchCell(adr('C2')) - const c3 = engine.addressMapping.fetchCell(adr('C3')) - expect(engine.graph.existsEdge(c1, c1c2)).toBe(true) - expect(engine.graph.existsEdge(c2, c1c2)).toBe(true) - expect(engine.graph.existsEdge(c1, c1c3)).toBe(false) - expect(engine.graph.existsEdge(c2, c1c3)).toBe(false) - expect(engine.graph.existsEdge(c3, c1c3)).toBe(true) + const c1 = engine.addressMapping.getCell(adr('C1')) + const c2 = engine.addressMapping.getCell(adr('C2')) + const c3 = engine.addressMapping.getCell(adr('C3')) + expect(engine.graph.existsEdge(c1!, c1c2)).toBe(true) + expect(engine.graph.existsEdge(c2!, c1c2)).toBe(true) + expect(engine.graph.existsEdge(c1!, c1c3)).toBe(false) + expect(engine.graph.existsEdge(c2!, c1c3)).toBe(false) + expect(engine.graph.existsEdge(c3!, c1c3)).toBe(true) expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([ [null, null, '1'], @@ -715,7 +715,7 @@ describe('overlapping areas', () => { ])) }) - it('ArrayVertex#formula should be updated', () => { + it('ArrayFormulaVertex#formula should be updated', () => { const engine = HyperFormula.buildFromArray([ ['1', '2'], ['3', '4'], @@ -728,7 +728,7 @@ describe('overlapping areas', () => { expect(extractMatrixRange(engine, adr('A3'))).toEqual(new AbsoluteCellRange(adr('C1'), adr('D2'))) }) - it('ArrayVertex#formula should be updated when different sheets', () => { + it('ArrayFormulaVertex#formula should be updated when different sheets', () => { const engine = HyperFormula.buildFromSheets({ Sheet1: [ ['1', '2'], @@ -883,7 +883,7 @@ describe('aborting cut paste', () => { expect(engine.isClipboardEmpty()).toBe(true) }) - it('should be aborted when addColumns is done before paste', () => { + it('should be aborted when removeColumns is done before paste', () => { const engine = HyperFormula.buildFromArray([ ['1', '2'] ]) diff --git a/test/unit/cruds/move-cells.spec.ts b/test/unit/cruds/move-cells.spec.ts index 8ea86113f..e05a75742 100644 --- a/test/unit/cruds/move-cells.spec.ts +++ b/test/unit/cruds/move-cells.spec.ts @@ -2,7 +2,7 @@ import {ErrorType, HyperFormula, SimpleCellAddress, SimpleCellRange} from '../.. import {AbsoluteCellRange} from '../../../src/AbsoluteCellRange' import {simpleCellAddress} from '../../../src/Cell' import {Config} from '../../../src/Config' -import {EmptyCellVertex, FormulaCellVertex} from '../../../src/DependencyGraph' +import {EmptyCellVertex, ScalarFormulaVertex} from '../../../src/DependencyGraph' import {SheetSizeLimitExceededError} from '../../../src/errors' import {EmptyValue} from '../../../src/interpreter/InterpreterValue' import {ColumnIndex} from '../../../src/Lookup/ColumnIndex' @@ -73,7 +73,7 @@ describe('Moving rows - checking if its possible', () => { expect(engine.isItPossibleToMoveCells(AbsoluteCellRange.spanFrom(adr('A1'), 1, 2), adr('B2'))).toBe(false) }) - it('no if we move beyond sheet size limits ', () => { + it('no if we move beyond sheet size limits', () => { const engine = HyperFormula.buildFromArray([ ['1', '2'], ['3', '4'], @@ -248,7 +248,7 @@ describe('Move cells', () => { engine.moveCells(AbsoluteCellRange.spanFrom(adr('A2'), 1, 1), adr('B1', 1)) - const vertex = engine.dependencyGraph.fetchCell(adr('B1', 1)) as FormulaCellVertex + const vertex = engine.dependencyGraph.fetchCell(adr('B1', 1)) as ScalarFormulaVertex expect(vertex.getAddress(engine.lazilyTransformingAstService)).toEqual(adr('B1', 1)) }) @@ -354,10 +354,10 @@ describe('Move cells', () => { engine.moveCells(AbsoluteCellRange.spanFrom(adr('A1'), 1, 1), adr('A2')) - const b1 = engine.addressMapping.fetchCell(adr('B1')) - const b2 = engine.addressMapping.fetchCell(adr('B2')) + const b1 = engine.addressMapping.getCell(adr('B1')) + const b2 = engine.addressMapping.getCell(adr('B2')) const source = engine.addressMapping.getCell(adr('A1')) - const target = engine.addressMapping.fetchCell(adr('A2')) + const target = engine.addressMapping.getCell(adr('A2')) expect(graphEdgesCount(engine.graph)).toBe( 2, // A2 -> B1, A2 -> B2 @@ -368,8 +368,8 @@ describe('Move cells', () => { ) expect(source).toBe(undefined) - expect(engine.graph.existsEdge(target, b2)).toBe(true) - expect(engine.graph.existsEdge(target, b1)).toBe(true) + expect(engine.graph.existsEdge(target!, b2!)).toBe(true) + expect(engine.graph.existsEdge(target!, b1!)).toBe(true) expect(engine.getCellValue(adr('A2'))).toBe(1) }) @@ -434,12 +434,12 @@ describe('moving ranges', () => { expect(range.end).toEqual(adr('A2')) expect(engine.getCellValue(adr('A3'))).toEqual(2) - const a1 = engine.addressMapping.fetchCell(adr('A1')) - const a2 = engine.addressMapping.fetchCell(adr('A2')) - const a1a2 = engine.rangeMapping.fetchRange(adr('A1'), adr('A2')) + const a1 = engine.addressMapping.getCell(adr('A1')) + const a2 = engine.addressMapping.getCell(adr('A2')) + const a1a2 = engine.rangeMapping.getVertexOrThrow(adr('A1'), adr('A2')) expect(a1).toBeInstanceOf(EmptyCellVertex) - expect(engine.graph.existsEdge(a1, a1a2)).toBe(true) - expect(engine.graph.existsEdge(a2, a1a2)).toBe(true) + expect(engine.graph.existsEdge(a1!, a1a2)).toBe(true) + expect(engine.graph.existsEdge(a2!, a1a2)).toBe(true) expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([ [null, '1'], @@ -457,7 +457,7 @@ describe('moving ranges', () => { engine.moveCells(AbsoluteCellRange.spanFrom(adr('A1'), 1, 2), adr('B1')) - expect(engine.rangeMapping.getRange(adr('B1'), adr('B2'))).not.toBe(undefined) + expect(engine.rangeMapping.getRangeVertex(adr('B1'), adr('B2'))).not.toBe(undefined) const range = extractRange(engine, adr('A3')) expect(range.start).toEqual(adr('B1')) @@ -504,14 +504,14 @@ describe('moving ranges', () => { engine.moveCells(AbsoluteCellRange.spanFrom(adr('A1'), 1, 1), adr('A2')) - const b1 = engine.addressMapping.fetchCell(adr('B1')) - const b2 = engine.addressMapping.fetchCell(adr('B2')) - const source = engine.addressMapping.fetchCell(adr('A1')) - const target = engine.addressMapping.fetchCell(adr('A2')) - const range = engine.rangeMapping.fetchRange(adr('A1'), adr('A2')) + const b1 = engine.addressMapping.getCell(adr('B1')) + const b2 = engine.addressMapping.getCell(adr('B2')) + const source = engine.addressMapping.getCell(adr('A1')) + const target = engine.addressMapping.getCell(adr('A2')) + const range = engine.rangeMapping.getVertexOrThrow(adr('A1'), adr('A2')) expect(source).toBeInstanceOf(EmptyCellVertex) - expect(source.getCellValue()).toBe(EmptyValue) + expect(source!.getCellValue()).toBe(EmptyValue) expect(engine.graph.getNodes().length).toBe( +2 // formulas + 1 // A2 @@ -523,10 +523,10 @@ describe('moving ranges', () => { + 1 // A1:A2 -> B1 + 1, // A2 -> B2 ) - expect(engine.graph.existsEdge(target, b2)).toBe(true) - expect(engine.graph.existsEdge(source, range)).toBe(true) - expect(engine.graph.existsEdge(target, range)).toBe(true) - expect(engine.graph.existsEdge(range, b1)).toBe(true) + expect(engine.graph.existsEdge(target!, b2!)).toBe(true) + expect(engine.graph.existsEdge(source!, range)).toBe(true) + expect(engine.graph.existsEdge(target!, range)).toBe(true) + expect(engine.graph.existsEdge(range, b1!)).toBe(true) expect(engine.getCellValue(adr('A2'))).toBe(1) expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([ @@ -545,10 +545,10 @@ describe('moving ranges', () => { const a1 = engine.addressMapping.getCell(adr('A1')) const a2 = engine.addressMapping.getCell(adr('A2')) - const b1 = engine.addressMapping.fetchCell(adr('B1')) - const c1 = engine.addressMapping.fetchCell(adr('C1')) - const c2 = engine.addressMapping.fetchCell(adr('C2')) - const range = engine.rangeMapping.fetchRange(adr('C1'), adr('C2')) + const b1 = engine.addressMapping.getCell(adr('B1')) + const c1 = engine.addressMapping.getCell(adr('C1')) + const c2 = engine.addressMapping.getCell(adr('C2')) + const range = engine.rangeMapping.getVertexOrThrow(adr('C1'), adr('C2')) expect(a1).toBe(undefined) expect(a2).toBe(undefined) @@ -564,9 +564,9 @@ describe('moving ranges', () => { + 1, // C2 -> B2 ) - expect(engine.graph.existsEdge(c1, range)).toBe(true) - expect(engine.graph.existsEdge(c2, range)).toBe(true) - expect(engine.graph.existsEdge(range, b1)).toBe(true) + expect(engine.graph.existsEdge(c1!, range)).toBe(true) + expect(engine.graph.existsEdge(c2!, range)).toBe(true) + expect(engine.graph.existsEdge(range, b1!)).toBe(true) expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([ [null, '=SUM(C1:C2)', '1'], @@ -594,16 +594,16 @@ describe('moving ranges', () => { )) /* edges */ - const c1c2 = engine.rangeMapping.fetchRange(adr('C1'), adr('C2')) - const a1a3 = engine.rangeMapping.fetchRange(adr('A1'), adr('A3')) + const c1c2 = engine.rangeMapping.getVertexOrThrow(adr('C1'), adr('C2')) + const a1a3 = engine.rangeMapping.getVertexOrThrow(adr('A1'), adr('A3')) expect(engine.graph.existsEdge(c1c2, a1a3)).toBe(false) - expect(engine.graph.existsEdge(engine.addressMapping.fetchCell(adr('A1')), a1a3)).toBe(true) - expect(engine.graph.existsEdge(engine.addressMapping.fetchCell(adr('A2')), a1a3)).toBe(true) - expect(engine.graph.existsEdge(engine.addressMapping.fetchCell(adr('A3')), a1a3)).toBe(true) + expect(engine.graph.existsEdge(engine.addressMapping.getCell(adr('A1'))!, a1a3)).toBe(true) + expect(engine.graph.existsEdge(engine.addressMapping.getCell(adr('A2'))!, a1a3)).toBe(true) + expect(engine.graph.existsEdge(engine.addressMapping.getCell(adr('A3'))!, a1a3)).toBe(true) - expect(engine.graph.existsEdge(engine.addressMapping.fetchCell(adr('C1')), c1c2)).toBe(true) - expect(engine.graph.existsEdge(engine.addressMapping.fetchCell(adr('C2')), c1c2)).toBe(true) + expect(engine.graph.existsEdge(engine.addressMapping.getCell(adr('C1'))!, c1c2)).toBe(true) + expect(engine.graph.existsEdge(engine.addressMapping.getCell(adr('C2'))!, c1c2)).toBe(true) expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([ [null, null, '1'], @@ -623,26 +623,26 @@ describe('moving ranges', () => { engine.moveCells(AbsoluteCellRange.spanFrom(adr('A1'), 1, 3), adr('C1')) /* edges */ - const c1c2 = engine.rangeMapping.fetchRange(adr('C1'), adr('C2')) - const c1c3 = engine.rangeMapping.fetchRange(adr('C1'), adr('C3')) - const a1a4 = engine.rangeMapping.fetchRange(adr('A1'), adr('A4')) + const c1c2 = engine.rangeMapping.getVertexOrThrow(adr('C1'), adr('C2')) + const c1c3 = engine.rangeMapping.getVertexOrThrow(adr('C1'), adr('C3')) + const a1a4 = engine.rangeMapping.getVertexOrThrow(adr('A1'), adr('A4')) expect(engine.graph.existsEdge(c1c2, c1c3)).toBe(true) expect(engine.graph.existsEdge(c1c3, a1a4)).toBe(false) - expect(engine.graph.existsEdge(engine.addressMapping.fetchCell(adr('A1')), a1a4)).toBe(true) - expect(engine.graph.existsEdge(engine.addressMapping.fetchCell(adr('A2')), a1a4)).toBe(true) - expect(engine.graph.existsEdge(engine.addressMapping.fetchCell(adr('A3')), a1a4)).toBe(true) - expect(engine.graph.existsEdge(engine.addressMapping.fetchCell(adr('A4')), a1a4)).toBe(true) + expect(engine.graph.existsEdge(engine.addressMapping.getCell(adr('A1'))!, a1a4)).toBe(true) + expect(engine.graph.existsEdge(engine.addressMapping.getCell(adr('A2'))!, a1a4)).toBe(true) + expect(engine.graph.existsEdge(engine.addressMapping.getCell(adr('A3'))!, a1a4)).toBe(true) + expect(engine.graph.existsEdge(engine.addressMapping.getCell(adr('A4'))!, a1a4)).toBe(true) - const c1 = engine.addressMapping.fetchCell(adr('C1')) - const c2 = engine.addressMapping.fetchCell(adr('C2')) - const c3 = engine.addressMapping.fetchCell(adr('C3')) - expect(engine.graph.existsEdge(c1, c1c2)).toBe(true) - expect(engine.graph.existsEdge(c2, c1c2)).toBe(true) - expect(engine.graph.existsEdge(c1, c1c3)).toBe(false) - expect(engine.graph.existsEdge(c2, c1c3)).toBe(false) - expect(engine.graph.existsEdge(c3, c1c3)).toBe(true) + const c1 = engine.addressMapping.getCell(adr('C1')) + const c2 = engine.addressMapping.getCell(adr('C2')) + const c3 = engine.addressMapping.getCell(adr('C3')) + expect(engine.graph.existsEdge(c1!, c1c2)).toBe(true) + expect(engine.graph.existsEdge(c2!, c1c2)).toBe(true) + expect(engine.graph.existsEdge(c1!, c1c3)).toBe(false) + expect(engine.graph.existsEdge(c2!, c1c3)).toBe(false) + expect(engine.graph.existsEdge(c3!, c1c3)).toBe(true) expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([ [null, null, '1'], @@ -833,7 +833,7 @@ describe('overlapping areas', () => { ])) }) - it('ArrayVertex#formula should be updated', () => { + it('ArrayFormulaVertex#formula should be updated', () => { const engine = HyperFormula.buildFromArray([ ['1', '2'], ['3', '4'], @@ -845,7 +845,7 @@ describe('overlapping areas', () => { expect(extractMatrixRange(engine, adr('A3'))).toEqual(new AbsoluteCellRange(adr('C1'), adr('D2'))) }) - it('ArrayVertex#formula should be updated when different sheets', () => { + it('ArrayFormulaVertex#formula should be updated when different sheets', () => { const engine = HyperFormula.buildFromSheets({ Sheet1: [ ['1', '2'], @@ -1033,12 +1033,12 @@ describe('column ranges', () => { expect(range.end).toEqual(colEnd('B')) expect(engine.getCellValue(adr('C1'))).toEqual(3) - const a1 = engine.addressMapping.fetchCell(adr('A1')) - const b1 = engine.addressMapping.fetchCell(adr('B1')) - const ab = engine.rangeMapping.fetchRange(colStart('A'), colEnd('B')) + const a1 = engine.addressMapping.getCell(adr('A1')) + const b1 = engine.addressMapping.getCell(adr('B1')) + const ab = engine.rangeMapping.getVertexOrThrow(colStart('A'), colEnd('B')) expect(a1).toBeInstanceOf(EmptyCellVertex) - expect(engine.graph.existsEdge(a1, ab)).toBe(true) - expect(engine.graph.existsEdge(b1, ab)).toBe(true) + expect(engine.graph.existsEdge(a1!, ab)).toBe(true) + expect(engine.graph.existsEdge(b1!, ab)).toBe(true) }) it('should transform relative column references', () => { @@ -1079,12 +1079,12 @@ describe('row ranges', () => { expect(range.end).toEqual(rowEnd(2)) expect(engine.getCellValue(adr('A3'))).toEqual(3) - const a1 = engine.addressMapping.fetchCell(adr('A1')) - const a2 = engine.addressMapping.fetchCell(adr('A2')) - const ab = engine.rangeMapping.fetchRange(rowStart(1), rowEnd(2)) + const a1 = engine.addressMapping.getCell(adr('A1')) + const a2 = engine.addressMapping.getCell(adr('A2')) + const ab = engine.rangeMapping.getVertexOrThrow(rowStart(1), rowEnd(2)) expect(a1).toBeInstanceOf(EmptyCellVertex) - expect(engine.graph.existsEdge(a1, ab)).toBe(true) - expect(engine.graph.existsEdge(a2, ab)).toBe(true) + expect(engine.graph.existsEdge(a1!, ab)).toBe(true) + expect(engine.graph.existsEdge(a2!, ab)).toBe(true) }) it('should transform relative column references', () => { diff --git a/test/unit/cruds/move-columns.spec.ts b/test/unit/cruds/move-columns.spec.ts index f9228bb70..0e4a69cc5 100644 --- a/test/unit/cruds/move-columns.spec.ts +++ b/test/unit/cruds/move-columns.spec.ts @@ -146,7 +146,7 @@ describe('Move columns', () => { expect(engine.getCellValue(adr('E1'))).toEqual(3) }) - it('should adjust reference when swapping formula with dependency ', () => { + it('should adjust reference when swapping formula with dependency', () => { const engine = HyperFormula.buildFromArray([ ['1', '=A1'], ['=B2', '1'], diff --git a/test/unit/cruds/move-rows.spec.ts b/test/unit/cruds/move-rows.spec.ts index 6f479bc79..9d43d45d8 100644 --- a/test/unit/cruds/move-rows.spec.ts +++ b/test/unit/cruds/move-rows.spec.ts @@ -163,7 +163,7 @@ describe('Move rows', () => { expect(engine.getCellValue(adr('A5'))).toEqual(3) }) - it('should adjust reference when swapping formula with dependency ', () => { + it('should adjust reference when swapping formula with dependency', () => { const engine = HyperFormula.buildFromArray([ ['1'], ['=A1'], diff --git a/test/unit/cruds/removing-columns.spec.ts b/test/unit/cruds/removing-columns.spec.ts index d934a04a0..fb278786e 100644 --- a/test/unit/cruds/removing-columns.spec.ts +++ b/test/unit/cruds/removing-columns.spec.ts @@ -1,6 +1,6 @@ import {AlwaysDense, AlwaysSparse, ExportedCellChange, HyperFormula} from '../../../src' import {AbsoluteCellRange} from '../../../src/AbsoluteCellRange' -import {ArrayVertex, RangeVertex} from '../../../src/DependencyGraph' +import {ArrayFormulaVertex, RangeVertex} from '../../../src/DependencyGraph' import {ColumnIndex} from '../../../src/Lookup/ColumnIndex' import {CellAddress} from '../../../src/parser' import { @@ -317,7 +317,7 @@ describe('Address dependencies, Case 2: formula in sheet where we make crud with }) describe('Address dependencies, Case 3: formula in different sheet', () => { - it('case ARa: relative/absolute dependency after removed column should be shifted ', () => { + it('case ARa: relative/absolute dependency after removed column should be shifted', () => { const engine = HyperFormula.buildFromSheets({ Sheet1: [ ['=Sheet2!C1', '=Sheet2!C1', '=Sheet2!C1', '=Sheet2!$C1'], @@ -492,7 +492,7 @@ describe('Removing columns - reevaluation', () => { }) describe('Removing rows - arrays', () => { - it('ArrayVertex#formula should be updated', () => { + it('ArrayFormulaVertex#formula should be updated', () => { const engine = HyperFormula.buildFromArray([ ['1', '2', '3', '=TRANSPOSE(A1:C2)'], ['4', '5', '6'], @@ -503,7 +503,7 @@ describe('Removing rows - arrays', () => { expect(extractMatrixRange(engine, adr('C1'))).toEqual(new AbsoluteCellRange(adr('A1'), adr('B2'))) }) - it('ArrayVertex#address should be updated', () => { + it('ArrayFormulaVertex#address should be updated', () => { const engine = HyperFormula.buildFromArray([ ['1', '2', '3', '=TRANSPOSE(A1:C2)'], ['4', '5', '6'], @@ -511,11 +511,11 @@ describe('Removing rows - arrays', () => { engine.removeColumns(0, [1, 1]) - const matrixVertex = engine.addressMapping.fetchCell(adr('C1')) as ArrayVertex + const matrixVertex = engine.addressMapping.getCell(adr('C1')) as ArrayFormulaVertex expect(matrixVertex.getAddress(engine.lazilyTransformingAstService)).toEqual(adr('C1')) }) - it('ArrayVertex#formula should be updated when different sheets', () => { + it('ArrayFormulaVertex#formula should be updated when different sheets', () => { const engine = HyperFormula.buildFromSheets({ Sheet1: [ ['1', '2', '3'], @@ -573,7 +573,7 @@ describe('Removing rows - arrays', () => { ], {useArrayArithmetic: true})) }) - it('it should be REF if no space after removing column', () => { + it('should be REF if no space after removing column', () => { const engine = HyperFormula.buildFromArray([ ['=-C2:D2', null, 1], [null, null, 1, 2] @@ -594,7 +594,7 @@ describe('Removing rows - arrays', () => { expectEngineToBeTheSameAs(engine, expected) }) - it('it should be REF, not CYCLE, after removing columns', () => { + it('should be REF, not CYCLE, after removing columns', () => { const engine = HyperFormula.buildFromArray([ ['=-C1:D1', null, 1, 2] ], {useArrayArithmetic: true}) @@ -611,7 +611,7 @@ describe('Removing rows - arrays', () => { expectEngineToBeTheSameAs(engine, expected) }) - it('it should remove array when removing column with left corner', () => { + it('should remove array when removing column with left corner', () => { const engine = HyperFormula.buildFromArray([ ['1', '2', '=MMULT(A1:B2, A1:B2)'], ['3', '4'], @@ -625,7 +625,7 @@ describe('Removing rows - arrays', () => { ])) }) - it('it should remove array when removing columns with whole matrix', () => { + it('should remove array when removing columns with whole matrix', () => { const engine = HyperFormula.buildFromArray([ ['1', '2', '=MMULT(A1:B2, A1:B2)'], ['3', '4'], @@ -648,8 +648,8 @@ describe('Removing columns - graph', function() { engine.removeColumns(0, [2, 1]) - const b1 = engine.addressMapping.fetchCell(adr('b1')) - expect(engine.graph.adjacentNodes(b1)).toEqual(new Set()) + const b1 = engine.addressMapping.getCell(adr('b1')) + expect(engine.graph.adjacentNodes(b1!)).toEqual(new Set()) }) it('should remove vertices from graph', function() { @@ -702,9 +702,9 @@ describe('Removing columns - ranges', function() { engine.removeColumns(0, [0, 1]) - const range = engine.rangeMapping.fetchRange(adr('A1'), adr('B1')) - const a1 = engine.addressMapping.fetchCell(adr('A1')) - expect(engine.graph.existsEdge(a1, range)).toBe(true) + const range = engine.rangeMapping.getVertexOrThrow(adr('A1'), adr('B1')) + const a1 = engine.addressMapping.getCell(adr('A1')) + expect(engine.graph.existsEdge(a1!, range)).toBe(true) }) it('shift ranges in range mapping, range start before removed columns', () => { @@ -716,9 +716,9 @@ describe('Removing columns - ranges', function() { engine.removeColumns(0, [1, 2]) - const range = engine.rangeMapping.fetchRange(adr('A1'), adr('A1')) - const a1 = engine.addressMapping.fetchCell(adr('A1')) - expect(engine.graph.existsEdge(a1, range)).toBe(true) + const range = engine.rangeMapping.getVertexOrThrow(adr('A1'), adr('A1')) + const a1 = engine.addressMapping.getCell(adr('A1')) + expect(engine.graph.existsEdge(a1!, range)).toBe(true) }) it('shift ranges in range mapping, whole range', () => { @@ -726,7 +726,7 @@ describe('Removing columns - ranges', function() { ['1', '2', '3', '=SUM(A1:C1)'], /* */ ]) - const range = engine.rangeMapping.getRange(adr('A1'), adr('C1')) as RangeVertex + const range = engine.rangeMapping.getRangeVertex(adr('A1'), adr('C1')) as RangeVertex engine.removeColumns(0, [0, 3]) @@ -885,7 +885,7 @@ describe('Removing columns - merge ranges', () => { verifyRangesInSheet(engine, 0, []) verifyValues(engine) expect(engine.dependencyGraph.graph.getNodes().length).toBe(0) - expect(engine.dependencyGraph.rangeMapping.getMappingSize(0)).toBe(0) + expect(engine.dependencyGraph.rangeMapping.getNumberOfRangesInSheet(0)).toBe(0) }) it('should merge ranges in proper order', () => { diff --git a/test/unit/cruds/removing-rows.spec.ts b/test/unit/cruds/removing-rows.spec.ts index 865aed248..cf02c8b7d 100644 --- a/test/unit/cruds/removing-rows.spec.ts +++ b/test/unit/cruds/removing-rows.spec.ts @@ -1,6 +1,6 @@ import {ExportedCellChange, HyperFormula, InvalidArgumentsError} from '../../../src' import {AbsoluteCellRange} from '../../../src/AbsoluteCellRange' -import {ArrayVertex} from '../../../src/DependencyGraph' +import {ArrayFormulaVertex} from '../../../src/DependencyGraph' import {ColumnIndex} from '../../../src/Lookup/ColumnIndex' import {CellAddress} from '../../../src/parser' import { @@ -255,7 +255,7 @@ describe('Address dependencies, Case 2: formula in sheet where we make crud with }) describe('Address dependencies, Case 3: formula in different sheet', () => { - it('case ARa: relative/absolute dependency below removed row should be shifted ', () => { + it('case ARa: relative/absolute dependency below removed row should be shifted', () => { const engine = HyperFormula.buildFromSheets({ Sheet1: [ ['=Sheet2!A3'], @@ -548,7 +548,7 @@ describe('Removing rows - reevaluation', () => { }) describe('Removing rows - arrays', () => { - it('ArrayVertex#formula should be updated', () => { + it('ArrayFormulaVertex#formula should be updated', () => { const engine = HyperFormula.buildFromArray([ ['1', '4'], ['2', '5'], @@ -561,7 +561,7 @@ describe('Removing rows - arrays', () => { expect(extractMatrixRange(engine, adr('A3'))).toEqual(new AbsoluteCellRange(adr('A1'), adr('B2'))) }) - it('ArrayVertex#address should be updated', () => { + it('ArrayFormulaVertex#address should be updated', () => { const engine = HyperFormula.buildFromArray([ ['1', '4'], ['2', '5'], @@ -571,11 +571,11 @@ describe('Removing rows - arrays', () => { engine.removeRows(0, [1, 1]) - const matrixVertex = engine.addressMapping.fetchCell(adr('A3')) as ArrayVertex + const matrixVertex = engine.addressMapping.getCell(adr('A3')) as ArrayFormulaVertex expect(matrixVertex.getAddress(engine.lazilyTransformingAstService)).toEqual(adr('A3')) }) - it('ArrayVertex#formula should be updated when different sheets', () => { + it('ArrayFormulaVertex#formula should be updated when different sheets', () => { const engine = HyperFormula.buildFromSheets({ Sheet1: [ ['1', '4'], @@ -648,7 +648,7 @@ describe('Removing rows - arrays', () => { ], {useArrayArithmetic: true})) }) - it('it should be REF if no space after removing row', () => { + it('should be REF if no space after removing row', () => { const engine = HyperFormula.buildFromArray([ ['=-B3:B4'], [], @@ -672,7 +672,7 @@ describe('Removing rows - arrays', () => { expectEngineToBeTheSameAs(engine, expected) }) - it('it should be REF, not CYCLE, after removing rows', () => { + it('should be REF, not CYCLE, after removing rows', () => { const engine = HyperFormula.buildFromArray([ ['=-A3:A4'], [], @@ -696,7 +696,7 @@ describe('Removing rows - arrays', () => { expectEngineToBeTheSameAs(engine, expected) }) - it('it should remove array when removing row with left corner', () => { + it('should remove array when removing row with left corner', () => { const engine = HyperFormula.buildFromArray([ ['1', '2'], ['3', '4'], @@ -711,7 +711,7 @@ describe('Removing rows - arrays', () => { ])) }) - it('it should remove array when removing rows with whole matrix', () => { + it('should remove array when removing rows with whole matrix', () => { const engine = HyperFormula.buildFromArray([ ['1', '2'], ['3', '4'], @@ -737,8 +737,8 @@ describe('Removing rows - graph', function() { engine.removeRows(0, [2, 1]) - const a2 = engine.addressMapping.fetchCell(adr('A2')) - expect(engine.graph.adjacentNodes(a2)).toEqual(new Set()) + const a2 = engine.addressMapping.getCell(adr('A2')) + expect(engine.graph.adjacentNodes(a2!)).toEqual(new Set()) }) it('should remove vertices from graph', function() { @@ -772,9 +772,9 @@ describe('Removing rows - range mapping', function() { ]) engine.removeRows(0, [0, 1]) - const range = engine.rangeMapping.fetchRange(adr('A1'), adr('A2')) - const a1 = engine.addressMapping.fetchCell(adr('A1')) - expect(engine.graph.existsEdge(a1, range)).toBe(true) + const range = engine.rangeMapping.getVertexOrThrow(adr('A1'), adr('A2')) + const a1 = engine.addressMapping.getCell(adr('A1')) + expect(engine.graph.existsEdge(a1!, range)).toBe(true) }) it('shift ranges in range mapping, range start above removed rows', () => { @@ -785,9 +785,9 @@ describe('Removing rows - range mapping', function() { ]) engine.removeRows(0, [1, 2]) - const range = engine.rangeMapping.fetchRange(adr('A1'), adr('A1')) - const a1 = engine.addressMapping.fetchCell(adr('A1')) - expect(engine.graph.existsEdge(a1, range)).toBe(true) + const range = engine.rangeMapping.getVertexOrThrow(adr('A1'), adr('A1')) + const a1 = engine.addressMapping.getCell(adr('A1')) + expect(engine.graph.existsEdge(a1!, range)).toBe(true) }) it('shift ranges in range mapping, whole range', () => { @@ -798,7 +798,7 @@ describe('Removing rows - range mapping', function() { ['=SUM(A1:A3)'], ]) - const range = engine.rangeMapping.fetchRange(adr('A1'), adr('A3')) + const range = engine.rangeMapping.getVertexOrThrow(adr('A1'), adr('A3')) engine.removeRows(0, [0, 3]) const ranges = Array.from(engine.rangeMapping.rangesInSheet(0)) expect(ranges.length).toBe(0) @@ -814,10 +814,10 @@ describe('Removing rows - range mapping', function() { ['=SUM(A1:A3)'], ]) - const a1a3 = engine.rangeMapping.fetchRange(adr('A1'), adr('A3')) + const a1a3 = engine.rangeMapping.getVertexOrThrow(adr('A1'), adr('A3')) expect(graphReversedAdjacentNodes(engine.graph, a1a3).length).toBe(2) engine.removeRows(0, [0, 2]) - const a1a1 = engine.rangeMapping.fetchRange(adr('A1'), adr('A1')) + const a1a1 = engine.rangeMapping.getVertexOrThrow(adr('A1'), adr('A1')) expect(a1a1).toBe(a1a3) expect(graphReversedAdjacentNodes(engine.graph, a1a1).length).toBe(1) }) @@ -1028,7 +1028,7 @@ describe('Removing rows - merge ranges', () => { verifyRangesInSheet(engine, 0, []) verifyValues(engine) expect(engine.dependencyGraph.graph.getNodes().length).toBe(0) - expect(engine.dependencyGraph.rangeMapping.getMappingSize(0)).toBe(0) + expect(engine.dependencyGraph.rangeMapping.getNumberOfRangesInSheet(0)).toBe(0) }) it('should merge ranges in proper order', () => { diff --git a/test/unit/cruds/removing-sheet.spec.ts b/test/unit/cruds/removing-sheet.spec.ts index 3ee8f6b77..08c39c4ee 100644 --- a/test/unit/cruds/removing-sheet.spec.ts +++ b/test/unit/cruds/removing-sheet.spec.ts @@ -1,15 +1,15 @@ -import {ExportedCellChange, HyperFormula, NoSheetWithIdError} from '../../../src' +import {ExportedCellChange, HyperFormula, NoSheetWithIdError, CellValueType} from '../../../src' import {AbsoluteCellRange} from '../../../src/AbsoluteCellRange' import {ErrorType} from '../../../src/Cell' -import {ArrayVertex} from '../../../src/DependencyGraph' +import {ArrayFormulaVertex} from '../../../src/DependencyGraph' +import { ErrorMessage } from '../../../src/error-message' import {ColumnIndex} from '../../../src/Lookup/ColumnIndex' import {CellAddress} from '../../../src/parser' import { adr, + detailedError, detailedErrorWithOrigin, expectArrayWithSameContent, - expectEngineToBeTheSameAs, - expectReferenceToHaveRefError, extractReference, } from '../testUtils' @@ -17,13 +17,13 @@ describe('Removing sheet - checking if its possible', () => { it('no if theres no such sheet', () => { const engine = HyperFormula.buildFromArray([[]]) - expect(engine.isItPossibleToRemoveSheet(1)).toEqual(false) + expect(engine.isItPossibleToRemoveSheet(1)).toBe(false) }) it('yes otherwise', () => { const engine = HyperFormula.buildFromArray([[]]) - expect(engine.isItPossibleToRemoveSheet(0)).toEqual(true) + expect(engine.isItPossibleToRemoveSheet(0)).toBe(true) }) }) @@ -54,33 +54,6 @@ describe('remove sheet', () => { expect(Array.from(engine.addressMapping.entries())).toEqual([]) }) - it('should decrease last sheet id when removing last sheet', () => { - const engine = HyperFormula.buildFromSheets({ - Sheet1: [], - Sheet2: [], - }) - - engine.removeSheet(1) - - expect(Array.from(engine.sheetMapping.displayNames())).toEqual(['Sheet1']) - engine.addSheet() - expect(Array.from(engine.sheetMapping.displayNames())).toEqual(['Sheet1', 'Sheet2']) - }) - - it('should not decrease last sheet id when removing sheet other than last', () => { - const engine = HyperFormula.buildFromSheets({ - Sheet1: [], - Sheet2: [], - Sheet3: [], - }) - - engine.removeSheet(1) - - expect(Array.from(engine.sheetMapping.displayNames())).toEqual(['Sheet1', 'Sheet3']) - engine.addSheet() - expect(Array.from(engine.sheetMapping.displayNames())).toEqual(['Sheet1', 'Sheet3', 'Sheet4']) - }) - it('should remove sheet with matrix', () => { const engine = HyperFormula.buildFromSheets({ Sheet1: [ @@ -136,11 +109,75 @@ describe('remove sheet', () => { engine.removeSheet(0) - expect(Array.from(engine.sheetMapping.displayNames())).toEqual(['Sheet2']) + expect(Array.from(engine.sheetMapping.iterateSheetNames())).toEqual(['Sheet2']) expect(engine.getCellValue(adr('A1', 1))).toBe(1) expect(engine.getCellValue(adr('A2', 1))).toBe(2) expect(engine.getCellValue(adr('A3', 1))).toBe(3) }) + + it('converts sheet to placeholder if other sheet depends on it', () => { + const engine = HyperFormula.buildFromSheets({ + Sheet1: [[42]], + Sheet2: [['=Sheet1!A1']], + }) + + const sheet1Id = engine.getSheetId('Sheet1')! + + engine.removeSheet(sheet1Id) + + expect(engine.sheetMapping.hasSheetWithId(sheet1Id, { includePlaceholders: false })).toBe(false) + expect(engine.sheetMapping.hasSheetWithId(sheet1Id, { includePlaceholders: true })).toBe(true) + }) + + it('removes sheet completely if nothing depends on it', () => { + const engine = HyperFormula.buildFromSheets({ + Sheet1: [[42]], + Sheet2: [[100]], + }) + + const sheet1Id = engine.getSheetId('Sheet1')! + + engine.removeSheet(sheet1Id) + + expect(engine.sheetMapping.hasSheetWithId(sheet1Id, { includePlaceholders: false })).toBe(false) + expect(engine.sheetMapping.hasSheetWithId(sheet1Id, { includePlaceholders: true })).toBe(false) + }) + + it('removes the placeholder sheet if nothing depends on it any longer', () => { + const engine = HyperFormula.buildFromSheets({ + Sheet1: [[42]], + Sheet2: [['=Sheet1!A1']], + }) + + const sheet1Id = engine.getSheetId('Sheet1')! + const sheet2Id = engine.getSheetId('Sheet2')! + + engine.removeSheet(sheet1Id) + + expect(engine.sheetMapping.hasSheetWithId(sheet1Id, { includePlaceholders: false })).toBe(false) + expect(engine.sheetMapping.hasSheetWithId(sheet1Id, { includePlaceholders: true })).toBe(true) + + engine.setCellContents(adr('A1', sheet2Id), 100) + + expect(engine.sheetMapping.hasSheetWithId(sheet1Id, { includePlaceholders: false })).toBe(false) + expect(engine.sheetMapping.hasSheetWithId(sheet1Id, { includePlaceholders: true })).toBe(false) + }) + + it('decreases lastSheetId if removed sheet was the last one', () => { + const engine = HyperFormula.buildFromSheets({ + Sheet1: [[1]], + Sheet2: [[2]], + }) + + const sheet2Id = engine.getSheetId('Sheet2')! + + engine.removeSheet(sheet2Id) + + engine.addSheet('Sheet3') + const sheet3Id = engine.getSheetId('Sheet3')! + + expect(sheet3Id).toBe(sheet2Id) // new sheet reuses the ID + }) }) describe('remove sheet - adjust edges', () => { @@ -156,10 +193,10 @@ describe('remove sheet - adjust edges', () => { engine.removeSheet(1) - const a1 = engine.addressMapping.fetchCell(adr('A1')) - const b1 = engine.addressMapping.fetchCell(adr('B1')) + const a1 = engine.addressMapping.getCell(adr('A1')) + const b1 = engine.addressMapping.getCell(adr('B1')) - expect(engine.graph.existsEdge(a1, b1)).toBe(true) + expect(engine.graph.existsEdge(a1!, b1!)).toBe(true) }) it('should remove edge between sheets', () => { @@ -172,13 +209,14 @@ describe('remove sheet - adjust edges', () => { ], }) - const a1From0 = engine.addressMapping.fetchCell(adr('A1')) - const a1From1 = engine.addressMapping.fetchCell(adr('A1', 1)) - expect(engine.graph.existsEdge(a1From1, a1From0)).toBe(true) + const a1From0 = engine.addressMapping.getCell(adr('A1')) + const a1From1 = engine.addressMapping.getCell(adr('A1', 1)) + + expect(engine.graph.existsEdge(a1From1!, a1From0!)).toBe(true) engine.removeSheet(1) - expect(engine.graph.existsEdge(a1From1, a1From0)).toBe(false) + expect(engine.graph.existsEdge(a1From1!, a1From0!)).toBe(false) }) }) @@ -198,28 +236,34 @@ describe('remove sheet - adjust formula dependencies', () => { const reference = extractReference(engine, adr('B1')) expect(reference).toEqual(CellAddress.relative(-1, 0)) - expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([['1', '=A1']])) + expect(engine.getAllSheetsSerialized()).toEqual({Sheet1: [['1', '=A1']]}) + expect(engine.getAllSheetsValues()).toEqual({Sheet1: [[1, 1]]}) }) it('should be #REF after removing sheet', () => { + const sheet1Name = 'Sheet1' + const sheet2Name = 'Sheet2' const engine = HyperFormula.buildFromSheets({ - Sheet1: [ + [sheet1Name]: [ ['=Sheet2!A1'], ['=Sheet2!A1:A2'], ['=Sheet2!A:B'], ['=Sheet2!1:2'], ], - Sheet2: [ + [sheet2Name]: [ ['1'], ], }) - engine.removeSheet(1) + const sheet1Id = engine.getSheetId(sheet1Name)! + const sheet2Id = engine.getSheetId(sheet2Name)! + + engine.removeSheet(sheet2Id) - expectReferenceToHaveRefError(engine, adr('A1')) - expectReferenceToHaveRefError(engine, adr('A2')) - expectReferenceToHaveRefError(engine, adr('A3')) - expectReferenceToHaveRefError(engine, adr('A4')) + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('A2', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('A3', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('A4', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) }) it('should return changed values', () => { @@ -235,17 +279,465 @@ describe('remove sheet - adjust formula dependencies', () => { const changes = engine.removeSheet(1) expect(changes.length).toBe(1) - expect(changes).toContainEqual(new ExportedCellChange(adr('A1'), detailedErrorWithOrigin(ErrorType.REF, 'Sheet1!A1'))) + expect(changes).toContainEqual(new ExportedCellChange(adr('A1'), detailedErrorWithOrigin(ErrorType.REF, 'Sheet1!A1', ErrorMessage.SheetRef))) + }) + +}) + +describe('removeSheet() recalculates formulas (issue #1116)', () => { + it('returns REF error if other sheet depends on the removed one', () => { + const table1Name = 'table1' + const table2Name = 'table2' + const engine = HyperFormula.buildFromSheets({ + [table1Name]: [[`='${table2Name}'!A1`]], + [table2Name]: [[10]], + }) + + expect(engine.getCellValue(adr('A1', engine.getSheetId(table1Name)))).toBe(10) + expect(engine.getCellValue(adr('A1', engine.getSheetId(table2Name)))).toBe(10) + + engine.removeSheet(engine.getSheetId(table2Name)!) + + expect(engine.getCellValue(adr('A1', engine.getSheetId(table1Name)))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('A1', engine.getSheetId(table2Name)))).toEqualError(detailedError(ErrorType.REF)) + }) + + it('returns REF error for chained dependencies across multiple sheets', () => { + const sheet1Name = 'Sheet1' + const sheet2Name = 'Sheet2' + const sheet3Name = 'Sheet3' + const engine = HyperFormula.buildFromSheets({ + [sheet1Name]: [[`='${sheet2Name}'!A1+2`]], + [sheet2Name]: [[`='${sheet3Name}'!A1*2`]], + [sheet3Name]: [[42]], + }) + + expect(engine.getCellValue(adr('A1', engine.getSheetId(sheet2Name)))).toBe(84) + expect(engine.getCellValue(adr('A1', engine.getSheetId(sheet1Name)))).toBe(86) + + engine.removeSheet(engine.getSheetId(sheet3Name)!) + + expect(engine.getCellValue(adr('A1', engine.getSheetId(sheet2Name)))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('A1', engine.getSheetId(sheet1Name)))).toEqualError(detailedError(ErrorType.REF)) + }) + + it('returns REF error for nested dependencies within same sheet referencing removed sheet', () => { + const sheet1Name = 'Sheet1' + const removedSheetName = 'RemovedSheet' + const engine = HyperFormula.buildFromSheets({ + [sheet1Name]: [['=B1*2', `='${removedSheetName}'!A1`]], + [removedSheetName]: [[15]], + }) + + expect(engine.getCellValue(adr('B1', engine.getSheetId(sheet1Name)))).toBe(15) + expect(engine.getCellValue(adr('A1', engine.getSheetId(sheet1Name)))).toBe(30) + + engine.removeSheet(engine.getSheetId(removedSheetName)!) + + expect(engine.getCellValue(adr('B1', engine.getSheetId(sheet1Name)))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('A1', engine.getSheetId(sheet1Name)))).toEqualError(detailedError(ErrorType.REF)) + }) + + it('returns REF error for multiple cells from different sheets referencing removed sheet', () => { + const sheet1Name = 'Sheet1' + const sheet2Name = 'Sheet2' + const targetSheetName = 'TargetSheet' + const engine = HyperFormula.buildFromSheets({ + [sheet1Name]: [[`='${targetSheetName}'!A1`, `='${targetSheetName}'!B1`]], + [sheet2Name]: [[`='${targetSheetName}'!A1+10`, `='${targetSheetName}'!B1+20`]], + [targetSheetName]: [[5, 7]], + }) + + expect(engine.getCellValue(adr('A1', engine.getSheetId(sheet1Name)))).toBe(5) + expect(engine.getCellValue(adr('B1', engine.getSheetId(sheet1Name)))).toBe(7) + expect(engine.getCellValue(adr('A1', engine.getSheetId(sheet2Name)))).toBe(15) + expect(engine.getCellValue(adr('B1', engine.getSheetId(sheet2Name)))).toBe(27) + + engine.removeSheet(engine.getSheetId(targetSheetName)!) + + expect(engine.getCellValue(adr('A1', engine.getSheetId(sheet1Name)))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('B1', engine.getSheetId(sheet1Name)))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('A1', engine.getSheetId(sheet2Name)))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('B1', engine.getSheetId(sheet2Name)))).toEqualError(detailedError(ErrorType.REF)) + }) + + it('returns REF error for formulas with mixed operations combining removed sheet references', () => { + const sheet1Name = 'Sheet1' + const removedSheetName = 'RemovedSheet' + const engine = HyperFormula.buildFromSheets({ + [sheet1Name]: [[100, `='${removedSheetName}'!A1 + A1`, `='${removedSheetName}'!B1 * 2`]], + [removedSheetName]: [[50, 25]], + }) + + expect(engine.getCellValue(adr('B1', engine.getSheetId(sheet1Name)))).toBe(150) + expect(engine.getCellValue(adr('C1', engine.getSheetId(sheet1Name)))).toBe(50) + + engine.removeSheet(engine.getSheetId(removedSheetName)!) + + expect(engine.getCellValue(adr('B1', engine.getSheetId(sheet1Name)))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('C1', engine.getSheetId(sheet1Name)))).toEqualError(detailedError(ErrorType.REF)) + }) + + it('returns REF error for formulas with multi-cell ranges from removed sheet', () => { + const sheet1Name = 'Sheet1' + const dataSheetName = 'DataSheet' + const engine = HyperFormula.buildFromSheets({ + [sheet1Name]: [ + [`=SUM('${dataSheetName}'!A1:B5)`], + [`=MEDIAN('${dataSheetName}'!A1:B5)`], + ], + [dataSheetName]: [ + [1, 2], + [3, 4], + [5, 6], + [7, 8], + [9, 10], + ], + }) + + expect(engine.getCellValue(adr('A1', engine.getSheetId(sheet1Name)))).toBe(55) + expect(engine.getCellValue(adr('A2', engine.getSheetId(sheet1Name)))).toBe(5.5) + + engine.removeSheet(engine.getSheetId(dataSheetName)!) + + expect(engine.getCellValue(adr('A1', engine.getSheetId(sheet1Name)))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('A2', engine.getSheetId(sheet1Name)))).toEqualError(detailedError(ErrorType.REF)) + }) + + it('returns REF error for named expressions referencing removed sheet', () => { + const sheet1Name = 'Sheet1' + const removedSheetName = 'RemovedSheet' + const engine = HyperFormula.buildFromSheets({ + [sheet1Name]: [['=MyValue'], ['=MyValue*2']], + [removedSheetName]: [[99]] + }, {}, [ + { name: 'MyValue', expression: `='${removedSheetName}'!$A$1` } + ]) + + expect(engine.getCellValue(adr('A1', engine.getSheetId(sheet1Name)))).toBe(99) + expect(engine.getCellValue(adr('A2', engine.getSheetId(sheet1Name)))).toBe(198) + + engine.removeSheet(engine.getSheetId(removedSheetName)!) + + expect(engine.getCellValue(adr('A1', engine.getSheetId(sheet1Name)))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('A2', engine.getSheetId(sheet1Name)))).toEqualError(detailedError(ErrorType.REF)) + }) + + it('handles add-remove-add cycle correctly', () => { + const engine = HyperFormula.buildEmpty() + const sheet1Name = 'Sheet1' + const sheet2Name = 'Sheet2' + + engine.addSheet(sheet1Name) + const sheet1Id = engine.getSheetId(sheet1Name)! + engine.setCellContents(adr('A1', sheet1Id), `='${sheet2Name}'!A1`) + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + + engine.addSheet(sheet2Name) + const oldSheet2Id = engine.getSheetId(sheet2Name)! + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBeNull() + + engine.setCellContents(adr('A1', oldSheet2Id), 42) + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(42) + expect(engine.getCellValue(adr('A1', oldSheet2Id))).toBe(42) + + engine.removeSheet(oldSheet2Id) + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('A1', oldSheet2Id))).toEqualError(detailedError(ErrorType.REF)) + + engine.addSheet(sheet2Name) + let newSheet2Id = engine.getSheetId(sheet2Name)! + engine.setCellContents(adr('A1', newSheet2Id), 43) + + expect(newSheet2Id).toBe(oldSheet2Id) + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(43) + expect(engine.getCellValue(adr('A1', newSheet2Id))).toBe(43) + + engine.removeSheet(oldSheet2Id) + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('A1', oldSheet2Id))).toEqualError(detailedError(ErrorType.REF)) + + engine.addSheet(sheet2Name) + newSheet2Id = engine.getSheetId(sheet2Name)! + engine.setCellContents(adr('A1', newSheet2Id), 44) + + expect(newSheet2Id).toBe(oldSheet2Id) + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(44) + expect(engine.getCellValue(adr('A1', newSheet2Id))).toBe(44) + + engine.removeSheet(oldSheet2Id) + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('A1', oldSheet2Id))).toEqualError(detailedError(ErrorType.REF)) + + engine.addSheet(sheet2Name) + newSheet2Id = engine.getSheetId(sheet2Name)! + engine.setCellContents(adr('A1', newSheet2Id), 45) + + expect(newSheet2Id).toBe(oldSheet2Id) + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(45) + expect(engine.getCellValue(adr('A1', newSheet2Id))).toBe(45) + }) + + it('REF error propagates through dependency chain when source sheet is removed', () => { + const engine = HyperFormula.buildFromSheets({ + 'Main': [['=Intermediate!A1*2']], + 'Intermediate': [['=Source!A1+10']], + 'Source': [[5]], + }) + const mainId = engine.getSheetId('Main')! + const intermediateId = engine.getSheetId('Intermediate')! + const sourceId = engine.getSheetId('Source')! + + expect(engine.getCellValue(adr('A1', mainId))).toBe(30) + expect(engine.getCellValue(adr('A1', intermediateId))).toBe(15) + + // Remove source sheet - error should propagate through chain + engine.removeSheet(sourceId) + + expect(engine.getCellValue(adr('A1', intermediateId))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('A1', mainId))).toEqualError(detailedError(ErrorType.REF)) + + // Re-add the sheet to resolve the errors + engine.addSheet('Source') + engine.setCellContents(adr('A1', engine.getSheetId('Source')), 5) + + expect(engine.getCellValue(adr('A1', intermediateId))).toBe(15) + expect(engine.getCellValue(adr('A1', mainId))).toBe(30) + }) + + it('removing sheet creates REF and adding it back resolves it', () => { + const engine = HyperFormula.buildFromSheets({ + 'Main': [['=Data!A1']], + 'Data': [[42]], + }) + const mainId = engine.getSheetId('Main')! + const dataId = engine.getSheetId('Data')! + + expect(engine.getCellValue(adr('A1', mainId))).toBe(42) + + engine.removeSheet(dataId) + + expect(engine.getCellValue(adr('A1', mainId))).toEqualError(detailedError(ErrorType.REF)) + + engine.addSheet('Data') + engine.setCellContents(adr('A1', engine.getSheetId('Data')), 99) + + expect(engine.getCellValue(adr('A1', mainId))).toBe(99) + }) + + describe('when using ranges with', () => { + it('function using `runFunction`', () => { + const sheet1Name = 'FirstSheet' + const sheet2Name = 'NewSheet' + const sheet1Data = [['=MEDIAN(NewSheet!A1:A1)', '=MEDIAN(NewSheet!A1:A2)', '=MEDIAN(NewSheet!A1:A3)', '=MEDIAN(NewSheet!A1:A4)']] + const sheet2Data = [[1], [2], [3], [4]] + const engine = HyperFormula.buildFromSheets({ + [sheet1Name]: sheet1Data, + [sheet2Name]: sheet2Data, + }) + + const sheet1Id = engine.getSheetId(sheet1Name)! + const sheet2Id = engine.getSheetId(sheet2Name)! + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(1) + expect(engine.getCellValue(adr('B1', sheet1Id))).toBe(1.5) + expect(engine.getCellValue(adr('C1', sheet1Id))).toBe(2) + expect(engine.getCellValue(adr('D1', sheet1Id))).toBe(2.5) + + engine.removeSheet(sheet2Id) + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('B1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('C1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('D1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + }) + + it('function not using `runFunction`', () => { + const sheet1Name = 'FirstSheet' + const sheet2Name = 'NewSheet' + const sheet1Data = [['=SUM(NewSheet!A1:A1)', '=SUM(NewSheet!A1:A2)', '=SUM(NewSheet!A1:A3)', '=SUM(NewSheet!A1:A4)']] + const sheet2Data = [[1], [2], [3], [4]] + const engine = HyperFormula.buildFromSheets({ + [sheet1Name]: sheet1Data, + [sheet2Name]: sheet2Data, + }) + + const sheet1Id = engine.getSheetId(sheet1Name)! + const sheet2Id = engine.getSheetId(sheet2Name)! + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(1) + expect(engine.getCellValue(adr('B1', sheet1Id))).toBe(3) + expect(engine.getCellValue(adr('C1', sheet1Id))).toBe(6) + expect(engine.getCellValue(adr('D1', sheet1Id))).toBe(10) + + engine.removeSheet(sheet2Id) + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('B1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('C1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('D1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + }) + + it('function using `runFunction` referencing range indirectly', () => { + const sheet1Name = 'FirstSheet' + const sheet2Name = 'NewSheet' + const sheet1Data = [ + ['=MEDIAN(A2)', '=MEDIAN(B2)', '=MEDIAN(C2)', '=MEDIAN(D2)'], + [`='${sheet2Name}'!A1:A1`, `='${sheet2Name}'!A1:B2`, `='${sheet2Name}'!A1:A3`, `='${sheet2Name}'!A1:A4`], + ] + const sheet2Data = [[1], [2], [3], [4]] + const engine = HyperFormula.buildFromSheets({ + [sheet1Name]: sheet1Data, + [sheet2Name]: sheet2Data, + }) + + const sheet1Id = engine.getSheetId(sheet1Name)! + const sheet2Id = engine.getSheetId(sheet2Name)! + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(1) + expect(engine.getCellValue(adr('B1', sheet1Id))).toBe(1.5) + expect(engine.getCellValue(adr('C1', sheet1Id))).toBe(2) + expect(engine.getCellValue(adr('D1', sheet1Id))).toBe(2.5) + + engine.removeSheet(sheet2Id) + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('B1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('C1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('D1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + }) + + it('function not using `runFunction` referencing range indirectly', () => { + const sheet1Name = 'FirstSheet' + const sheet2Name = 'NewSheet' + const sheet1Data = [ + ['=SUM(A2)', '=SUM(B2)', '=SUM(C2)', '=SUM(D2)'], + [`='${sheet2Name}'!A1:A1`, `='${sheet2Name}'!A1:B2`, `='${sheet2Name}'!A1:A3`, `='${sheet2Name}'!A1:A4`], + ] + const sheet2Data = [[1], [2], [3], [4]] + const engine = HyperFormula.buildFromSheets({ + [sheet1Name]: sheet1Data, + [sheet2Name]: sheet2Data, + }) + + const sheet1Id = engine.getSheetId(sheet1Name)! + const sheet2Id = engine.getSheetId(sheet2Name)! + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(1) + expect(engine.getCellValue(adr('B1', sheet1Id))).toBe(3) + expect(engine.getCellValue(adr('C1', sheet1Id))).toBe(6) + expect(engine.getCellValue(adr('D1', sheet1Id))).toBe(10) + + engine.removeSheet(sheet2Id) + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('B1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('C1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('D1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + }) + + it('function calling a named expression', () => { + const sheet1Name = 'FirstSheet' + const sheet2Name = 'NewSheet' + const sheet1Data = [[`='${sheet2Name}'!A1:A4`]] + const sheet2Data = [[1], [2], [3], [4]] + const engine = HyperFormula.buildFromSheets({ + [sheet1Name]: sheet1Data, + [sheet2Name]: sheet2Data, + }, {}, [ + { name: 'ExprA', expression: `=MEDIAN(${sheet2Name}!$A$1:$A$1)` }, + { name: 'ExprB', expression: `=MEDIAN(${sheet2Name}!$A$1:$A$2)` }, + { name: 'ExprC', expression: `=MEDIAN(${sheet2Name}!$A$1:$A$3)` }, + { name: 'ExprD', expression: `=MEDIAN(${sheet1Name}!$A$1)` } + ]) + + const sheet2Id = engine.getSheetId(sheet2Name)! + + expect(engine.getNamedExpressionValue('ExprA')).toBe(1) + expect(engine.getNamedExpressionValue('ExprB')).toBe(1.5) + expect(engine.getNamedExpressionValue('ExprC')).toBe(2) + expect(engine.getNamedExpressionValue('ExprD')).toBe(2.5) + + engine.removeSheet(sheet2Id) + + expect(engine.getNamedExpressionValue('ExprA')).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getNamedExpressionValue('ExprB')).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getNamedExpressionValue('ExprC')).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getNamedExpressionValue('ExprD')).toEqualError(detailedError(ErrorType.REF)) + }) }) }) describe('remove sheet - adjust address mapping', () => { - it('should remove sheet from address mapping', () => { - const engine = HyperFormula.buildFromArray([]) + it('should remove sheet from address mapping if nothing depends on it', () => { + const sheet1Name = 'Sheet1' - engine.removeSheet(0) + const engine = HyperFormula.buildFromSheets({ + [sheet1Name]: [[42]] + }) - expect(() => engine.addressMapping.strategyFor(0)).toThrowError("There's no sheet with id = 0") + const sheet1Id = engine.getSheetId(sheet1Name)! + engine.removeSheet(sheet1Id) + + expect(() => engine.addressMapping.getStrategyForSheetOrThrow(sheet1Id)).toThrow(new NoSheetWithIdError(sheet1Id)) + }) + + it('should not remove sheet from address mapping if another sheet depends on it', () => { + const sheet1Name = 'Sheet1' + const sheet2Name = 'Sheet2' + + const engine = HyperFormula.buildFromSheets({ + [sheet1Name]: [[42]], + [sheet2Name]: [[`='${sheet1Name}'!A1`]], + }) + + const sheet1Id = engine.getSheetId(sheet1Name)! + + engine.removeSheet(sheet1Id) + + expect(() => engine.addressMapping.getStrategyForSheetOrThrow(sheet1Id)).not.toThrow() + }) + + it('should not remove sheet from address mapping if a named expression depends on it', () => { + const sheet1Name = 'Sheet1' + const engine = HyperFormula.buildFromSheets({ + [sheet1Name]: [[42]], + }, {}, [ + { name: 'namedExpressionName', expression: `='${sheet1Name}'!$A$1` }, + ]) + + const sheet1Id = engine.getSheetId(sheet1Name)! + + engine.removeSheet(sheet1Id) + + expect(() => engine.addressMapping.getStrategyForSheetOrThrow(sheet1Id)).not.toThrow() + }) + + it('removes the placeholder sheet from address mapping if nothing depends on it any longer', () => { + const sheet1Name = 'Sheet1' + const sheet2Name = 'Sheet2' + + const engine = HyperFormula.buildFromSheets({ + [sheet1Name]: [[42]], + [sheet2Name]: [[`='${sheet1Name}'!A1`]], + }) + + const sheet1Id = engine.getSheetId(sheet1Name)! + const sheet2Id = engine.getSheetId(sheet2Name)! + + engine.removeSheet(sheet1Id) + + expect(() => engine.addressMapping.getStrategyForSheetOrThrow(sheet1Id)).not.toThrow() + + engine.setCellContents(adr('A1', sheet2Id), 100) + + expect(() => engine.addressMapping.getStrategyForSheetOrThrow(sheet1Id)).toThrow(new NoSheetWithIdError(sheet1Id)) }) }) @@ -284,12 +776,13 @@ describe('remove sheet - adjust matrix mapping', () => { ['=TRANSPOSE(A1:B1)'], ], }) - expect(engine.arrayMapping.getArray(AbsoluteCellRange.spanFrom(adr('A2'), 1, 2))).toBeInstanceOf(ArrayVertex) + + expect(engine.arrayMapping.getArray(AbsoluteCellRange.spanFrom(adr('A2'), 1, 2))).toBeInstanceOf(ArrayFormulaVertex) engine.removeSheet(0) expect(engine.arrayMapping.getArray(AbsoluteCellRange.spanFrom(adr('A2'), 1, 2))).toBeUndefined() - expect(engine.arrayMapping.getArray(AbsoluteCellRange.spanFrom(adr('A2', 1), 1, 2))).toBeInstanceOf(ArrayVertex) + expect(engine.arrayMapping.getArray(AbsoluteCellRange.spanFrom(adr('A2', 1), 1, 2))).toBeInstanceOf(ArrayFormulaVertex) }) }) @@ -303,7 +796,65 @@ describe('remove sheet - adjust column index', () => { engine.removeSheet(0) - expect(removeSheetSpy).toHaveBeenCalled() + expect(removeSheetSpy).toHaveBeenCalledWith(0) expectArrayWithSameContent([], index.getValueIndex(0, 0, 1).index) }) }) + +describe('remove sheet - placeholder sheet behavior', () => { + it('should return ERROR type when getting cell value from a placeholder sheet', () => { + const engine = HyperFormula.buildFromSheets({ + Sheet1: [[42]], + Sheet2: [['=Sheet1!A1']], + }) + + const sheet1Id = engine.getSheetId('Sheet1')! + + engine.removeSheet(sheet1Id) + + expect(engine.getCellValueType(adr('A1', sheet1Id))).toBe('ERROR') + }) + + it('should return null when getting serialized cell from a placeholder sheet', () => { + const engine = HyperFormula.buildFromSheets({ + Sheet1: [[42]], + Sheet2: [['=Sheet1!A1']], + }) + + const sheet1Id = engine.getSheetId('Sheet1')! + + engine.removeSheet(sheet1Id) + + const result = engine.getCellSerialized(adr('A1', sheet1Id)) + expect(result).toBeNull() + }) + + it('should remove range vertices when clearing formulas that use ranges', () => { + const engine = HyperFormula.buildFromArray([ + [1, 2, 3], + ['=SUM(A1:C1)'], + ]) + + expect(engine.rangeMapping.getNumberOfRangesInSheet(0)).toBe(1) + + engine.setCellContents(adr('A2'), null) + + expect(engine.rangeMapping.getNumberOfRangesInSheet(0)).toBe(0) + }) + + it('should remove range vertices when removing sheet with ranges', () => { + const engine = HyperFormula.buildFromSheets({ + Sheet1: [ + [1, 2, 3], + ['=SUM(A1:C1)'], + ], + Sheet2: [[100]], + }) + + expect(engine.rangeMapping.getNumberOfRangesInSheet(0)).toBe(1) + + engine.removeSheet(0) + + expect(engine.rangeMapping.getNumberOfRangesInSheet(0)).toBe(0) + }) +}) diff --git a/test/unit/cruds/rename-sheet.spec.ts b/test/unit/cruds/rename-sheet.spec.ts index 22a62f072..1114f63ff 100644 --- a/test/unit/cruds/rename-sheet.spec.ts +++ b/test/unit/cruds/rename-sheet.spec.ts @@ -1,34 +1,36 @@ import {HyperFormula, NoSheetWithIdError, SheetNameAlreadyTakenError} from '../../../src' +import {ErrorType} from '../../../src/Cell' +import {adr, detailedError} from '../testUtils' -describe('Is it possible to rename sheet', () => { +describe('isItPossibleToRenameSheet() returns', () => { it('true if possible', () => { const engine = HyperFormula.buildFromSheets({'Sheet1': []}) - expect(engine.isItPossibleToRenameSheet(0, 'Foo')).toEqual(true) - expect(engine.isItPossibleToRenameSheet(0, '~`!@#$%^&*()_-+_=/|?{}[]\"')).toEqual(true) + expect(engine.isItPossibleToRenameSheet(0, 'Foo')).toBe(true) + expect(engine.isItPossibleToRenameSheet(0, '~`!@#$%^&*()_-+_=/|?{}[]\"')).toBe(true) }) it('true if same name', () => { const engine = HyperFormula.buildFromSheets({'Sheet1': []}) - expect(engine.isItPossibleToRenameSheet(0, 'Sheet1')).toEqual(true) + expect(engine.isItPossibleToRenameSheet(0, 'Sheet1')).toBe(true) }) it('false if sheet does not exists', () => { const engine = HyperFormula.buildFromSheets({'Sheet1': []}) - expect(engine.isItPossibleToRenameSheet(1, 'Foo')).toEqual(false) + expect(engine.isItPossibleToRenameSheet(1, 'Foo')).toBe(false) }) it('false if given name is taken', () => { const engine = HyperFormula.buildFromSheets({'Sheet1': [], 'Sheet2': []}) - expect(engine.isItPossibleToRenameSheet(0, 'Sheet2')).toEqual(false) + expect(engine.isItPossibleToRenameSheet(0, 'Sheet2')).toBe(false) }) }) -describe('Rename sheet', () => { - it('works', () => { +describe('renameSheet()', () => { + it('renames sheet and updates sheet mapping', () => { const engine = HyperFormula.buildEmpty() engine.addSheet('foo') @@ -39,7 +41,7 @@ describe('Rename sheet', () => { expect(engine.doesSheetExist('bar')).toBe(true) }) - it('error when there is no sheet with given ID', () => { + it('throws error when sheet with given ID does not exist', () => { const engine = HyperFormula.buildEmpty() expect(() => { @@ -47,7 +49,7 @@ describe('Rename sheet', () => { }).toThrow(new NoSheetWithIdError(0)) }) - it('error when new sheet name is already taken', () => { + it('throws error when new sheet name is already taken', () => { const engine = HyperFormula.buildEmpty() engine.addSheet() engine.addSheet('bar') @@ -57,7 +59,7 @@ describe('Rename sheet', () => { }).toThrow(new SheetNameAlreadyTakenError('bar')) }) - it('change for the same name', () => { + it('allows renaming to the same name (no-op)', () => { const engine = HyperFormula.buildEmpty() engine.addSheet('foo') @@ -67,7 +69,7 @@ describe('Rename sheet', () => { expect(engine.doesSheetExist('foo')).toBe(true) }) - it('change for the same canonical name', () => { + it('allows changing case of the same canonical name', () => { const engine = HyperFormula.buildEmpty() engine.addSheet('Foo') @@ -77,11 +79,505 @@ describe('Rename sheet', () => { expect(engine.doesSheetExist('FOO')).toBe(true) }) - it('should update the sheet dependencies', () => { - const engine = HyperFormula.buildFromSheets({'OldSheetName': [[42]], 'DependantSheet': [['=OldSheetName!A1']]}) + describe('recalculates formulas (issue #1116)', () => { + it('recalculates single cell reference', () => { + const sheet1Name = 'Sheet1' + const oldName = 'OldName' + const newName = 'NewName' + const engine = HyperFormula.buildFromSheets({ + [sheet1Name]: [[`='${newName}'!A1`]], + [oldName]: [[42]], + }) + const sheet1Id = engine.getSheetId(sheet1Name)! + const oldNameId = engine.getSheetId(oldName)! - engine.renameSheet(0, 'NewSheetName') + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) - expect(engine.getCellFormula({ sheet: 1, row: 0, col: 0 })).toEqual('=NewSheetName!A1') + engine.renameSheet(oldNameId, newName) + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(42) + }) + + it('recalculates nested dependencies within same sheet', () => { + const sheet1Name = 'Sheet1' + const oldName = 'OldName' + const newName = 'NewName' + const engine = HyperFormula.buildFromSheets({ + [sheet1Name]: [['=B1*2', `='${newName}'!A1`]], + [oldName]: [[15]], + }) + const sheet1Id = engine.getSheetId(sheet1Name)! + const oldNameId = engine.getSheetId(oldName)! + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('B1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + + engine.renameSheet(oldNameId, newName) + + expect(engine.getCellValue(adr('B1', sheet1Id))).toBe(15) + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(30) + }) + + it('recalculates formulas with mixed operations', () => { + const sheet1Name = 'Sheet1' + const oldName = 'OldName' + const newName = 'NewName' + const engine = HyperFormula.buildFromSheets({ + [sheet1Name]: [[100, `='${newName}'!A1 + A1`, `='${newName}'!B1 * 2`]], + [oldName]: [[50, 25]], + }) + const sheet1Id = engine.getSheetId(sheet1Name)! + const oldNameId = engine.getSheetId(oldName)! + + expect(engine.getCellValue(adr('B1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('C1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + + engine.renameSheet(oldNameId, newName) + + expect(engine.getCellValue(adr('B1', sheet1Id))).toBe(150) + expect(engine.getCellValue(adr('C1', sheet1Id))).toBe(50) + }) + + it('recalculates named expressions', () => { + const sheet1Name = 'Sheet1' + const oldName = 'OldName' + const newName = 'NewName' + const engine = HyperFormula.buildFromSheets({ + [sheet1Name]: [['=MyValue'], ['=MyValue*2']], + [oldName]: [[99]], + }, {}, [ + { name: 'MyValue', expression: `='${newName}'!$A$1` } + ]) + const sheet1Id = engine.getSheetId(sheet1Name)! + const oldNameId = engine.getSheetId(oldName)! + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('A2', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + + engine.renameSheet(oldNameId, newName) + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(99) + expect(engine.getCellValue(adr('A2', sheet1Id))).toBe(198) + }) + + it('moves reserved range vertices onto renamed sheet', () => { + const formulaSheet = 'FormulaSheet' + const oldName = 'SourceSheet' + const newName = 'GhostSheet' + const engine = HyperFormula.buildFromSheets({ + [formulaSheet]: [[`=SUM('${newName}'!A1:B1)`]], + [oldName]: [[1, 2]], + }) + const formulaSheetId = engine.getSheetId(formulaSheet)! + const oldNameId = engine.getSheetId(oldName)! + + expect(engine.getCellValue(adr('A1', formulaSheetId))).toEqualError(detailedError(ErrorType.REF)) + + engine.renameSheet(oldNameId, newName) + + const renamedSheetId = engine.getSheetId(newName)! + + expect(engine.getCellValue(adr('A1', formulaSheetId))).toBe(3) + + const movedRange = engine.rangeMapping.getRangeVertex(adr('A1', renamedSheetId), adr('B1', renamedSheetId)) + + expect(movedRange).toBeDefined() + expect(movedRange?.sheet).toBe(renamedSheetId) + }) + + it('merges duplicate range vertices after renaming into reserved name', () => { + const oldName = 'OldName' + const newName = 'GhostSheet' + const usesOld = 'UsesOld' + const usesNew = 'UsesNew' + const engine = HyperFormula.buildFromSheets({ + [usesOld]: [[`=SUM('${oldName}'!A1:A2)`]], + [usesNew]: [[`=SUM('${newName}'!A1:A2)`]], + [oldName]: [[5], [7]], + }) + const usesOldId = engine.getSheetId(usesOld)! + const usesNewId = engine.getSheetId(usesNew)! + const oldNameId = engine.getSheetId(oldName)! + + expect(engine.getCellValue(adr('A1', usesOldId))).toBe(12) + expect(engine.getCellValue(adr('A1', usesNewId))).toEqualError(detailedError(ErrorType.REF)) + + engine.renameSheet(oldNameId, newName) + + expect(engine.getCellValue(adr('A1', usesOldId))).toBe(12) + expect(engine.getCellValue(adr('A1', usesNewId))).toBe(12) + expect(engine.getCellFormula(adr('A1', usesNewId))).toBe(`=SUM(${newName}!A1:A2)`) + expect(engine.getCellFormula(adr('A1', usesOldId))).toBe(`=SUM(${newName}!A1:A2)`) + + engine.setCellContents(adr('A1', oldNameId), 100) + + expect(engine.getCellValue(adr('A1', usesOldId))).toBe(107) + expect(engine.getCellValue(adr('A1', usesNewId))).toBe(107) + }) + + it('recalculates column and row ranges', () => { + const sheet1Name = 'Sheet1' + const oldName = 'OldName' + const newName = 'NewName' + const engine = HyperFormula.buildFromSheets({ + [sheet1Name]: [ + [`=SUM('${newName}'!A:A)`], + [`=SUM('${newName}'!1:2)`], + ], + [oldName]: [ + [1, 2], + [3, 4], + ], + }) + const sheet1Id = engine.getSheetId(sheet1Name)! + const oldNameId = engine.getSheetId(oldName)! + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('A2', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + + engine.renameSheet(oldNameId, newName) + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(4) + expect(engine.getCellValue(adr('A2', sheet1Id))).toBe(10) + }) + + it('keeps existing dependencies and dependents when renaming a sheet', () => { + const mainSheetName = 'Sheet1' + const secondarySheetName = 'Sheet2' + const newNameForSecondarySheet = 'Sheet3' + const engine = HyperFormula.buildFromSheets({ + [mainSheetName]: [['main sheet', `=${secondarySheetName}!A1`, `=${newNameForSecondarySheet}!A1`]], + [secondarySheetName]: [['secondary sheet', `=${mainSheetName}!A1`]], + }) + const mainSheetId = engine.getSheetId(mainSheetName)! + const secondarySheetId = engine.getSheetId(secondarySheetName)! + + expect(engine.getCellValue(adr('B1', mainSheetId))).toBe('secondary sheet') + expect(engine.getCellValue(adr('C1', mainSheetId))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('B1', secondarySheetId))).toBe('main sheet') + + engine.renameSheet(secondarySheetId, newNameForSecondarySheet) + + expect(engine.getSheetId(newNameForSecondarySheet)).toBe(secondarySheetId) + expect(engine.getCellValue(adr('B1', mainSheetId))).toBe('secondary sheet') + expect(engine.getCellValue(adr('C1', mainSheetId))).toBe('secondary sheet') + expect(engine.getCellValue(adr('B1', secondarySheetId))).toBe('main sheet') + }) + + it('removing renamed sheet returns REF error', () => { + const sheet1Name = 'Sheet1' + const oldName = 'OldName' + const newName = 'NewName' + const engine = HyperFormula.buildFromSheets({ + [sheet1Name]: [[`='${newName}'!A1`]], + [oldName]: [[42]], + }) + const sheet1Id = engine.getSheetId(sheet1Name)! + const oldNameId = engine.getSheetId(oldName)! + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + + engine.renameSheet(oldNameId, newName) + + expect(engine.getSheetId(newName)).toBe(oldNameId) + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(42) + + engine.removeSheet(oldNameId) + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + }) + + it('adding sheet with the same name as new name of renamed sheet throws error', () => { + const oldName = 'OldName' + const newName = 'NewName' + const engine = HyperFormula.buildFromSheets({ + [oldName]: [[42]], + }) + const oldNameId = engine.getSheetId(oldName)! + + engine.renameSheet(oldNameId, newName) + + expect(() => { + engine.addSheet(newName) + }).toThrow(new SheetNameAlreadyTakenError(newName)) + }) + + it('adding sheet with the old name of renamed sheet creates new sheet', () => { + const sheet1Name = 'Sheet1' + const oldName = 'OldName' + const newName = 'NewName' + const engine = HyperFormula.buildFromSheets({ + [sheet1Name]: [[`='${oldName}'!A1`]], + [oldName]: [[42]], + }) + const sheet1Id = engine.getSheetId(sheet1Name)! + const oldNameId = engine.getSheetId(oldName)! + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(42) + + engine.renameSheet(oldNameId, newName) + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(42) + + engine.addSheet(oldName) + engine.setCellContents(adr('A1', engine.getSheetId(oldName)), 100) + + expect(engine.getSheetId(oldName)).not.toBe(engine.getSheetId(newName)) + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(42) + expect(engine.getCellValue(adr('A1', engine.getSheetId(newName)))).toBe(42) + expect(engine.getCellValue(adr('A1', engine.getSheetId(oldName)))).toBe(100) + }) + + + it('renaming sheet that references non-existent sheet keeps REF error', () => { + const sheet1Name = 'Sheet1' + const newName = 'NewName' + const nonExistent = 'NonExistent' + const engine = HyperFormula.buildFromSheets({ + [sheet1Name]: [[`='${nonExistent}'!A1`]], + }) + const sheet1Id = engine.getSheetId(sheet1Name)! + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + + engine.renameSheet(sheet1Id, newName) + + expect(engine.getCellValue(adr('A1', engine.getSheetId(newName)))).toEqualError(detailedError(ErrorType.REF)) + }) + + it('chain renaming resolves multiple placeholder references sequentially', () => { + const engine = HyperFormula.buildFromSheets({ + 'Main': [['=SheetB!A1', '=SheetC!A1']], + 'SheetA': [[10]], + }) + const mainId = engine.getSheetId('Main')! + const sheetAId = engine.getSheetId('SheetA')! + + // Both are initially REF errors (placeholders) + expect(engine.getCellValue(adr('A1', mainId))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('B1', mainId))).toEqualError(detailedError(ErrorType.REF)) + + // Rename SheetA → SheetB: resolves first placeholder + engine.renameSheet(sheetAId, 'SheetB') + + expect(engine.getCellValue(adr('A1', mainId))).toBe(10) + expect(engine.getCellValue(adr('B1', mainId))).toEqualError(detailedError(ErrorType.REF)) + // Formula A1 now references SheetB (follows the rename) + expect(engine.getCellFormula(adr('A1', mainId))).toBe('=SheetB!A1') + + // Add a new sheet named SheetC to resolve second placeholder + engine.addSheet('SheetC') + engine.setCellContents(adr('A1', engine.getSheetId('SheetC')), 20) + + expect(engine.getCellValue(adr('A1', mainId))).toBe(10) + expect(engine.getCellValue(adr('B1', mainId))).toBe(20) + }) + + it('renaming sheet with circular formula does not break engine', () => { + const engine = HyperFormula.buildFromSheets({ + 'Sheet1': [['=Sheet2!A1']], + 'Sheet2': [['=Sheet1!A1']], + }) + const sheet1Id = engine.getSheetId('Sheet1')! + const sheet2Id = engine.getSheetId('Sheet2')! + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.CYCLE)) + expect(engine.getCellValue(adr('A1', sheet2Id))).toEqualError(detailedError(ErrorType.CYCLE)) + + engine.renameSheet(sheet1Id, 'RenamedSheet1') + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.CYCLE)) + expect(engine.getCellValue(adr('A1', sheet2Id))).toEqualError(detailedError(ErrorType.CYCLE)) + expect(engine.getCellFormula(adr('A1', sheet2Id))).toBe('=RenamedSheet1!A1') + }) + + it('multiple formulas referencing same placeholder all resolve after rename', () => { + const engine = HyperFormula.buildFromSheets({ + 'Sheet1': [['=Ghost!A1'], ['=Ghost!A1+1'], ['=SUM(Ghost!A1:A2)']], + 'Sheet2': [['=Ghost!A1*2'], ['=Ghost!B1']], + 'RealSheet': [[100, 200], [300]], + }) + const sheet1Id = engine.getSheetId('Sheet1')! + const sheet2Id = engine.getSheetId('Sheet2')! + const realSheetId = engine.getSheetId('RealSheet')! + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('A2', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('A3', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('A1', sheet2Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('A2', sheet2Id))).toEqualError(detailedError(ErrorType.REF)) + + engine.renameSheet(realSheetId, 'Ghost') + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(100) + expect(engine.getCellValue(adr('A2', sheet1Id))).toBe(101) + expect(engine.getCellValue(adr('A3', sheet1Id))).toBe(400) + expect(engine.getCellValue(adr('A1', sheet2Id))).toBe(200) + expect(engine.getCellValue(adr('A2', sheet2Id))).toBe(200) + }) + + it('when new name is already referenced, engine merges both sheet names', () => { + const engine = HyperFormula.buildFromSheets({ + 'Main': [['=Source!A1', '=Target!A1']], + 'Source': [[42]], + }) + const mainId = engine.getSheetId('Main')! + const sourceId = engine.getSheetId('Source')! + + expect(engine.getCellValue(adr('A1', mainId))).toBe(42) + expect(engine.getCellValue(adr('B1', mainId))).toEqualError(detailedError(ErrorType.REF)) + + engine.renameSheet(sourceId, 'Target') + + expect(engine.getCellFormula(adr('A1', mainId))).toBe('=Target!A1') + expect(engine.getCellFormula(adr('B1', mainId))).toBe('=Target!A1') + expect(engine.getCellValue(adr('A1', mainId))).toBe(42) + expect(engine.getCellValue(adr('B1', mainId))).toBe(42) + + engine.setCellContents(adr('A1', sourceId), 100) + + expect(engine.getCellValue(adr('A1', mainId))).toBe(100) + expect(engine.getCellValue(adr('B1', mainId))).toBe(100) + }) + + it('handles deeply nested REF error propagation', () => { + const engine = HyperFormula.buildFromSheets({ + 'L1': [['=L2!A1+1']], + 'L2': [['=L3!A1+1']], + 'L3': [['=L4!A1+1']], + 'L4': [['=Ghost!A1']], + 'Source': [[100]], + }) + const l1Id = engine.getSheetId('L1')! + const l2Id = engine.getSheetId('L2')! + const l3Id = engine.getSheetId('L3')! + const l4Id = engine.getSheetId('L4')! + const sourceId = engine.getSheetId('Source')! + + expect(engine.getCellValue(adr('A1', l1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('A1', l2Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('A1', l3Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('A1', l4Id))).toEqualError(detailedError(ErrorType.REF)) + + engine.renameSheet(sourceId, 'Ghost') + + expect(engine.getCellValue(adr('A1', l4Id))).toBe(100) + expect(engine.getCellValue(adr('A1', l3Id))).toBe(101) + expect(engine.getCellValue(adr('A1', l2Id))).toBe(102) + expect(engine.getCellValue(adr('A1', l1Id))).toBe(103) + }) + + describe('when using ranges with', () => { + it('function using `runFunction`', () => { + const engine = HyperFormula.buildFromSheets({ + 'FirstSheet': [['=MEDIAN(NewName!A1:A1)', '=MEDIAN(NewName!A1:A2)', '=MEDIAN(NewName!A1:A3)', '=MEDIAN(NewName!A1:A4)']], + 'OldName': [[1], [2], [3], [4]], + }) + const sheet1Id = engine.getSheetId('FirstSheet')! + const sheet2Id = engine.getSheetId('OldName')! + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('B1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('C1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('D1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + + engine.renameSheet(sheet2Id, 'NewName') + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(1) + expect(engine.getCellValue(adr('B1', sheet1Id))).toBe(1.5) + expect(engine.getCellValue(adr('C1', sheet1Id))).toBe(2) + expect(engine.getCellValue(adr('D1', sheet1Id))).toBe(2.5) + }) + + it('function not using `runFunction`', () => { + const engine = HyperFormula.buildFromSheets({ + 'FirstSheet': [['=SUM(NewName!A1:A1)', '=SUM(NewName!A1:A2)', '=SUM(NewName!A1:A3)', '=SUM(NewName!A1:A4)']], + 'OldName': [[1], [2], [3], [4]], + }) + const sheet1Id = engine.getSheetId('FirstSheet')! + const sheet2Id = engine.getSheetId('OldName')! + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('B1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('C1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('D1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + + engine.renameSheet(sheet2Id, 'NewName') + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(1) + expect(engine.getCellValue(adr('B1', sheet1Id))).toBe(3) + expect(engine.getCellValue(adr('C1', sheet1Id))).toBe(6) + expect(engine.getCellValue(adr('D1', sheet1Id))).toBe(10) + }) + + it('function using `runFunction` referencing range indirectly', () => { + const engine = HyperFormula.buildFromSheets({ + 'FirstSheet': [ + ['=MEDIAN(A2)', '=MEDIAN(B2)', '=MEDIAN(C2)', '=MEDIAN(D2)'], + ['=\'NewName\'!A1:A1', '=\'NewName\'!A1:B2', '=\'NewName\'!A1:A3', '=\'NewName\'!A1:A4'], + ], + 'OldName': [[1], [2], [3], [4]], + }) + const sheet1Id = engine.getSheetId('FirstSheet')! + const sheet2Id = engine.getSheetId('OldName')! + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('B1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('C1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('D1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + + engine.renameSheet(sheet2Id, 'NewName') + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(1) + expect(engine.getCellValue(adr('B1', sheet1Id))).toBe(1.5) + expect(engine.getCellValue(adr('C1', sheet1Id))).toBe(2) + expect(engine.getCellValue(adr('D1', sheet1Id))).toBe(2.5) + }) + + it('function not using `runFunction` referencing range indirectly', () => { + const engine = HyperFormula.buildFromSheets({ + 'FirstSheet': [ + ['=SUM(A2)', '=SUM(B2)', '=SUM(C2)', '=SUM(D2)'], + ['=\'NewName\'!A1:A1', '=\'NewName\'!A1:B2', '=\'NewName\'!A1:A3', '=\'NewName\'!A1:A4'], + ], + 'OldName': [[1], [2], [3], [4]], + }) + const sheet1Id = engine.getSheetId('FirstSheet')! + const sheet2Id = engine.getSheetId('OldName')! + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('B1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('C1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getCellValue(adr('D1', sheet1Id))).toEqualError(detailedError(ErrorType.REF)) + + engine.renameSheet(sheet2Id, 'NewName') + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(1) + expect(engine.getCellValue(adr('B1', sheet1Id))).toBe(3) + expect(engine.getCellValue(adr('C1', sheet1Id))).toBe(6) + expect(engine.getCellValue(adr('D1', sheet1Id))).toBe(10) + }) + + it('function calling a named expression', () => { + const engine = HyperFormula.buildFromSheets({ + 'FirstSheet': [['=\'OldName\'!A1:A4']], + 'OldName': [[1], [2], [3], [4]], + }, {}, [ + { name: 'ExprA', expression: '=MEDIAN(NewName!$A$1:$A$1)' }, + { name: 'ExprB', expression: '=MEDIAN(NewName!$A$1:$A$2)' }, + { name: 'ExprC', expression: '=MEDIAN(NewName!$A$1:$A$3)' }, + ]) + + expect(engine.getNamedExpressionValue('ExprA')).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getNamedExpressionValue('ExprB')).toEqualError(detailedError(ErrorType.REF)) + expect(engine.getNamedExpressionValue('ExprC')).toEqualError(detailedError(ErrorType.REF)) + + engine.renameSheet(engine.getSheetId('OldName')!, 'NewName') + + expect(engine.getNamedExpressionValue('ExprA')).toBe(1) + expect(engine.getNamedExpressionValue('ExprB')).toBe(1.5) + expect(engine.getNamedExpressionValue('ExprC')).toBe(2) + }) + }) }) }) diff --git a/test/unit/cruds/set-matrix-empty.spec.ts b/test/unit/cruds/set-matrix-empty.spec.ts index e5cd68f9f..b0b8e20fb 100644 --- a/test/unit/cruds/set-matrix-empty.spec.ts +++ b/test/unit/cruds/set-matrix-empty.spec.ts @@ -9,7 +9,7 @@ describe('Set matrix empty', () => { ['=TRANSPOSE(A1:B1)'], ]) const dependencyGraph = engine.dependencyGraph - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const matrixVertex = dependencyGraph.arrayMapping.getArray(AbsoluteCellRange.spanFrom(adr('A2'), 1, 2))! dependencyGraph.setArrayEmpty(matrixVertex) @@ -25,7 +25,7 @@ describe('Set matrix empty', () => { ['=TRANSPOSE(A1:B1)'], ]) const dependencyGraph = engine.dependencyGraph - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const matrixVertex = dependencyGraph.arrayMapping.getArray(AbsoluteCellRange.spanFrom(adr('A2'), 1, 2))! dependencyGraph.setArrayEmpty(matrixVertex) @@ -50,7 +50,7 @@ describe('Set matrix empty', () => { ['=TRANSPOSE(A1:B1)'], ]) const dependencyGraph = engine.dependencyGraph - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const matrixVertex = dependencyGraph.arrayMapping.getArray(AbsoluteCellRange.spanFrom(adr('A2'), 1, 2))! dependencyGraph.setArrayEmpty(matrixVertex) @@ -71,10 +71,10 @@ describe('Set matrix empty', () => { ['=TRANSPOSE(A1:B1)'], ]) const dependencyGraph = engine.dependencyGraph - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const matrixVertex = dependencyGraph.arrayMapping.getArray(AbsoluteCellRange.spanFrom(adr('A2'), 1, 2))! - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const rangeVertex = dependencyGraph.rangeMapping.getRange(adr('A2'), adr('A3'))! + + const rangeVertex = dependencyGraph.rangeMapping.getRangeVertex(adr('A2'), adr('A3'))! expect(dependencyGraph.existsEdge(matrixVertex, rangeVertex)).toBe(true) dependencyGraph.setArrayEmpty(matrixVertex) @@ -97,10 +97,10 @@ describe('Set matrix empty', () => { ['=TRANSPOSE(A1:B1)'], ]) const dependencyGraph = engine.dependencyGraph - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const matrixVertex = dependencyGraph.arrayMapping.getArray(AbsoluteCellRange.spanFrom(adr('A2'), 1, 2))! - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const rangeVertex = dependencyGraph.rangeMapping.getRange(adr('A1'), adr('A2'))! + + const rangeVertex = dependencyGraph.rangeMapping.getRangeVertex(adr('A1'), adr('A2'))! expect(dependencyGraph.existsEdge(matrixVertex, rangeVertex)).toBe(true) dependencyGraph.setArrayEmpty(matrixVertex) diff --git a/test/unit/date.spec.ts b/test/unit/date.spec.ts index 32739f072..44d1f25b7 100644 --- a/test/unit/date.spec.ts +++ b/test/unit/date.spec.ts @@ -234,12 +234,6 @@ describe('By default function parseDateTimeFromConfigFormats', () => { expect(parsedDate).toEqual({}) }) - it('returns {} when trying to parse time but timeFormats=[]', () => { - const dateHelper = new DateTimeHelper(new Config({ timeFormats: [] })) - const parsedDate = dateHelper.parseDateTimeFromConfigFormats('01:01') - expect(parsedDate).toEqual({}) - }) - it('returns {} when time format contains no day term', () => { const dateHelper = new DateTimeHelper(new Config({ dateFormats: ['MM/YY'] })) const parsedDate = dateHelper.parseDateTimeFromConfigFormats('12/12') diff --git a/test/unit/dependency-graph-sheet-reference-registrar.spec.ts b/test/unit/dependency-graph-sheet-reference-registrar.spec.ts new file mode 100644 index 000000000..1fe440d6a --- /dev/null +++ b/test/unit/dependency-graph-sheet-reference-registrar.spec.ts @@ -0,0 +1,72 @@ +import { AlwaysDense } from '../../src' +import {AddressMapping, DenseStrategy, SheetMapping, SheetReferenceRegistrar} from '../../src/DependencyGraph' +import {buildTranslationPackage} from '../../src/i18n' +import {enGB} from '../../src/i18n/languages' + +describe('SheetReferenceRegistrar', () => { + const createDependencies = () => { + const sheetMapping = new SheetMapping(buildTranslationPackage(enGB)) + const addressMapping = new AddressMapping(new AlwaysDense()) + const registrar = new SheetReferenceRegistrar(sheetMapping, addressMapping) + return { sheetMapping, addressMapping, registrar } + } + + it('when called with non-existing sheet, adds a placeholder', () => { + const { sheetMapping, addressMapping, registrar } = createDependencies() + + const sheetId = registrar.ensureSheetRegistered('NewSheet') + + expect(sheetMapping.numberOfSheets({ includePlaceholders: true })).toBe(1) + expect(sheetMapping.hasSheetWithId(sheetId, { includePlaceholders: true })).toBe(true) + expect(() => addressMapping.getStrategyForSheetOrThrow(sheetId)).not.toThrow() + }) + + it('when called with existing placeholder sheet, doesnt modify address mapping nor sheet mapping', () => { + const { sheetMapping, addressMapping, registrar } = createDependencies() + const firstSheetId = registrar.ensureSheetRegistered('PlaceholderSheet') + + const secondSheetId = registrar.ensureSheetRegistered('PlaceholderSheet') + + expect(secondSheetId).toBe(firstSheetId) + expect(sheetMapping.numberOfSheets({ includePlaceholders: true })).toBe(1) + expect(() => addressMapping.getStrategyForSheetOrThrow(firstSheetId)).not.toThrow() + }) + + it('when called with existing real sheet, doesnt modify address mapping nor sheet mapping', () => { + const { sheetMapping, addressMapping, registrar } = createDependencies() + const realSheetId = sheetMapping.addSheet('RealSheet') + addressMapping.addSheetWithStrategy(realSheetId, new DenseStrategy(0, 0)) + + const returnedSheetId = registrar.ensureSheetRegistered('RealSheet') + + expect(returnedSheetId).toBe(realSheetId) + expect(sheetMapping.numberOfSheets({ includePlaceholders: true })).toBe(1) + expect(sheetMapping.numberOfSheets({ includePlaceholders: false })).toBe(1) + expect(() => addressMapping.getStrategyForSheetOrThrow(realSheetId)).not.toThrow() + }) + + it('when called with name that differs from some placeholder sheet only by case, doesnt modify address mapping nor sheet mapping', () => { + const { sheetMapping, addressMapping, registrar } = createDependencies() + const firstSheetId = registrar.ensureSheetRegistered('PlaceholderSheet') + + const secondSheetId = registrar.ensureSheetRegistered('PLACEHOLDERSHEET') + + expect(secondSheetId).toBe(firstSheetId) + expect(secondSheetId).toBe(firstSheetId) + expect(sheetMapping.numberOfSheets({ includePlaceholders: true })).toBe(1) + expect(() => addressMapping.getStrategyForSheetOrThrow(firstSheetId)).not.toThrow() + }) + + it('when called with name that differs from some real sheet only by case, doesnt modify address mapping nor sheet mapping', () => { + const { sheetMapping, addressMapping, registrar } = createDependencies() + const realSheetId = sheetMapping.addSheet('RealSheet') + addressMapping.addSheetWithStrategy(realSheetId, new DenseStrategy(0, 0)) + + const returnedSheetId = registrar.ensureSheetRegistered('REALSHEET') + + expect(returnedSheetId).toBe(realSheetId) + expect(sheetMapping.numberOfSheets({ includePlaceholders: true })).toBe(1) + expect(sheetMapping.numberOfSheets({ includePlaceholders: false })).toBe(1) + expect(() => addressMapping.getStrategyForSheetOrThrow(realSheetId)).not.toThrow() + }) +}) diff --git a/test/unit/emitting-events.spec.ts b/test/unit/emitting-events.spec.ts index 5393ecff8..6db2665da 100644 --- a/test/unit/emitting-events.spec.ts +++ b/test/unit/emitting-events.spec.ts @@ -6,6 +6,7 @@ import { NamedExpressionDoesNotExistError, } from '../../src' import {Events} from '../../src/Emitter' +import { ErrorMessage } from '../../src/error-message' import {adr, detailedErrorWithOrigin} from './testUtils' @@ -32,7 +33,7 @@ describe('Events', () => { engine.removeSheet(1) expect(handler).toHaveBeenCalledTimes(1) - expect(handler).toHaveBeenCalledWith('Sheet2', [new ExportedCellChange(adr('A1'), detailedErrorWithOrigin(ErrorType.REF, 'Sheet1!A1'))]) + expect(handler).toHaveBeenCalledWith('Sheet2', [new ExportedCellChange(adr('A1'), detailedErrorWithOrigin(ErrorType.REF, 'Sheet1!A1', ErrorMessage.SheetRef))]) }) it('sheetRemoved name contains actual display name', function() { @@ -46,7 +47,7 @@ describe('Events', () => { engine.removeSheet(1) expect(handler).toHaveBeenCalledTimes(1) - expect(handler).toHaveBeenCalledWith('Sheet2', [new ExportedCellChange(adr('A1'), detailedErrorWithOrigin(ErrorType.REF, 'Sheet1!A1'))]) + expect(handler).toHaveBeenCalledWith('Sheet2', [new ExportedCellChange(adr('A1'), detailedErrorWithOrigin(ErrorType.REF, 'Sheet1!A1', ErrorMessage.SheetRef))]) }) it('sheetRenamed works', () => { diff --git a/test/unit/graph-builder.spec.ts b/test/unit/graph-builder.spec.ts index 60133863d..0041398fa 100644 --- a/test/unit/graph-builder.spec.ts +++ b/test/unit/graph-builder.spec.ts @@ -10,9 +10,9 @@ describe('GraphBuilder', () => { ['42'], ]) - const vertex = engine.addressMapping.fetchCell(adr('A1')) + const vertex = engine.addressMapping.getCell(adr('A1')) expect(vertex).toBeInstanceOf(ValueCellVertex) - expect(vertex.getCellValue()).toBe(42) + expect(vertex!.getCellValue()).toBe(42) }) it('build sheet with simple string cell', () => { @@ -20,9 +20,9 @@ describe('GraphBuilder', () => { ['foo'], ]) - const vertex = engine.addressMapping.fetchCell(adr('A1')) + const vertex = engine.addressMapping.getCell(adr('A1')) expect(vertex).toBeInstanceOf(ValueCellVertex) - expect(vertex.getCellValue()).toBe('foo') + expect(vertex!.getCellValue()).toBe('foo') }) it('building for cell with null should give empty vertex', () => { @@ -30,7 +30,7 @@ describe('GraphBuilder', () => { [null, '=A1'], ]) - const vertex = engine.addressMapping.fetchCell(adr('A1')) + const vertex = engine.addressMapping.getCell(adr('A1')) expect(vertex).toBeInstanceOf(EmptyCellVertex) }) @@ -40,12 +40,12 @@ describe('GraphBuilder', () => { ['=A1:B1'], ]) - const a1 = engine.addressMapping.fetchCell(adr('A1')) - const b1 = engine.addressMapping.fetchCell(adr('B1')) - const a1b2 = engine.rangeMapping.fetchRange(adr('A1'), adr('B1')) - const a2 = engine.addressMapping.fetchCell(adr('A2')) - expect(engine.graph.adjacentNodes(a1)).toContain(a1b2) - expect(engine.graph.adjacentNodes(b1)).toContain(a1b2) + const a1 = engine.addressMapping.getCell(adr('A1')) + const b1 = engine.addressMapping.getCell(adr('B1')) + const a1b2 = engine.rangeMapping.getVertexOrThrow(adr('A1'), adr('B1')) + const a2 = engine.addressMapping.getCell(adr('A2')) + expect(engine.graph.adjacentNodes(a1!)).toContain(a1b2) + expect(engine.graph.adjacentNodes(b1!)).toContain(a1b2) expect(engine.graph.adjacentNodes(a1b2)).toContain(a2) }) @@ -54,12 +54,12 @@ describe('GraphBuilder', () => { ['1', '2', '=A:B'], ]) - const a1 = engine.addressMapping.fetchCell(adr('A1')) - const b1 = engine.addressMapping.fetchCell(adr('B1')) - const ab = engine.rangeMapping.fetchRange(colStart('A'), colEnd('B')) - const c1 = engine.addressMapping.fetchCell(adr('C1')) - expect(engine.graph.adjacentNodes(a1)).toContain(ab) - expect(engine.graph.adjacentNodes(b1)).toContain(ab) + const a1 = engine.addressMapping.getCell(adr('A1')) + const b1 = engine.addressMapping.getCell(adr('B1')) + const ab = engine.rangeMapping.getVertexOrThrow(colStart('A'), colEnd('B')) + const c1 = engine.addressMapping.getCell(adr('C1')) + expect(engine.graph.adjacentNodes(a1!)).toContain(ab) + expect(engine.graph.adjacentNodes(b1!)).toContain(ab) expect(engine.graph.adjacentNodes(ab)).toContain(c1) }) @@ -70,16 +70,16 @@ describe('GraphBuilder', () => { ['=A1:B1'], ]) - const a1 = engine.addressMapping.fetchCell(adr('A1')) - const b1 = engine.addressMapping.fetchCell(adr('B1')) - const a1b2 = engine.rangeMapping.fetchRange(adr('A1'), adr('B1')) - const a2 = engine.addressMapping.fetchCell(adr('A2')) - const a3 = engine.addressMapping.fetchCell(adr('A3')) + const a1 = engine.addressMapping.getCell(adr('A1')) + const b1 = engine.addressMapping.getCell(adr('B1')) + const a1b2 = engine.rangeMapping.getVertexOrThrow(adr('A1'), adr('B1')) + const a2 = engine.addressMapping.getCell(adr('A2')) + const a3 = engine.addressMapping.getCell(adr('A3')) - expect(engine.graph.existsEdge(a1, a1b2)).toBe(true) - expect(engine.graph.existsEdge(b1, a1b2)).toBe(true) - expect(engine.graph.existsEdge(a1b2, a2)).toBe(true) - expect(engine.graph.existsEdge(a1b2, a3)).toBe(true) + expect(engine.graph.existsEdge(a1!, a1b2)).toBe(true) + expect(engine.graph.existsEdge(b1!, a1b2)).toBe(true) + expect(engine.graph.existsEdge(a1b2, a2!)).toBe(true) + expect(engine.graph.existsEdge(a1b2, a3!)).toBe(true) expect(engine.graph.getNodes().length).toBe( 4 + // for cells above 1, // for both ranges (reuse same ranges) @@ -93,11 +93,11 @@ describe('GraphBuilder', () => { ['5', '=A1:A3'], ]) - const a3 = engine.addressMapping.fetchCell(adr('A3')) - const a1a2 = engine.rangeMapping.fetchRange(adr('A1'), adr('A2')) - const a1a3 = engine.rangeMapping.fetchRange(adr('A1'), adr('A3')) + const a3 = engine.addressMapping.getCell(adr('A3')) + const a1a2 = engine.rangeMapping.getVertexOrThrow(adr('A1'), adr('A2')) + const a1a3 = engine.rangeMapping.getVertexOrThrow(adr('A1'), adr('A3')) - expect(engine.graph.existsEdge(a3, a1a3)).toBe(true) + expect(engine.graph.existsEdge(a3!, a1a3)).toBe(true) expect(engine.graph.existsEdge(a1a2, a1a3)).toBe(true) expect(graphEdgesCount(engine.graph)).toBe( 2 + // from cells to range(A1:A2) @@ -130,11 +130,11 @@ describe('GraphBuilder', () => { ['5', '=A1:A2'], ]) - const a1a2 = engine.rangeMapping.fetchRange(adr('A1'), adr('A2')) - const a1a3 = engine.rangeMapping.fetchRange(adr('A1'), adr('A3')) - const a2 = engine.addressMapping.fetchCell(adr('A2')) - expect(engine.graph.existsEdge(a2, a1a3)).toBe(true) - expect(engine.graph.existsEdge(a2, a1a2)).toBe(true) + const a1a2 = engine.rangeMapping.getVertexOrThrow(adr('A1'), adr('A2')) + const a1a3 = engine.rangeMapping.getVertexOrThrow(adr('A1'), adr('A3')) + const a2 = engine.addressMapping.getCell(adr('A2')) + expect(engine.graph.existsEdge(a2!, a1a3)).toBe(true) + expect(engine.graph.existsEdge(a2!, a1a2)).toBe(true) expect(engine.graph.existsEdge(a1a2, a1a3)).toBe(false) expect(graphEdgesCount(engine.graph)).toBe( 3 + // from 3 cells to range(A1:A2) diff --git a/test/unit/graph-dependencies-queries.spec.ts b/test/unit/graph-dependencies-queries.spec.ts index 6d4dd40f3..ea35ed705 100644 --- a/test/unit/graph-dependencies-queries.spec.ts +++ b/test/unit/graph-dependencies-queries.spec.ts @@ -79,6 +79,13 @@ describe('address queries', () => { }).toThrow(new ExpectedValueOfTypeError('SimpleCellAddress | SimpleCellRange', malformedAddress.toString())) }) + it('should return empty array when sheet does not exist', () => { + const engine = HyperFormula.buildFromArray([[1]]) + + const nonExistentSheetId = 999 + + expect(engine.getCellDependents({ sheet: nonExistentSheetId, col: 0, row: 0 })).toEqual([]) + }) }) describe('getCellPrecedents', () => { @@ -131,5 +138,32 @@ describe('address queries', () => { engine.getCellPrecedents(malformedAddress) }).toThrow(new ExpectedValueOfTypeError('SimpleCellAddress | SimpleCellRange', malformedAddress.toString())) }) + + it('should correctly process cell dependencies with multiple range types', () => { + const engine = HyperFormula.buildFromArray([ + [1, 2, 3, 4], + [5, 6, 7, 8], + ['=SUM(A1:D1)', '=SUM(A2:D2)', '=SUM(A1:D2)', '=A1+B1'], + ]) + + expect(engine.getCellValue(adr('A3'))).toBe(10) + expect(engine.getCellValue(adr('B3'))).toBe(26) + expect(engine.getCellValue(adr('C3'))).toBe(36) + expect(engine.getCellValue(adr('D3'))).toBe(3) + }) + + it('should correctly process named expression dependencies', () => { + const engine = HyperFormula.buildFromArray([ + [42], + ['=MyValue * 2'], + ], {}, [ + {name: 'MyValue', expression: '=Sheet1!$A$1'}, + ]) + + expect(engine.getCellValue(adr('A2'))).toBe(84) + + engine.setCellContents(adr('A1'), 100) + expect(engine.getCellValue(adr('A2'))).toBe(200) + }) }) }) diff --git a/test/unit/graph-garbage-collection.spec.ts b/test/unit/graph-garbage-collection.spec.ts index fdd759069..c7c0aa060 100644 --- a/test/unit/graph-garbage-collection.spec.ts +++ b/test/unit/graph-garbage-collection.spec.ts @@ -32,9 +32,9 @@ describe('range mapping', () => { ['1', '2'], ['3', '4'] ]) - expect(engine.dependencyGraph.rangeMapping.getMappingSize(0)).toBe(0) + expect(engine.dependencyGraph.rangeMapping.getNumberOfRangesInSheet(0)).toBe(0) engine.calculateFormula('=SUM(A1:B2)', 0) - expect(engine.dependencyGraph.rangeMapping.getMappingSize(0)).toBe(0) + expect(engine.dependencyGraph.rangeMapping.getNumberOfRangesInSheet(0)).toBe(0) }) it('cruds', () => { @@ -42,11 +42,11 @@ describe('range mapping', () => { ['1', '2'], ['3', '4'] ]) - expect(engine.dependencyGraph.rangeMapping.getMappingSize(0)).toBe(0) + expect(engine.dependencyGraph.rangeMapping.getNumberOfRangesInSheet(0)).toBe(0) engine.setCellContents(adr('A1'), '=SUM(A2:B2)') - expect(engine.dependencyGraph.rangeMapping.getMappingSize(0)).toBe(1) + expect(engine.dependencyGraph.rangeMapping.getNumberOfRangesInSheet(0)).toBe(1) engine.setCellContents(adr('A1'), 1) - expect(engine.dependencyGraph.rangeMapping.getMappingSize(0)).toBe(0) + expect(engine.dependencyGraph.rangeMapping.getNumberOfRangesInSheet(0)).toBe(0) }) }) @@ -76,7 +76,7 @@ describe('larger tests', () => { } } expect(engine.dependencyGraph.graph.getNodes().length).toBe(0) - expect(engine.dependencyGraph.rangeMapping.getMappingSize(0)).toBe(0) + expect(engine.dependencyGraph.rangeMapping.getNumberOfRangesInSheet(0)).toBe(0) }) it('large fixed #2', () => { @@ -93,7 +93,7 @@ describe('larger tests', () => { engine.setCellContents({sheet: 0, col: 2, row: 0}, null) engine.setCellContents({sheet: 0, col: 3, row: 0}, null) expect(engine.dependencyGraph.graph.getNodes().length).toBe(0) - expect(engine.dependencyGraph.rangeMapping.getMappingSize(0)).toBe(0) + expect(engine.dependencyGraph.rangeMapping.getNumberOfRangesInSheet(0)).toBe(0) }) it('large fixed #3', () => { @@ -107,7 +107,7 @@ describe('larger tests', () => { engine.setCellContents({sheet: 0, col: 0, row: 0}, null) expect(engine.dependencyGraph.graph.getNodes().length).toBe(0) - expect(engine.dependencyGraph.rangeMapping.getMappingSize(0)).toBe(0) + expect(engine.dependencyGraph.rangeMapping.getNumberOfRangesInSheet(0)).toBe(0) }) it('large fixed #4', () => { @@ -124,7 +124,7 @@ describe('larger tests', () => { engine.setCellContents({sheet: 0, col: 4, row: 0}, null) expect(engine.dependencyGraph.graph.getNodes().length).toBe(0) - expect(engine.dependencyGraph.rangeMapping.getMappingSize(0)).toBe(0) + expect(engine.dependencyGraph.rangeMapping.getNumberOfRangesInSheet(0)).toBe(0) }) it('repeat the same crud', () => { @@ -157,7 +157,7 @@ describe('larger tests', () => { } } expect(engine.dependencyGraph.graph.getNodes().length).toBe(0) - expect(engine.dependencyGraph.rangeMapping.getMappingSize(0)).toBe(0) + expect(engine.dependencyGraph.rangeMapping.getNumberOfRangesInSheet(0)).toBe(0) }) }) @@ -176,7 +176,7 @@ describe('cruds', () => { engine.removeRows(0, [2, 2]) expect(engine.dependencyGraph.graph.getNodes().length).toBe(0) - expect(engine.dependencyGraph.rangeMapping.getMappingSize(0)).toBe(0) + expect(engine.dependencyGraph.rangeMapping.getNumberOfRangesInSheet(0)).toBe(0) }) it('should collect empty vertices when bigger range is no longer bind to smaller range #2', () => { @@ -193,7 +193,7 @@ describe('cruds', () => { engine.removeRows(0, [2, 2]) expect(engine.dependencyGraph.graph.getNodes().length).toBe(0) - expect(engine.dependencyGraph.rangeMapping.getMappingSize(0)).toBe(0) + expect(engine.dependencyGraph.rangeMapping.getNumberOfRangesInSheet(0)).toBe(0) }) it('should collect empty vertices when bigger range is no longer bind to smaller range #3', () => { @@ -209,7 +209,7 @@ describe('cruds', () => { engine.removeRows(0, [4, 2]) expect(engine.dependencyGraph.graph.getNodes().length).toBe(0) - expect(engine.dependencyGraph.rangeMapping.getMappingSize(0)).toBe(0) + expect(engine.dependencyGraph.rangeMapping.getNumberOfRangesInSheet(0)).toBe(0) }) it('should collect empty vertices when bigger range is no longer bind to smaller range #4', () => { @@ -230,7 +230,7 @@ describe('cruds', () => { engine.removeRows(0, [0, 6]) expect(engine.dependencyGraph.graph.getNodes().length).toBe(0) - expect(engine.dependencyGraph.rangeMapping.getMappingSize(0)).toBe(0) + expect(engine.dependencyGraph.rangeMapping.getNumberOfRangesInSheet(0)).toBe(0) }) it('should collect empty vertices when bigger range is no longer bind to smaller range #5', () => { @@ -251,7 +251,7 @@ describe('cruds', () => { engine.removeRows(0, [0, 6]) expect(engine.dependencyGraph.graph.getNodes().length).toBe(0) - expect(engine.dependencyGraph.rangeMapping.getMappingSize(0)).toBe(0) + expect(engine.dependencyGraph.rangeMapping.getNumberOfRangesInSheet(0)).toBe(0) }) it('should collect empty vertices when bigger range is no longer bind to smaller range #6', () => { @@ -270,7 +270,7 @@ describe('cruds', () => { engine.removeRows(0, [0, 6]) expect(engine.dependencyGraph.graph.getNodes().length).toBe(0) - expect(engine.dependencyGraph.rangeMapping.getMappingSize(0)).toBe(0) + expect(engine.dependencyGraph.rangeMapping.getNumberOfRangesInSheet(0)).toBe(0) }) it('should collect empty vertices when bigger range is no longer bind to smaller range #7', () => { @@ -293,7 +293,7 @@ describe('cruds', () => { engine.removeRows(0, [0, 6]) expect(engine.dependencyGraph.graph.getNodes().length).toBe(0) - expect(engine.dependencyGraph.rangeMapping.getMappingSize(0)).toBe(0) + expect(engine.dependencyGraph.rangeMapping.getNumberOfRangesInSheet(0)).toBe(0) }) it('column adding', () => { @@ -330,7 +330,7 @@ describe('cruds', () => { engine.setCellContents(adr('A6'), null) expect(engine.dependencyGraph.graph.getNodes().length).toBe(0) - expect(engine.dependencyGraph.rangeMapping.getMappingSize(0)).toBe(0) + expect(engine.dependencyGraph.rangeMapping.getNumberOfRangesInSheet(0)).toBe(0) }) it('addColumns after addRows', () => { diff --git a/test/unit/graphComparator.ts b/test/unit/graphComparator.ts index a373cb63b..e9d977bce 100644 --- a/test/unit/graphComparator.ts +++ b/test/unit/graphComparator.ts @@ -3,9 +3,9 @@ import {CellError, HyperFormula} from '../../src' import {AbsoluteCellRange} from '../../src/AbsoluteCellRange' import {SimpleCellAddress, simpleCellAddress} from '../../src/Cell' import { - ArrayVertex, + ArrayFormulaVertex, EmptyCellVertex, - FormulaCellVertex, + ScalarFormulaVertex, ParsingErrorVertex, RangeVertex, ValueCellVertex, @@ -20,9 +20,9 @@ export class EngineComparator { private actual: HyperFormula) { } - public compare() { - const expectedNumberOfSheets = this.expected.sheetMapping.numberOfSheets() - const numberOfSheets = this.actual.sheetMapping.numberOfSheets() + public compare(): void { + const expectedNumberOfSheets = this.expected.sheetMapping.numberOfSheets({includePlaceholders: true}) + const numberOfSheets = this.actual.sheetMapping.numberOfSheets({includePlaceholders: true}) if (expectedNumberOfSheets !== numberOfSheets) { throw Error(`Expected number of sheets ${expectedNumberOfSheets}, actual: ${numberOfSheets}`) @@ -36,7 +36,7 @@ export class EngineComparator { } } - private compareSheet(sheet: number) { + private compareSheet(sheet: number): void { const expectedGraph = this.expected.graph const actualGraph = this.actual.graph @@ -44,10 +44,10 @@ export class EngineComparator { const actualSheetName = this.actual.getSheetName(sheet) equal(expectedSheetName, actualSheetName, `Expected sheet name '${expectedSheetName}', actual '${actualSheetName}'`) - const expectedWidth = this.expected.addressMapping.getWidth(sheet) - const expectedHeight = this.expected.addressMapping.getHeight(sheet) - const actualWidth = this.actual.addressMapping.getWidth(sheet) - const actualHeight = this.actual.addressMapping.getHeight(sheet) + const expectedWidth = this.expected.addressMapping.getSheetWidth(sheet) + const expectedHeight = this.expected.addressMapping.getSheetHeight(sheet) + const actualWidth = this.actual.addressMapping.getSheetWidth(sheet) + const actualHeight = this.actual.addressMapping.getSheetHeight(sheet) this.compareMatrixMappings() @@ -59,8 +59,8 @@ export class EngineComparator { if (expectedVertex === undefined && actualVertex === undefined) { continue } else if ( - (expectedVertex instanceof FormulaCellVertex && actualVertex instanceof FormulaCellVertex) || - (expectedVertex instanceof ArrayVertex && actualVertex instanceof ArrayVertex) + (expectedVertex instanceof ScalarFormulaVertex && actualVertex instanceof ScalarFormulaVertex) || + (expectedVertex instanceof ArrayFormulaVertex && actualVertex instanceof ArrayFormulaVertex) ) { const actualVertexAddress = actualVertex.getAddress(this.actual.dependencyGraph.lazilyTransformingAstService) const expectedVertexAddress = expectedVertex.getAddress(this.expected.dependencyGraph.lazilyTransformingAstService) @@ -87,7 +87,9 @@ export class EngineComparator { actualAdjacentAddresses.add(this.getAddressOfVertex(this.actual, adjacentNode)) } const sheetMapping = this.expected.sheetMapping - deepStrictEqual(actualAdjacentAddresses, expectedAdjacentAddresses, `Dependent vertices of ${simpleCellAddressToString(sheetMapping.fetchDisplayName, address, 0)} (Sheet '${sheetMapping.fetchDisplayName(address.sheet)}') are not same`) + deepStrictEqual(actualAdjacentAddresses, expectedAdjacentAddresses, `Dependent vertices of ${ + simpleCellAddressToString(sheetMapping.getSheetName.bind(sheetMapping), address, 0) ?? 'ERROR' + } (Sheet '${sheetMapping.getSheetName(address.sheet) ?? 'Unknown sheet'}') are not same`) } } } @@ -110,7 +112,7 @@ export class EngineComparator { expect(actual.size).toEqual(expected.size) for (const [key, value] of expected.entries()) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const actualEntry = actual.get(key)! expect(actualEntry).toBeDefined() expect(actualEntry.array.size.isRef).toBe(value.array.size.isRef) diff --git a/test/unit/interpreter.spec.ts b/test/unit/interpreter.spec.ts index 6854952d4..96b8630ef 100644 --- a/test/unit/interpreter.spec.ts +++ b/test/unit/interpreter.spec.ts @@ -177,4 +177,172 @@ describe('Interpreter', () => { expect(engine.getCellValue(adr('A5'))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.RangeManySheets)) expect(engine.getCellValue(adr('A6'))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.RangeManySheets)) }) + + it('should return #REF when referencing non-existing sheet - just cell reference', () => { + const engine = HyperFormula.buildFromArray([ + ['=NonExistingSheet!A1'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.REF)) + }) + + it('should return #REF when referencing non-existing sheet - cell reference inside a formula', () => { + const engine = HyperFormula.buildFromArray([ + ['=ABS(NonExistingSheet!A1)'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.REF)) + }) + + it('should return #REF when referencing non-existing sheet - cell reference inside a numeric aggregation formula', () => { + const engine = HyperFormula.buildFromArray([ + ['=SUM(NonExistingSheet!A1, 0)'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.REF)) + }) + + it('should return #REF when referencing non-existing sheet - cell range', () => { + const engine = HyperFormula.buildFromArray([ + ['=MEDIAN(NonExistingSheet!C4:F16)'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.REF)) + }) + + it('should return #REF when referencing non-existing sheet - cell range - numeric aggregation function', () => { + const engine = HyperFormula.buildFromArray([ + ['=SUM(NonExistingSheet!C4:F16)'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.REF)) + }) + + it('should return #REF when referencing non-existing sheet - row range', () => { + const engine = HyperFormula.buildFromArray([ + ['=MEDIAN(NonExistingSheet!1:2)'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.REF)) + }) + + it('should return #REF when referencing non-existing sheet - row range - numeric aggregation function', () => { + const engine = HyperFormula.buildFromArray([ + ['=SUM(NonExistingSheet!1:2)'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.REF)) + }) + + it('should return #REF when referencing non-existing sheet - column range', () => { + const engine = HyperFormula.buildFromArray([ + ['=MEDIAN(NonExistingSheet!A:B)'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.REF)) + }) + + it('should return #REF when referencing non-existing sheet - column range - numeric aggregation function', () => { + const engine = HyperFormula.buildFromArray([ + ['=SUM(NonExistingSheet!A:B)'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.REF)) + }) + + it('should return #REF when range starts with non-existing sheet - cell range', () => { + const engine = HyperFormula.buildFromArray([ + ['=MEDIAN(NonExistingSheet!A1:Sheet1!B2)'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.REF)) + }) + + it('should return #REF when range starts with non-existing sheet - cell range - numeric aggregation function', () => { + const engine = HyperFormula.buildFromArray([ + ['=SUM(NonExistingSheet!A1:Sheet1!B2)'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.REF)) + }) + + it('should return #REF when range ends with non-existing sheet - cell range', () => { + const engine = HyperFormula.buildFromArray([ + ['=MEDIAN(Sheet1!A1:NonExistingSheet!B2)'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.REF)) + }) + + it('should return #REF when range ends with non-existing sheet - cell range - numeric aggregation function', () => { + const engine = HyperFormula.buildFromArray([ + ['=SUM(Sheet1!A1:NonExistingSheet!B2)'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.REF)) + }) + + it('should return #REF when range starts with non-existing sheet - row range', () => { + const engine = HyperFormula.buildFromArray([ + ['=MEDIAN(NonExistingSheet!1:Sheet1!2)'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.REF)) + }) + + it('should return #REF when range starts with non-existing sheet - row range - numeric aggregation function', () => { + const engine = HyperFormula.buildFromArray([ + ['=SUM(NonExistingSheet!1:Sheet1!2)'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.REF)) + }) + + it('should return #REF when range ends with non-existing sheet - row range', () => { + const engine = HyperFormula.buildFromArray([ + ['=MEDIAN(Sheet1!1:NonExistingSheet!2)'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.REF)) + }) + + it('should return #REF when range ends with non-existing sheet - row range - numeric aggregation function', () => { + const engine = HyperFormula.buildFromArray([ + ['=SUM(Sheet1!1:NonExistingSheet!2)'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.REF)) + }) + + it('should return #REF when range starts with non-existing sheet - column range', () => { + const engine = HyperFormula.buildFromArray([ + ['=MEDIAN(NonExistingSheet!A:Sheet1!B)'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.REF)) + }) + + it('should return #REF when range starts with non-existing sheet - column range - numeric aggregation function', () => { + const engine = HyperFormula.buildFromArray([ + ['=SUM(NonExistingSheet!A:Sheet1!B)'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.REF)) + }) + + it('should return #REF when range ends with non-existing sheet - column range', () => { + const engine = HyperFormula.buildFromArray([ + ['=MEDIAN(Sheet1!A:NonExistingSheet!B)'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.REF)) + }) + + it('should return #REF when range ends with non-existing sheet - column range - numeric aggregation function', () => { + const engine = HyperFormula.buildFromArray([ + ['=SUM(Sheet1!A:NonExistingSheet!B)'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.REF)) + }) }) diff --git a/test/unit/interpreter/aliases.spec.ts b/test/unit/interpreter/aliases.spec.ts index 0f1b69d30..7a5627d2a 100644 --- a/test/unit/interpreter/aliases.spec.ts +++ b/test/unit/interpreter/aliases.spec.ts @@ -1,7 +1,5 @@ import {HyperFormula} from '../../../src' -/* eslint-disable @typescript-eslint/no-non-null-assertion */ - describe('Function aliases', () => { const engine = HyperFormula.buildEmpty() it('NEGBINOMDIST should be an alias of NEGBINOM.DIST', () => { diff --git a/test/unit/interpreter/function-date.spec.ts b/test/unit/interpreter/function-date.spec.ts index 342e1faba..c7078bef7 100644 --- a/test/unit/interpreter/function-date.spec.ts +++ b/test/unit/interpreter/function-date.spec.ts @@ -103,7 +103,7 @@ describe('Function DATE', () => { }) describe('Function DATE + leap years', () => { - it('should support nonleap year 2001 ', () => { + it('should support nonleap year 2001', () => { const config = new Config() const engine = HyperFormula.buildFromArray([ ['=DATE(2001, 02, 29)'], diff --git a/test/unit/interpreter/function-filter.spec.ts b/test/unit/interpreter/function-filter.spec.ts index 7a2efd3f9..8306f072b 100644 --- a/test/unit/interpreter/function-filter.spec.ts +++ b/test/unit/interpreter/function-filter.spec.ts @@ -33,7 +33,7 @@ describe('Function FILTER', () => { expect(engine.getSheetValues(0)).toEqual([[1], [1, 2, 3], [true, false, true], [true, true, false]]) }) - it('should filter a vertical range ', () => { + it('should filter a vertical range', () => { const engine = HyperFormula.buildFromArray([['=FILTER(B1:B3,C1:C3)', 1, true], [undefined, 2, false], [undefined, 3, true]]) expect(engine.getSheetValues(0)).toEqual([[1, 1, true], [3, 2, false], [null, 3, true]]) diff --git a/test/unit/interpreter/function-formulatext.spec.ts b/test/unit/interpreter/function-formulatext.spec.ts index d7c0f7705..a8e201633 100644 --- a/test/unit/interpreter/function-formulatext.spec.ts +++ b/test/unit/interpreter/function-formulatext.spec.ts @@ -57,7 +57,7 @@ describe('Function FORMULATEXT', () => { expect(engine.getCellValue(adr('B1'))).toEqual('=SUM(1, 2)') }) - it('should return REF when ', () => { + it('should return REF when', () => { const engine = HyperFormula.buildFromArray([ ['=SUM(1, 2)'] ]) diff --git a/test/unit/interpreter/function-roman.spec.ts b/test/unit/interpreter/function-roman.spec.ts index e29351023..d0ea81476 100644 --- a/test/unit/interpreter/function-roman.spec.ts +++ b/test/unit/interpreter/function-roman.spec.ts @@ -78,8 +78,8 @@ function input(mode: number) { return ret } -export const mode0 = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI', 'XII', 'XIII', 'XIV', 'XV', 'XVI', 'XVII', 'XVIII', 'XIX', 'XX', 'XXI', 'XXII', 'XXIII', 'XXIV', 'XXV', 'XXVI', 'XXVII', 'XXVIII', 'XXIX', 'XXX', 'XXXI', 'XXXII', 'XXXIII', 'XXXIV', 'XXXV', 'XXXVI', 'XXXVII', 'XXXVIII', 'XXXIX', 'XL', 'XLI', 'XLII', 'XLIII', 'XLIV', 'XLV', 'XLVI', 'XLVII', 'XLVIII', 'XLIX', 'L', 'LI', 'LII', 'LIII', 'LIV', 'LV', 'LVI', 'LVII', 'LVIII', 'LIX', 'LX', 'LXI', 'LXII', 'LXIII', 'LXIV', 'LXV', 'LXVI', 'LXVII', 'LXVIII', 'LXIX', 'LXX', 'LXXI', 'LXXII', 'LXXIII', 'LXXIV', 'LXXV', 'LXXVI', 'LXXVII', 'LXXVIII', 'LXXIX', 'LXXX', 'LXXXI', 'LXXXII', 'LXXXIII', 'LXXXIV', 'LXXXV', 'LXXXVI', 'LXXXVII', 'LXXXVIII', 'LXXXIX', 'XC', 'XCI', 'XCII', 'XCIII', 'XCIV', 'XCV', 'XCVI', 'XCVII', 'XCVIII', 'XCIX', 'C', 'CI', 'CII', 'CIII', 'CIV', 'CV', 'CVI', 'CVII', 'CVIII', 'CIX', 'CX', 'CXI', 'CXII', 'CXIII', 'CXIV', 'CXV', 'CXVI', 'CXVII', 'CXVIII', 'CXIX', 'CXX', 'CXXI', 'CXXII', 'CXXIII', 'CXXIV', 'CXXV', 'CXXVI', 'CXXVII', 'CXXVIII', 'CXXIX', 'CXXX', 'CXXXI', 'CXXXII', 'CXXXIII', 'CXXXIV', 'CXXXV', 'CXXXVI', 'CXXXVII', 'CXXXVIII', 'CXXXIX', 'CXL', 'CXLI', 'CXLII', 'CXLIII', 'CXLIV', 'CXLV', 'CXLVI', 'CXLVII', 'CXLVIII', 'CXLIX', 'CL', 'CLI', 'CLII', 'CLIII', 'CLIV', 'CLV', 'CLVI', 'CLVII', 'CLVIII', 'CLIX', 'CLX', 'CLXI', 'CLXII', 'CLXIII', 'CLXIV', 'CLXV', 'CLXVI', 'CLXVII', 'CLXVIII', 'CLXIX', 'CLXX', 'CLXXI', 'CLXXII', 'CLXXIII', 'CLXXIV', 'CLXXV', 'CLXXVI', 'CLXXVII', 'CLXXVIII', 'CLXXIX', 'CLXXX', 'CLXXXI', 'CLXXXII', 'CLXXXIII', 'CLXXXIV', 'CLXXXV', 'CLXXXVI', 'CLXXXVII', 'CLXXXVIII', 'CLXXXIX', 'CXC', 'CXCI', 'CXCII', 'CXCIII', 'CXCIV', 'CXCV', 'CXCVI', 'CXCVII', 'CXCVIII', 'CXCIX', 'CC', 'CCI', 'CCII', 'CCIII', 'CCIV', 'CCV', 'CCVI', 'CCVII', 'CCVIII', 'CCIX', 'CCX', 'CCXI', 'CCXII', 'CCXIII', 'CCXIV', 'CCXV', 'CCXVI', 'CCXVII', 'CCXVIII', 'CCXIX', 'CCXX', 'CCXXI', 'CCXXII', 'CCXXIII', 'CCXXIV', 'CCXXV', 'CCXXVI', 'CCXXVII', 'CCXXVIII', 'CCXXIX', 'CCXXX', 'CCXXXI', 'CCXXXII', 'CCXXXIII', 'CCXXXIV', 'CCXXXV', 'CCXXXVI', 'CCXXXVII', 'CCXXXVIII', 'CCXXXIX', 'CCXL', 'CCXLI', 'CCXLII', 'CCXLIII', 'CCXLIV', 'CCXLV', 'CCXLVI', 'CCXLVII', 'CCXLVIII', 'CCXLIX', 'CCL', 'CCLI', 'CCLII', 'CCLIII', 'CCLIV', 'CCLV', 'CCLVI', 'CCLVII', 'CCLVIII', 'CCLIX', 'CCLX', 'CCLXI', 'CCLXII', 'CCLXIII', 'CCLXIV', 'CCLXV', 'CCLXVI', 'CCLXVII', 'CCLXVIII', 'CCLXIX', 'CCLXX', 'CCLXXI', 'CCLXXII', 'CCLXXIII', 'CCLXXIV', 'CCLXXV', 'CCLXXVI', 'CCLXXVII', 'CCLXXVIII', 'CCLXXIX', 'CCLXXX', 'CCLXXXI', 'CCLXXXII', 'CCLXXXIII', 'CCLXXXIV', 'CCLXXXV', 'CCLXXXVI', 'CCLXXXVII', 'CCLXXXVIII', 'CCLXXXIX', 'CCXC', 'CCXCI', 'CCXCII', 'CCXCIII', 'CCXCIV', 'CCXCV', 'CCXCVI', 'CCXCVII', 'CCXCVIII', 'CCXCIX', 'CCC', 'CCCI', 'CCCII', 'CCCIII', 'CCCIV', 'CCCV', 'CCCVI', 'CCCVII', 'CCCVIII', 'CCCIX', 'CCCX', 'CCCXI', 'CCCXII', 'CCCXIII', 'CCCXIV', 'CCCXV', 'CCCXVI', 'CCCXVII', 'CCCXVIII', 'CCCXIX', 'CCCXX', 'CCCXXI', 'CCCXXII', 'CCCXXIII', 'CCCXXIV', 'CCCXXV', 'CCCXXVI', 'CCCXXVII', 'CCCXXVIII', 'CCCXXIX', 'CCCXXX', 'CCCXXXI', 'CCCXXXII', 'CCCXXXIII', 'CCCXXXIV', 'CCCXXXV', 'CCCXXXVI', 'CCCXXXVII', 'CCCXXXVIII', 'CCCXXXIX', 'CCCXL', 'CCCXLI', 'CCCXLII', 'CCCXLIII', 'CCCXLIV', 'CCCXLV', 'CCCXLVI', 'CCCXLVII', 'CCCXLVIII', 'CCCXLIX', 'CCCL', 'CCCLI', 'CCCLII', 'CCCLIII', 'CCCLIV', 'CCCLV', 'CCCLVI', 'CCCLVII', 'CCCLVIII', 'CCCLIX', 'CCCLX', 'CCCLXI', 'CCCLXII', 'CCCLXIII', 'CCCLXIV', 'CCCLXV', 'CCCLXVI', 'CCCLXVII', 'CCCLXVIII', 'CCCLXIX', 'CCCLXX', 'CCCLXXI', 'CCCLXXII', 'CCCLXXIII', 'CCCLXXIV', 'CCCLXXV', 'CCCLXXVI', 'CCCLXXVII', 'CCCLXXVIII', 'CCCLXXIX', 'CCCLXXX', 'CCCLXXXI', 'CCCLXXXII', 'CCCLXXXIII', 'CCCLXXXIV', 'CCCLXXXV', 'CCCLXXXVI', 'CCCLXXXVII', 'CCCLXXXVIII', 'CCCLXXXIX', 'CCCXC', 'CCCXCI', 'CCCXCII', 'CCCXCIII', 'CCCXCIV', 'CCCXCV', 'CCCXCVI', 'CCCXCVII', 'CCCXCVIII', 'CCCXCIX', 'CD', 'CDI', 'CDII', 'CDIII', 'CDIV', 'CDV', 'CDVI', 'CDVII', 'CDVIII', 'CDIX', 'CDX', 'CDXI', 'CDXII', 'CDXIII', 'CDXIV', 'CDXV', 'CDXVI', 'CDXVII', 'CDXVIII', 'CDXIX', 'CDXX', 'CDXXI', 'CDXXII', 'CDXXIII', 'CDXXIV', 'CDXXV', 'CDXXVI', 'CDXXVII', 'CDXXVIII', 'CDXXIX', 'CDXXX', 'CDXXXI', 'CDXXXII', 'CDXXXIII', 'CDXXXIV', 'CDXXXV', 'CDXXXVI', 'CDXXXVII', 'CDXXXVIII', 'CDXXXIX', 'CDXL', 'CDXLI', 'CDXLII', 'CDXLIII', 'CDXLIV', 'CDXLV', 'CDXLVI', 'CDXLVII', 'CDXLVIII', 'CDXLIX', 'CDL', 'CDLI', 'CDLII', 'CDLIII', 'CDLIV', 'CDLV', 'CDLVI', 'CDLVII', 'CDLVIII', 'CDLIX', 'CDLX', 'CDLXI', 'CDLXII', 'CDLXIII', 'CDLXIV', 'CDLXV', 'CDLXVI', 'CDLXVII', 'CDLXVIII', 'CDLXIX', 'CDLXX', 'CDLXXI', 'CDLXXII', 'CDLXXIII', 'CDLXXIV', 'CDLXXV', 'CDLXXVI', 'CDLXXVII', 'CDLXXVIII', 'CDLXXIX', 'CDLXXX', 'CDLXXXI', 'CDLXXXII', 'CDLXXXIII', 'CDLXXXIV', 'CDLXXXV', 'CDLXXXVI', 'CDLXXXVII', 'CDLXXXVIII', 'CDLXXXIX', 'CDXC', 'CDXCI', 'CDXCII', 'CDXCIII', 'CDXCIV', 'CDXCV', 'CDXCVI', 'CDXCVII', 'CDXCVIII', 'CDXCIX', 'D', 'DI', 'DII', 'DIII', 'DIV', 'DV', 'DVI', 'DVII', 'DVIII', 'DIX', 'DX', 'DXI', 'DXII', 'DXIII', 'DXIV', 'DXV', 'DXVI', 'DXVII', 'DXVIII', 'DXIX', 'DXX', 'DXXI', 'DXXII', 'DXXIII', 'DXXIV', 'DXXV', 'DXXVI', 'DXXVII', 'DXXVIII', 'DXXIX', 'DXXX', 'DXXXI', 'DXXXII', 'DXXXIII', 'DXXXIV', 'DXXXV', 'DXXXVI', 'DXXXVII', 'DXXXVIII', 'DXXXIX', 'DXL', 'DXLI', 'DXLII', 'DXLIII', 'DXLIV', 'DXLV', 'DXLVI', 'DXLVII', 'DXLVIII', 'DXLIX', 'DL', 'DLI', 'DLII', 'DLIII', 'DLIV', 'DLV', 'DLVI', 'DLVII', 'DLVIII', 'DLIX', 'DLX', 'DLXI', 'DLXII', 'DLXIII', 'DLXIV', 'DLXV', 'DLXVI', 'DLXVII', 'DLXVIII', 'DLXIX', 'DLXX', 'DLXXI', 'DLXXII', 'DLXXIII', 'DLXXIV', 'DLXXV', 'DLXXVI', 'DLXXVII', 'DLXXVIII', 'DLXXIX', 'DLXXX', 'DLXXXI', 'DLXXXII', 'DLXXXIII', 'DLXXXIV', 'DLXXXV', 'DLXXXVI', 'DLXXXVII', 'DLXXXVIII', 'DLXXXIX', 'DXC', 'DXCI', 'DXCII', 'DXCIII', 'DXCIV', 'DXCV', 'DXCVI', 'DXCVII', 'DXCVIII', 'DXCIX', 'DC', 'DCI', 'DCII', 'DCIII', 'DCIV', 'DCV', 'DCVI', 'DCVII', 'DCVIII', 'DCIX', 'DCX', 'DCXI', 'DCXII', 'DCXIII', 'DCXIV', 'DCXV', 'DCXVI', 'DCXVII', 'DCXVIII', 'DCXIX', 'DCXX', 'DCXXI', 'DCXXII', 'DCXXIII', 'DCXXIV', 'DCXXV', 'DCXXVI', 'DCXXVII', 'DCXXVIII', 'DCXXIX', 'DCXXX', 'DCXXXI', 'DCXXXII', 'DCXXXIII', 'DCXXXIV', 'DCXXXV', 'DCXXXVI', 'DCXXXVII', 'DCXXXVIII', 'DCXXXIX', 'DCXL', 'DCXLI', 'DCXLII', 'DCXLIII', 'DCXLIV', 'DCXLV', 'DCXLVI', 'DCXLVII', 'DCXLVIII', 'DCXLIX', 'DCL', 'DCLI', 'DCLII', 'DCLIII', 'DCLIV', 'DCLV', 'DCLVI', 'DCLVII', 'DCLVIII', 'DCLIX', 'DCLX', 'DCLXI', 'DCLXII', 'DCLXIII', 'DCLXIV', 'DCLXV', 'DCLXVI', 'DCLXVII', 'DCLXVIII', 'DCLXIX', 'DCLXX', 'DCLXXI', 'DCLXXII', 'DCLXXIII', 'DCLXXIV', 'DCLXXV', 'DCLXXVI', 'DCLXXVII', 'DCLXXVIII', 'DCLXXIX', 'DCLXXX', 'DCLXXXI', 'DCLXXXII', 'DCLXXXIII', 'DCLXXXIV', 'DCLXXXV', 'DCLXXXVI', 'DCLXXXVII', 'DCLXXXVIII', 'DCLXXXIX', 'DCXC', 'DCXCI', 'DCXCII', 'DCXCIII', 'DCXCIV', 'DCXCV', 'DCXCVI', 'DCXCVII', 'DCXCVIII', 'DCXCIX', 'DCC', 'DCCI', 'DCCII', 'DCCIII', 'DCCIV', 'DCCV', 'DCCVI', 'DCCVII', 'DCCVIII', 'DCCIX', 'DCCX', 'DCCXI', 'DCCXII', 'DCCXIII', 'DCCXIV', 'DCCXV', 'DCCXVI', 'DCCXVII', 'DCCXVIII', 'DCCXIX', 'DCCXX', 'DCCXXI', 'DCCXXII', 'DCCXXIII', 'DCCXXIV', 'DCCXXV', 'DCCXXVI', 'DCCXXVII', 'DCCXXVIII', 'DCCXXIX', 'DCCXXX', 'DCCXXXI', 'DCCXXXII', 'DCCXXXIII', 'DCCXXXIV', 'DCCXXXV', 'DCCXXXVI', 'DCCXXXVII', 'DCCXXXVIII', 'DCCXXXIX', 'DCCXL', 'DCCXLI', 'DCCXLII', 'DCCXLIII', 'DCCXLIV', 'DCCXLV', 'DCCXLVI', 'DCCXLVII', 'DCCXLVIII', 'DCCXLIX', 'DCCL', 'DCCLI', 'DCCLII', 'DCCLIII', 'DCCLIV', 'DCCLV', 'DCCLVI', 'DCCLVII', 'DCCLVIII', 'DCCLIX', 'DCCLX', 'DCCLXI', 'DCCLXII', 'DCCLXIII', 'DCCLXIV', 'DCCLXV', 'DCCLXVI', 'DCCLXVII', 'DCCLXVIII', 'DCCLXIX', 'DCCLXX', 'DCCLXXI', 'DCCLXXII', 'DCCLXXIII', 'DCCLXXIV', 'DCCLXXV', 'DCCLXXVI', 'DCCLXXVII', 'DCCLXXVIII', 'DCCLXXIX', 'DCCLXXX', 'DCCLXXXI', 'DCCLXXXII', 'DCCLXXXIII', 'DCCLXXXIV', 'DCCLXXXV', 'DCCLXXXVI', 'DCCLXXXVII', 'DCCLXXXVIII', 'DCCLXXXIX', 'DCCXC', 'DCCXCI', 'DCCXCII', 'DCCXCIII', 'DCCXCIV', 'DCCXCV', 'DCCXCVI', 'DCCXCVII', 'DCCXCVIII', 'DCCXCIX', 'DCCC', 'DCCCI', 'DCCCII', 'DCCCIII', 'DCCCIV', 'DCCCV', 'DCCCVI', 'DCCCVII', 'DCCCVIII', 'DCCCIX', 'DCCCX', 'DCCCXI', 'DCCCXII', 'DCCCXIII', 'DCCCXIV', 'DCCCXV', 'DCCCXVI', 'DCCCXVII', 'DCCCXVIII', 'DCCCXIX', 'DCCCXX', 'DCCCXXI', 'DCCCXXII', 'DCCCXXIII', 'DCCCXXIV', 'DCCCXXV', 'DCCCXXVI', 'DCCCXXVII', 'DCCCXXVIII', 'DCCCXXIX', 'DCCCXXX', 'DCCCXXXI', 'DCCCXXXII', 'DCCCXXXIII', 'DCCCXXXIV', 'DCCCXXXV', 'DCCCXXXVI', 'DCCCXXXVII', 'DCCCXXXVIII', 'DCCCXXXIX', 'DCCCXL', 'DCCCXLI', 'DCCCXLII', 'DCCCXLIII', 'DCCCXLIV', 'DCCCXLV', 'DCCCXLVI', 'DCCCXLVII', 'DCCCXLVIII', 'DCCCXLIX', 'DCCCL', 'DCCCLI', 'DCCCLII', 'DCCCLIII', 'DCCCLIV', 'DCCCLV', 'DCCCLVI', 'DCCCLVII', 'DCCCLVIII', 'DCCCLIX', 'DCCCLX', 'DCCCLXI', 'DCCCLXII', 'DCCCLXIII', 'DCCCLXIV', 'DCCCLXV', 'DCCCLXVI', 'DCCCLXVII', 'DCCCLXVIII', 'DCCCLXIX', 'DCCCLXX', 'DCCCLXXI', 'DCCCLXXII', 'DCCCLXXIII', 'DCCCLXXIV', 'DCCCLXXV', 'DCCCLXXVI', 'DCCCLXXVII', 'DCCCLXXVIII', 'DCCCLXXIX', 'DCCCLXXX', 'DCCCLXXXI', 'DCCCLXXXII', 'DCCCLXXXIII', 'DCCCLXXXIV', 'DCCCLXXXV', 'DCCCLXXXVI', 'DCCCLXXXVII', 'DCCCLXXXVIII', 'DCCCLXXXIX', 'DCCCXC', 'DCCCXCI', 'DCCCXCII', 'DCCCXCIII', 'DCCCXCIV', 'DCCCXCV', 'DCCCXCVI', 'DCCCXCVII', 'DCCCXCVIII', 'DCCCXCIX', 'CM', 'CMI', 'CMII', 'CMIII', 'CMIV', 'CMV', 'CMVI', 'CMVII', 'CMVIII', 'CMIX', 'CMX', 'CMXI', 'CMXII', 'CMXIII', 'CMXIV', 'CMXV', 'CMXVI', 'CMXVII', 'CMXVIII', 'CMXIX', 'CMXX', 'CMXXI', 'CMXXII', 'CMXXIII', 'CMXXIV', 'CMXXV', 'CMXXVI', 'CMXXVII', 'CMXXVIII', 'CMXXIX', 'CMXXX', 'CMXXXI', 'CMXXXII', 'CMXXXIII', 'CMXXXIV', 'CMXXXV', 'CMXXXVI', 'CMXXXVII', 'CMXXXVIII', 'CMXXXIX', 'CMXL', 'CMXLI', 'CMXLII', 'CMXLIII', 'CMXLIV', 'CMXLV', 'CMXLVI', 'CMXLVII', 'CMXLVIII', 'CMXLIX', 'CML', 'CMLI', 'CMLII', 'CMLIII', 'CMLIV', 'CMLV', 'CMLVI', 'CMLVII', 'CMLVIII', 'CMLIX', 'CMLX', 'CMLXI', 'CMLXII', 'CMLXIII', 'CMLXIV', 'CMLXV', 'CMLXVI', 'CMLXVII', 'CMLXVIII', 'CMLXIX', 'CMLXX', 'CMLXXI', 'CMLXXII', 'CMLXXIII', 'CMLXXIV', 'CMLXXV', 'CMLXXVI', 'CMLXXVII', 'CMLXXVIII', 'CMLXXIX', 'CMLXXX', 'CMLXXXI', 'CMLXXXII', 'CMLXXXIII', 'CMLXXXIV', 'CMLXXXV', 'CMLXXXVI', 'CMLXXXVII', 'CMLXXXVIII', 'CMLXXXIX', 'CMXC', 'CMXCI', 'CMXCII', 'CMXCIII', 'CMXCIV', 'CMXCV', 'CMXCVI', 'CMXCVII', 'CMXCVIII', 'CMXCIX', 'M', 'MI', 'MII', 'MIII', 'MIV', 'MV', 'MVI', 'MVII', 'MVIII', 'MIX', 'MX', 'MXI', 'MXII', 'MXIII', 'MXIV', 'MXV', 'MXVI', 'MXVII', 'MXVIII', 'MXIX', 'MXX', 'MXXI', 'MXXII', 'MXXIII', 'MXXIV', 'MXXV', 'MXXVI', 'MXXVII', 'MXXVIII', 'MXXIX', 'MXXX', 'MXXXI', 'MXXXII', 'MXXXIII', 'MXXXIV', 'MXXXV', 'MXXXVI', 'MXXXVII', 'MXXXVIII', 'MXXXIX', 'MXL', 'MXLI', 'MXLII', 'MXLIII', 'MXLIV', 'MXLV', 'MXLVI', 'MXLVII', 'MXLVIII', 'MXLIX', 'ML', 'MLI', 'MLII', 'MLIII', 'MLIV', 'MLV', 'MLVI', 'MLVII', 'MLVIII', 'MLIX', 'MLX', 'MLXI', 'MLXII', 'MLXIII', 'MLXIV', 'MLXV', 'MLXVI', 'MLXVII', 'MLXVIII', 'MLXIX', 'MLXX', 'MLXXI', 'MLXXII', 'MLXXIII', 'MLXXIV', 'MLXXV', 'MLXXVI', 'MLXXVII', 'MLXXVIII', 'MLXXIX', 'MLXXX', 'MLXXXI', 'MLXXXII', 'MLXXXIII', 'MLXXXIV', 'MLXXXV', 'MLXXXVI', 'MLXXXVII', 'MLXXXVIII', 'MLXXXIX', 'MXC', 'MXCI', 'MXCII', 'MXCIII', 'MXCIV', 'MXCV', 'MXCVI', 'MXCVII', 'MXCVIII', 'MXCIX', 'MC', 'MCI', 'MCII', 'MCIII', 'MCIV', 'MCV', 'MCVI', 'MCVII', 'MCVIII', 'MCIX', 'MCX', 'MCXI', 'MCXII', 'MCXIII', 'MCXIV', 'MCXV', 'MCXVI', 'MCXVII', 'MCXVIII', 'MCXIX', 'MCXX', 'MCXXI', 'MCXXII', 'MCXXIII', 'MCXXIV', 'MCXXV', 'MCXXVI', 'MCXXVII', 'MCXXVIII', 'MCXXIX', 'MCXXX', 'MCXXXI', 'MCXXXII', 'MCXXXIII', 'MCXXXIV', 'MCXXXV', 'MCXXXVI', 'MCXXXVII', 'MCXXXVIII', 'MCXXXIX', 'MCXL', 'MCXLI', 'MCXLII', 'MCXLIII', 'MCXLIV', 'MCXLV', 'MCXLVI', 'MCXLVII', 'MCXLVIII', 'MCXLIX', 'MCL', 'MCLI', 'MCLII', 'MCLIII', 'MCLIV', 'MCLV', 'MCLVI', 'MCLVII', 'MCLVIII', 'MCLIX', 'MCLX', 'MCLXI', 'MCLXII', 'MCLXIII', 'MCLXIV', 'MCLXV', 'MCLXVI', 'MCLXVII', 'MCLXVIII', 'MCLXIX', 'MCLXX', 'MCLXXI', 'MCLXXII', 'MCLXXIII', 'MCLXXIV', 'MCLXXV', 'MCLXXVI', 'MCLXXVII', 'MCLXXVIII', 'MCLXXIX', 'MCLXXX', 'MCLXXXI', 'MCLXXXII', 'MCLXXXIII', 'MCLXXXIV', 'MCLXXXV', 'MCLXXXVI', 'MCLXXXVII', 'MCLXXXVIII', 'MCLXXXIX', 'MCXC', 'MCXCI', 'MCXCII', 'MCXCIII', 'MCXCIV', 'MCXCV', 'MCXCVI', 'MCXCVII', 'MCXCVIII', 'MCXCIX', 'MCC', 'MCCI', 'MCCII', 'MCCIII', 'MCCIV', 'MCCV', 'MCCVI', 'MCCVII', 'MCCVIII', 'MCCIX', 'MCCX', 'MCCXI', 'MCCXII', 'MCCXIII', 'MCCXIV', 'MCCXV', 'MCCXVI', 'MCCXVII', 'MCCXVIII', 'MCCXIX', 'MCCXX', 'MCCXXI', 'MCCXXII', 'MCCXXIII', 'MCCXXIV', 'MCCXXV', 'MCCXXVI', 'MCCXXVII', 'MCCXXVIII', 'MCCXXIX', 'MCCXXX', 'MCCXXXI', 'MCCXXXII', 'MCCXXXIII', 'MCCXXXIV', 'MCCXXXV', 'MCCXXXVI', 'MCCXXXVII', 'MCCXXXVIII', 'MCCXXXIX', 'MCCXL', 'MCCXLI', 'MCCXLII', 'MCCXLIII', 'MCCXLIV', 'MCCXLV', 'MCCXLVI', 'MCCXLVII', 'MCCXLVIII', 'MCCXLIX', 'MCCL', 'MCCLI', 'MCCLII', 'MCCLIII', 'MCCLIV', 'MCCLV', 'MCCLVI', 'MCCLVII', 'MCCLVIII', 'MCCLIX', 'MCCLX', 'MCCLXI', 'MCCLXII', 'MCCLXIII', 'MCCLXIV', 'MCCLXV', 'MCCLXVI', 'MCCLXVII', 'MCCLXVIII', 'MCCLXIX', 'MCCLXX', 'MCCLXXI', 'MCCLXXII', 'MCCLXXIII', 'MCCLXXIV', 'MCCLXXV', 'MCCLXXVI', 'MCCLXXVII', 'MCCLXXVIII', 'MCCLXXIX', 'MCCLXXX', 'MCCLXXXI', 'MCCLXXXII', 'MCCLXXXIII', 'MCCLXXXIV', 'MCCLXXXV', 'MCCLXXXVI', 'MCCLXXXVII', 'MCCLXXXVIII', 'MCCLXXXIX', 'MCCXC', 'MCCXCI', 'MCCXCII', 'MCCXCIII', 'MCCXCIV', 'MCCXCV', 'MCCXCVI', 'MCCXCVII', 'MCCXCVIII', 'MCCXCIX', 'MCCC', 'MCCCI', 'MCCCII', 'MCCCIII', 'MCCCIV', 'MCCCV', 'MCCCVI', 'MCCCVII', 'MCCCVIII', 'MCCCIX', 'MCCCX', 'MCCCXI', 'MCCCXII', 'MCCCXIII', 'MCCCXIV', 'MCCCXV', 'MCCCXVI', 'MCCCXVII', 'MCCCXVIII', 'MCCCXIX', 'MCCCXX', 'MCCCXXI', 'MCCCXXII', 'MCCCXXIII', 'MCCCXXIV', 'MCCCXXV', 'MCCCXXVI', 'MCCCXXVII', 'MCCCXXVIII', 'MCCCXXIX', 'MCCCXXX', 'MCCCXXXI', 'MCCCXXXII', 'MCCCXXXIII', 'MCCCXXXIV', 'MCCCXXXV', 'MCCCXXXVI', 'MCCCXXXVII', 'MCCCXXXVIII', 'MCCCXXXIX', 'MCCCXL', 'MCCCXLI', 'MCCCXLII', 'MCCCXLIII', 'MCCCXLIV', 'MCCCXLV', 'MCCCXLVI', 'MCCCXLVII', 'MCCCXLVIII', 'MCCCXLIX', 'MCCCL', 'MCCCLI', 'MCCCLII', 'MCCCLIII', 'MCCCLIV', 'MCCCLV', 'MCCCLVI', 'MCCCLVII', 'MCCCLVIII', 'MCCCLIX', 'MCCCLX', 'MCCCLXI', 'MCCCLXII', 'MCCCLXIII', 'MCCCLXIV', 'MCCCLXV', 'MCCCLXVI', 'MCCCLXVII', 'MCCCLXVIII', 'MCCCLXIX', 'MCCCLXX', 'MCCCLXXI', 'MCCCLXXII', 'MCCCLXXIII', 'MCCCLXXIV', 'MCCCLXXV', 'MCCCLXXVI', 'MCCCLXXVII', 'MCCCLXXVIII', 'MCCCLXXIX', 'MCCCLXXX', 'MCCCLXXXI', 'MCCCLXXXII', 'MCCCLXXXIII', 'MCCCLXXXIV', 'MCCCLXXXV', 'MCCCLXXXVI', 'MCCCLXXXVII', 'MCCCLXXXVIII', 'MCCCLXXXIX', 'MCCCXC', 'MCCCXCI', 'MCCCXCII', 'MCCCXCIII', 'MCCCXCIV', 'MCCCXCV', 'MCCCXCVI', 'MCCCXCVII', 'MCCCXCVIII', 'MCCCXCIX', 'MCD', 'MCDI', 'MCDII', 'MCDIII', 'MCDIV', 'MCDV', 'MCDVI', 'MCDVII', 'MCDVIII', 'MCDIX', 'MCDX', 'MCDXI', 'MCDXII', 'MCDXIII', 'MCDXIV', 'MCDXV', 'MCDXVI', 'MCDXVII', 'MCDXVIII', 'MCDXIX', 'MCDXX', 'MCDXXI', 'MCDXXII', 'MCDXXIII', 'MCDXXIV', 'MCDXXV', 'MCDXXVI', 'MCDXXVII', 'MCDXXVIII', 'MCDXXIX', 'MCDXXX', 'MCDXXXI', 'MCDXXXII', 'MCDXXXIII', 'MCDXXXIV', 'MCDXXXV', 'MCDXXXVI', 'MCDXXXVII', 'MCDXXXVIII', 'MCDXXXIX', 'MCDXL', 'MCDXLI', 'MCDXLII', 'MCDXLIII', 'MCDXLIV', 'MCDXLV', 'MCDXLVI', 'MCDXLVII', 'MCDXLVIII', 'MCDXLIX', 'MCDL', 'MCDLI', 'MCDLII', 'MCDLIII', 'MCDLIV', 'MCDLV', 'MCDLVI', 'MCDLVII', 'MCDLVIII', 'MCDLIX', 'MCDLX', 'MCDLXI', 'MCDLXII', 'MCDLXIII', 'MCDLXIV', 'MCDLXV', 'MCDLXVI', 'MCDLXVII', 'MCDLXVIII', 'MCDLXIX', 'MCDLXX', 'MCDLXXI', 'MCDLXXII', 'MCDLXXIII', 'MCDLXXIV', 'MCDLXXV', 'MCDLXXVI', 'MCDLXXVII', 'MCDLXXVIII', 'MCDLXXIX', 'MCDLXXX', 'MCDLXXXI', 'MCDLXXXII', 'MCDLXXXIII', 'MCDLXXXIV', 'MCDLXXXV', 'MCDLXXXVI', 'MCDLXXXVII', 'MCDLXXXVIII', 'MCDLXXXIX', 'MCDXC', 'MCDXCI', 'MCDXCII', 'MCDXCIII', 'MCDXCIV', 'MCDXCV', 'MCDXCVI', 'MCDXCVII', 'MCDXCVIII', 'MCDXCIX', 'MD', 'MDI', 'MDII', 'MDIII', 'MDIV', 'MDV', 'MDVI', 'MDVII', 'MDVIII', 'MDIX', 'MDX', 'MDXI', 'MDXII', 'MDXIII', 'MDXIV', 'MDXV', 'MDXVI', 'MDXVII', 'MDXVIII', 'MDXIX', 'MDXX', 'MDXXI', 'MDXXII', 'MDXXIII', 'MDXXIV', 'MDXXV', 'MDXXVI', 'MDXXVII', 'MDXXVIII', 'MDXXIX', 'MDXXX', 'MDXXXI', 'MDXXXII', 'MDXXXIII', 'MDXXXIV', 'MDXXXV', 'MDXXXVI', 'MDXXXVII', 'MDXXXVIII', 'MDXXXIX', 'MDXL', 'MDXLI', 'MDXLII', 'MDXLIII', 'MDXLIV', 'MDXLV', 'MDXLVI', 'MDXLVII', 'MDXLVIII', 'MDXLIX', 'MDL', 'MDLI', 'MDLII', 'MDLIII', 'MDLIV', 'MDLV', 'MDLVI', 'MDLVII', 'MDLVIII', 'MDLIX', 'MDLX', 'MDLXI', 'MDLXII', 'MDLXIII', 'MDLXIV', 'MDLXV', 'MDLXVI', 'MDLXVII', 'MDLXVIII', 'MDLXIX', 'MDLXX', 'MDLXXI', 'MDLXXII', 'MDLXXIII', 'MDLXXIV', 'MDLXXV', 'MDLXXVI', 'MDLXXVII', 'MDLXXVIII', 'MDLXXIX', 'MDLXXX', 'MDLXXXI', 'MDLXXXII', 'MDLXXXIII', 'MDLXXXIV', 'MDLXXXV', 'MDLXXXVI', 'MDLXXXVII', 'MDLXXXVIII', 'MDLXXXIX', 'MDXC', 'MDXCI', 'MDXCII', 'MDXCIII', 'MDXCIV', 'MDXCV', 'MDXCVI', 'MDXCVII', 'MDXCVIII', 'MDXCIX', 'MDC', 'MDCI', 'MDCII', 'MDCIII', 'MDCIV', 'MDCV', 'MDCVI', 'MDCVII', 'MDCVIII', 'MDCIX', 'MDCX', 'MDCXI', 'MDCXII', 'MDCXIII', 'MDCXIV', 'MDCXV', 'MDCXVI', 'MDCXVII', 'MDCXVIII', 'MDCXIX', 'MDCXX', 'MDCXXI', 'MDCXXII', 'MDCXXIII', 'MDCXXIV', 'MDCXXV', 'MDCXXVI', 'MDCXXVII', 'MDCXXVIII', 'MDCXXIX', 'MDCXXX', 'MDCXXXI', 'MDCXXXII', 'MDCXXXIII', 'MDCXXXIV', 'MDCXXXV', 'MDCXXXVI', 'MDCXXXVII', 'MDCXXXVIII', 'MDCXXXIX', 'MDCXL', 'MDCXLI', 'MDCXLII', 'MDCXLIII', 'MDCXLIV', 'MDCXLV', 'MDCXLVI', 'MDCXLVII', 'MDCXLVIII', 'MDCXLIX', 'MDCL', 'MDCLI', 'MDCLII', 'MDCLIII', 'MDCLIV', 'MDCLV', 'MDCLVI', 'MDCLVII', 'MDCLVIII', 'MDCLIX', 'MDCLX', 'MDCLXI', 'MDCLXII', 'MDCLXIII', 'MDCLXIV', 'MDCLXV', 'MDCLXVI', 'MDCLXVII', 'MDCLXVIII', 'MDCLXIX', 'MDCLXX', 'MDCLXXI', 'MDCLXXII', 'MDCLXXIII', 'MDCLXXIV', 'MDCLXXV', 'MDCLXXVI', 'MDCLXXVII', 'MDCLXXVIII', 'MDCLXXIX', 'MDCLXXX', 'MDCLXXXI', 'MDCLXXXII', 'MDCLXXXIII', 'MDCLXXXIV', 'MDCLXXXV', 'MDCLXXXVI', 'MDCLXXXVII', 'MDCLXXXVIII', 'MDCLXXXIX', 'MDCXC', 'MDCXCI', 'MDCXCII', 'MDCXCIII', 'MDCXCIV', 'MDCXCV', 'MDCXCVI', 'MDCXCVII', 'MDCXCVIII', 'MDCXCIX', 'MDCC', 'MDCCI', 'MDCCII', 'MDCCIII', 'MDCCIV', 'MDCCV', 'MDCCVI', 'MDCCVII', 'MDCCVIII', 'MDCCIX', 'MDCCX', 'MDCCXI', 'MDCCXII', 'MDCCXIII', 'MDCCXIV', 'MDCCXV', 'MDCCXVI', 'MDCCXVII', 'MDCCXVIII', 'MDCCXIX', 'MDCCXX', 'MDCCXXI', 'MDCCXXII', 'MDCCXXIII', 'MDCCXXIV', 'MDCCXXV', 'MDCCXXVI', 'MDCCXXVII', 'MDCCXXVIII', 'MDCCXXIX', 'MDCCXXX', 'MDCCXXXI', 'MDCCXXXII', 'MDCCXXXIII', 'MDCCXXXIV', 'MDCCXXXV', 'MDCCXXXVI', 'MDCCXXXVII', 'MDCCXXXVIII', 'MDCCXXXIX', 'MDCCXL', 'MDCCXLI', 'MDCCXLII', 'MDCCXLIII', 'MDCCXLIV', 'MDCCXLV', 'MDCCXLVI', 'MDCCXLVII', 'MDCCXLVIII', 'MDCCXLIX', 'MDCCL', 'MDCCLI', 'MDCCLII', 'MDCCLIII', 'MDCCLIV', 'MDCCLV', 'MDCCLVI', 'MDCCLVII', 'MDCCLVIII', 'MDCCLIX', 'MDCCLX', 'MDCCLXI', 'MDCCLXII', 'MDCCLXIII', 'MDCCLXIV', 'MDCCLXV', 'MDCCLXVI', 'MDCCLXVII', 'MDCCLXVIII', 'MDCCLXIX', 'MDCCLXX', 'MDCCLXXI', 'MDCCLXXII', 'MDCCLXXIII', 'MDCCLXXIV', 'MDCCLXXV', 'MDCCLXXVI', 'MDCCLXXVII', 'MDCCLXXVIII', 'MDCCLXXIX', 'MDCCLXXX', 'MDCCLXXXI', 'MDCCLXXXII', 'MDCCLXXXIII', 'MDCCLXXXIV', 'MDCCLXXXV', 'MDCCLXXXVI', 'MDCCLXXXVII', 'MDCCLXXXVIII', 'MDCCLXXXIX', 'MDCCXC', 'MDCCXCI', 'MDCCXCII', 'MDCCXCIII', 'MDCCXCIV', 'MDCCXCV', 'MDCCXCVI', 'MDCCXCVII', 'MDCCXCVIII', 'MDCCXCIX', 'MDCCC', 'MDCCCI', 'MDCCCII', 'MDCCCIII', 'MDCCCIV', 'MDCCCV', 'MDCCCVI', 'MDCCCVII', 'MDCCCVIII', 'MDCCCIX', 'MDCCCX', 'MDCCCXI', 'MDCCCXII', 'MDCCCXIII', 'MDCCCXIV', 'MDCCCXV', 'MDCCCXVI', 'MDCCCXVII', 'MDCCCXVIII', 'MDCCCXIX', 'MDCCCXX', 'MDCCCXXI', 'MDCCCXXII', 'MDCCCXXIII', 'MDCCCXXIV', 'MDCCCXXV', 'MDCCCXXVI', 'MDCCCXXVII', 'MDCCCXXVIII', 'MDCCCXXIX', 'MDCCCXXX', 'MDCCCXXXI', 'MDCCCXXXII', 'MDCCCXXXIII', 'MDCCCXXXIV', 'MDCCCXXXV', 'MDCCCXXXVI', 'MDCCCXXXVII', 'MDCCCXXXVIII', 'MDCCCXXXIX', 'MDCCCXL', 'MDCCCXLI', 'MDCCCXLII', 'MDCCCXLIII', 'MDCCCXLIV', 'MDCCCXLV', 'MDCCCXLVI', 'MDCCCXLVII', 'MDCCCXLVIII', 'MDCCCXLIX', 'MDCCCL', 'MDCCCLI', 'MDCCCLII', 'MDCCCLIII', 'MDCCCLIV', 'MDCCCLV', 'MDCCCLVI', 'MDCCCLVII', 'MDCCCLVIII', 'MDCCCLIX', 'MDCCCLX', 'MDCCCLXI', 'MDCCCLXII', 'MDCCCLXIII', 'MDCCCLXIV', 'MDCCCLXV', 'MDCCCLXVI', 'MDCCCLXVII', 'MDCCCLXVIII', 'MDCCCLXIX', 'MDCCCLXX', 'MDCCCLXXI', 'MDCCCLXXII', 'MDCCCLXXIII', 'MDCCCLXXIV', 'MDCCCLXXV', 'MDCCCLXXVI', 'MDCCCLXXVII', 'MDCCCLXXVIII', 'MDCCCLXXIX', 'MDCCCLXXX', 'MDCCCLXXXI', 'MDCCCLXXXII', 'MDCCCLXXXIII', 'MDCCCLXXXIV', 'MDCCCLXXXV', 'MDCCCLXXXVI', 'MDCCCLXXXVII', 'MDCCCLXXXVIII', 'MDCCCLXXXIX', 'MDCCCXC', 'MDCCCXCI', 'MDCCCXCII', 'MDCCCXCIII', 'MDCCCXCIV', 'MDCCCXCV', 'MDCCCXCVI', 'MDCCCXCVII', 'MDCCCXCVIII', 'MDCCCXCIX', 'MCM', 'MCMI', 'MCMII', 'MCMIII', 'MCMIV', 'MCMV', 'MCMVI', 'MCMVII', 'MCMVIII', 'MCMIX', 'MCMX', 'MCMXI', 'MCMXII', 'MCMXIII', 'MCMXIV', 'MCMXV', 'MCMXVI', 'MCMXVII', 'MCMXVIII', 'MCMXIX', 'MCMXX', 'MCMXXI', 'MCMXXII', 'MCMXXIII', 'MCMXXIV', 'MCMXXV', 'MCMXXVI', 'MCMXXVII', 'MCMXXVIII', 'MCMXXIX', 'MCMXXX', 'MCMXXXI', 'MCMXXXII', 'MCMXXXIII', 'MCMXXXIV', 'MCMXXXV', 'MCMXXXVI', 'MCMXXXVII', 'MCMXXXVIII', 'MCMXXXIX', 'MCMXL', 'MCMXLI', 'MCMXLII', 'MCMXLIII', 'MCMXLIV', 'MCMXLV', 'MCMXLVI', 'MCMXLVII', 'MCMXLVIII', 'MCMXLIX', 'MCML', 'MCMLI', 'MCMLII', 'MCMLIII', 'MCMLIV', 'MCMLV', 'MCMLVI', 'MCMLVII', 'MCMLVIII', 'MCMLIX', 'MCMLX', 'MCMLXI', 'MCMLXII', 'MCMLXIII', 'MCMLXIV', 'MCMLXV', 'MCMLXVI', 'MCMLXVII', 'MCMLXVIII', 'MCMLXIX', 'MCMLXX', 'MCMLXXI', 'MCMLXXII', 'MCMLXXIII', 'MCMLXXIV', 'MCMLXXV', 'MCMLXXVI', 'MCMLXXVII', 'MCMLXXVIII', 'MCMLXXIX', 'MCMLXXX', 'MCMLXXXI', 'MCMLXXXII', 'MCMLXXXIII', 'MCMLXXXIV', 'MCMLXXXV', 'MCMLXXXVI', 'MCMLXXXVII', 'MCMLXXXVIII', 'MCMLXXXIX', 'MCMXC', 'MCMXCI', 'MCMXCII', 'MCMXCIII', 'MCMXCIV', 'MCMXCV', 'MCMXCVI', 'MCMXCVII', 'MCMXCVIII', 'MCMXCIX', 'MM', 'MMI', 'MMII', 'MMIII', 'MMIV', 'MMV', 'MMVI', 'MMVII', 'MMVIII', 'MMIX', 'MMX', 'MMXI', 'MMXII', 'MMXIII', 'MMXIV', 'MMXV', 'MMXVI', 'MMXVII', 'MMXVIII', 'MMXIX', 'MMXX', 'MMXXI', 'MMXXII', 'MMXXIII', 'MMXXIV', 'MMXXV', 'MMXXVI', 'MMXXVII', 'MMXXVIII', 'MMXXIX', 'MMXXX', 'MMXXXI', 'MMXXXII', 'MMXXXIII', 'MMXXXIV', 'MMXXXV', 'MMXXXVI', 'MMXXXVII', 'MMXXXVIII', 'MMXXXIX', 'MMXL', 'MMXLI', 'MMXLII', 'MMXLIII', 'MMXLIV', 'MMXLV', 'MMXLVI', 'MMXLVII', 'MMXLVIII', 'MMXLIX', 'MML', 'MMLI', 'MMLII', 'MMLIII', 'MMLIV', 'MMLV', 'MMLVI', 'MMLVII', 'MMLVIII', 'MMLIX', 'MMLX', 'MMLXI', 'MMLXII', 'MMLXIII', 'MMLXIV', 'MMLXV', 'MMLXVI', 'MMLXVII', 'MMLXVIII', 'MMLXIX', 'MMLXX', 'MMLXXI', 'MMLXXII', 'MMLXXIII', 'MMLXXIV', 'MMLXXV', 'MMLXXVI', 'MMLXXVII', 'MMLXXVIII', 'MMLXXIX', 'MMLXXX', 'MMLXXXI', 'MMLXXXII', 'MMLXXXIII', 'MMLXXXIV', 'MMLXXXV', 'MMLXXXVI', 'MMLXXXVII', 'MMLXXXVIII', 'MMLXXXIX', 'MMXC', 'MMXCI', 'MMXCII', 'MMXCIII', 'MMXCIV', 'MMXCV', 'MMXCVI', 'MMXCVII', 'MMXCVIII', 'MMXCIX', 'MMC', 'MMCI', 'MMCII', 'MMCIII', 'MMCIV', 'MMCV', 'MMCVI', 'MMCVII', 'MMCVIII', 'MMCIX', 'MMCX', 'MMCXI', 'MMCXII', 'MMCXIII', 'MMCXIV', 'MMCXV', 'MMCXVI', 'MMCXVII', 'MMCXVIII', 'MMCXIX', 'MMCXX', 'MMCXXI', 'MMCXXII', 'MMCXXIII', 'MMCXXIV', 'MMCXXV', 'MMCXXVI', 'MMCXXVII', 'MMCXXVIII', 'MMCXXIX', 'MMCXXX', 'MMCXXXI', 'MMCXXXII', 'MMCXXXIII', 'MMCXXXIV', 'MMCXXXV', 'MMCXXXVI', 'MMCXXXVII', 'MMCXXXVIII', 'MMCXXXIX', 'MMCXL', 'MMCXLI', 'MMCXLII', 'MMCXLIII', 'MMCXLIV', 'MMCXLV', 'MMCXLVI', 'MMCXLVII', 'MMCXLVIII', 'MMCXLIX', 'MMCL', 'MMCLI', 'MMCLII', 'MMCLIII', 'MMCLIV', 'MMCLV', 'MMCLVI', 'MMCLVII', 'MMCLVIII', 'MMCLIX', 'MMCLX', 'MMCLXI', 'MMCLXII', 'MMCLXIII', 'MMCLXIV', 'MMCLXV', 'MMCLXVI', 'MMCLXVII', 'MMCLXVIII', 'MMCLXIX', 'MMCLXX', 'MMCLXXI', 'MMCLXXII', 'MMCLXXIII', 'MMCLXXIV', 'MMCLXXV', 'MMCLXXVI', 'MMCLXXVII', 'MMCLXXVIII', 'MMCLXXIX', 'MMCLXXX', 'MMCLXXXI', 'MMCLXXXII', 'MMCLXXXIII', 'MMCLXXXIV', 'MMCLXXXV', 'MMCLXXXVI', 'MMCLXXXVII', 'MMCLXXXVIII', 'MMCLXXXIX', 'MMCXC', 'MMCXCI', 'MMCXCII', 'MMCXCIII', 'MMCXCIV', 'MMCXCV', 'MMCXCVI', 'MMCXCVII', 'MMCXCVIII', 'MMCXCIX', 'MMCC', 'MMCCI', 'MMCCII', 'MMCCIII', 'MMCCIV', 'MMCCV', 'MMCCVI', 'MMCCVII', 'MMCCVIII', 'MMCCIX', 'MMCCX', 'MMCCXI', 'MMCCXII', 'MMCCXIII', 'MMCCXIV', 'MMCCXV', 'MMCCXVI', 'MMCCXVII', 'MMCCXVIII', 'MMCCXIX', 'MMCCXX', 'MMCCXXI', 'MMCCXXII', 'MMCCXXIII', 'MMCCXXIV', 'MMCCXXV', 'MMCCXXVI', 'MMCCXXVII', 'MMCCXXVIII', 'MMCCXXIX', 'MMCCXXX', 'MMCCXXXI', 'MMCCXXXII', 'MMCCXXXIII', 'MMCCXXXIV', 'MMCCXXXV', 'MMCCXXXVI', 'MMCCXXXVII', 'MMCCXXXVIII', 'MMCCXXXIX', 'MMCCXL', 'MMCCXLI', 'MMCCXLII', 'MMCCXLIII', 'MMCCXLIV', 'MMCCXLV', 'MMCCXLVI', 'MMCCXLVII', 'MMCCXLVIII', 'MMCCXLIX', 'MMCCL', 'MMCCLI', 'MMCCLII', 'MMCCLIII', 'MMCCLIV', 'MMCCLV', 'MMCCLVI', 'MMCCLVII', 'MMCCLVIII', 'MMCCLIX', 'MMCCLX', 'MMCCLXI', 'MMCCLXII', 'MMCCLXIII', 'MMCCLXIV', 'MMCCLXV', 'MMCCLXVI', 'MMCCLXVII', 'MMCCLXVIII', 'MMCCLXIX', 'MMCCLXX', 'MMCCLXXI', 'MMCCLXXII', 'MMCCLXXIII', 'MMCCLXXIV', 'MMCCLXXV', 'MMCCLXXVI', 'MMCCLXXVII', 'MMCCLXXVIII', 'MMCCLXXIX', 'MMCCLXXX', 'MMCCLXXXI', 'MMCCLXXXII', 'MMCCLXXXIII', 'MMCCLXXXIV', 'MMCCLXXXV', 'MMCCLXXXVI', 'MMCCLXXXVII', 'MMCCLXXXVIII', 'MMCCLXXXIX', 'MMCCXC', 'MMCCXCI', 'MMCCXCII', 'MMCCXCIII', 'MMCCXCIV', 'MMCCXCV', 'MMCCXCVI', 'MMCCXCVII', 'MMCCXCVIII', 'MMCCXCIX', 'MMCCC', 'MMCCCI', 'MMCCCII', 'MMCCCIII', 'MMCCCIV', 'MMCCCV', 'MMCCCVI', 'MMCCCVII', 'MMCCCVIII', 'MMCCCIX', 'MMCCCX', 'MMCCCXI', 'MMCCCXII', 'MMCCCXIII', 'MMCCCXIV', 'MMCCCXV', 'MMCCCXVI', 'MMCCCXVII', 'MMCCCXVIII', 'MMCCCXIX', 'MMCCCXX', 'MMCCCXXI', 'MMCCCXXII', 'MMCCCXXIII', 'MMCCCXXIV', 'MMCCCXXV', 'MMCCCXXVI', 'MMCCCXXVII', 'MMCCCXXVIII', 'MMCCCXXIX', 'MMCCCXXX', 'MMCCCXXXI', 'MMCCCXXXII', 'MMCCCXXXIII', 'MMCCCXXXIV', 'MMCCCXXXV', 'MMCCCXXXVI', 'MMCCCXXXVII', 'MMCCCXXXVIII', 'MMCCCXXXIX', 'MMCCCXL', 'MMCCCXLI', 'MMCCCXLII', 'MMCCCXLIII', 'MMCCCXLIV', 'MMCCCXLV', 'MMCCCXLVI', 'MMCCCXLVII', 'MMCCCXLVIII', 'MMCCCXLIX', 'MMCCCL', 'MMCCCLI', 'MMCCCLII', 'MMCCCLIII', 'MMCCCLIV', 'MMCCCLV', 'MMCCCLVI', 'MMCCCLVII', 'MMCCCLVIII', 'MMCCCLIX', 'MMCCCLX', 'MMCCCLXI', 'MMCCCLXII', 'MMCCCLXIII', 'MMCCCLXIV', 'MMCCCLXV', 'MMCCCLXVI', 'MMCCCLXVII', 'MMCCCLXVIII', 'MMCCCLXIX', 'MMCCCLXX', 'MMCCCLXXI', 'MMCCCLXXII', 'MMCCCLXXIII', 'MMCCCLXXIV', 'MMCCCLXXV', 'MMCCCLXXVI', 'MMCCCLXXVII', 'MMCCCLXXVIII', 'MMCCCLXXIX', 'MMCCCLXXX', 'MMCCCLXXXI', 'MMCCCLXXXII', 'MMCCCLXXXIII', 'MMCCCLXXXIV', 'MMCCCLXXXV', 'MMCCCLXXXVI', 'MMCCCLXXXVII', 'MMCCCLXXXVIII', 'MMCCCLXXXIX', 'MMCCCXC', 'MMCCCXCI', 'MMCCCXCII', 'MMCCCXCIII', 'MMCCCXCIV', 'MMCCCXCV', 'MMCCCXCVI', 'MMCCCXCVII', 'MMCCCXCVIII', 'MMCCCXCIX', 'MMCD', 'MMCDI', 'MMCDII', 'MMCDIII', 'MMCDIV', 'MMCDV', 'MMCDVI', 'MMCDVII', 'MMCDVIII', 'MMCDIX', 'MMCDX', 'MMCDXI', 'MMCDXII', 'MMCDXIII', 'MMCDXIV', 'MMCDXV', 'MMCDXVI', 'MMCDXVII', 'MMCDXVIII', 'MMCDXIX', 'MMCDXX', 'MMCDXXI', 'MMCDXXII', 'MMCDXXIII', 'MMCDXXIV', 'MMCDXXV', 'MMCDXXVI', 'MMCDXXVII', 'MMCDXXVIII', 'MMCDXXIX', 'MMCDXXX', 'MMCDXXXI', 'MMCDXXXII', 'MMCDXXXIII', 'MMCDXXXIV', 'MMCDXXXV', 'MMCDXXXVI', 'MMCDXXXVII', 'MMCDXXXVIII', 'MMCDXXXIX', 'MMCDXL', 'MMCDXLI', 'MMCDXLII', 'MMCDXLIII', 'MMCDXLIV', 'MMCDXLV', 'MMCDXLVI', 'MMCDXLVII', 'MMCDXLVIII', 'MMCDXLIX', 'MMCDL', 'MMCDLI', 'MMCDLII', 'MMCDLIII', 'MMCDLIV', 'MMCDLV', 'MMCDLVI', 'MMCDLVII', 'MMCDLVIII', 'MMCDLIX', 'MMCDLX', 'MMCDLXI', 'MMCDLXII', 'MMCDLXIII', 'MMCDLXIV', 'MMCDLXV', 'MMCDLXVI', 'MMCDLXVII', 'MMCDLXVIII', 'MMCDLXIX', 'MMCDLXX', 'MMCDLXXI', 'MMCDLXXII', 'MMCDLXXIII', 'MMCDLXXIV', 'MMCDLXXV', 'MMCDLXXVI', 'MMCDLXXVII', 'MMCDLXXVIII', 'MMCDLXXIX', 'MMCDLXXX', 'MMCDLXXXI', 'MMCDLXXXII', 'MMCDLXXXIII', 'MMCDLXXXIV', 'MMCDLXXXV', 'MMCDLXXXVI', 'MMCDLXXXVII', 'MMCDLXXXVIII', 'MMCDLXXXIX', 'MMCDXC', 'MMCDXCI', 'MMCDXCII', 'MMCDXCIII', 'MMCDXCIV', 'MMCDXCV', 'MMCDXCVI', 'MMCDXCVII', 'MMCDXCVIII', 'MMCDXCIX', 'MMD', 'MMDI', 'MMDII', 'MMDIII', 'MMDIV', 'MMDV', 'MMDVI', 'MMDVII', 'MMDVIII', 'MMDIX', 'MMDX', 'MMDXI', 'MMDXII', 'MMDXIII', 'MMDXIV', 'MMDXV', 'MMDXVI', 'MMDXVII', 'MMDXVIII', 'MMDXIX', 'MMDXX', 'MMDXXI', 'MMDXXII', 'MMDXXIII', 'MMDXXIV', 'MMDXXV', 'MMDXXVI', 'MMDXXVII', 'MMDXXVIII', 'MMDXXIX', 'MMDXXX', 'MMDXXXI', 'MMDXXXII', 'MMDXXXIII', 'MMDXXXIV', 'MMDXXXV', 'MMDXXXVI', 'MMDXXXVII', 'MMDXXXVIII', 'MMDXXXIX', 'MMDXL', 'MMDXLI', 'MMDXLII', 'MMDXLIII', 'MMDXLIV', 'MMDXLV', 'MMDXLVI', 'MMDXLVII', 'MMDXLVIII', 'MMDXLIX', 'MMDL', 'MMDLI', 'MMDLII', 'MMDLIII', 'MMDLIV', 'MMDLV', 'MMDLVI', 'MMDLVII', 'MMDLVIII', 'MMDLIX', 'MMDLX', 'MMDLXI', 'MMDLXII', 'MMDLXIII', 'MMDLXIV', 'MMDLXV', 'MMDLXVI', 'MMDLXVII', 'MMDLXVIII', 'MMDLXIX', 'MMDLXX', 'MMDLXXI', 'MMDLXXII', 'MMDLXXIII', 'MMDLXXIV', 'MMDLXXV', 'MMDLXXVI', 'MMDLXXVII', 'MMDLXXVIII', 'MMDLXXIX', 'MMDLXXX', 'MMDLXXXI', 'MMDLXXXII', 'MMDLXXXIII', 'MMDLXXXIV', 'MMDLXXXV', 'MMDLXXXVI', 'MMDLXXXVII', 'MMDLXXXVIII', 'MMDLXXXIX', 'MMDXC', 'MMDXCI', 'MMDXCII', 'MMDXCIII', 'MMDXCIV', 'MMDXCV', 'MMDXCVI', 'MMDXCVII', 'MMDXCVIII', 'MMDXCIX', 'MMDC', 'MMDCI', 'MMDCII', 'MMDCIII', 'MMDCIV', 'MMDCV', 'MMDCVI', 'MMDCVII', 'MMDCVIII', 'MMDCIX', 'MMDCX', 'MMDCXI', 'MMDCXII', 'MMDCXIII', 'MMDCXIV', 'MMDCXV', 'MMDCXVI', 'MMDCXVII', 'MMDCXVIII', 'MMDCXIX', 'MMDCXX', 'MMDCXXI', 'MMDCXXII', 'MMDCXXIII', 'MMDCXXIV', 'MMDCXXV', 'MMDCXXVI', 'MMDCXXVII', 'MMDCXXVIII', 'MMDCXXIX', 'MMDCXXX', 'MMDCXXXI', 'MMDCXXXII', 'MMDCXXXIII', 'MMDCXXXIV', 'MMDCXXXV', 'MMDCXXXVI', 'MMDCXXXVII', 'MMDCXXXVIII', 'MMDCXXXIX', 'MMDCXL', 'MMDCXLI', 'MMDCXLII', 'MMDCXLIII', 'MMDCXLIV', 'MMDCXLV', 'MMDCXLVI', 'MMDCXLVII', 'MMDCXLVIII', 'MMDCXLIX', 'MMDCL', 'MMDCLI', 'MMDCLII', 'MMDCLIII', 'MMDCLIV', 'MMDCLV', 'MMDCLVI', 'MMDCLVII', 'MMDCLVIII', 'MMDCLIX', 'MMDCLX', 'MMDCLXI', 'MMDCLXII', 'MMDCLXIII', 'MMDCLXIV', 'MMDCLXV', 'MMDCLXVI', 'MMDCLXVII', 'MMDCLXVIII', 'MMDCLXIX', 'MMDCLXX', 'MMDCLXXI', 'MMDCLXXII', 'MMDCLXXIII', 'MMDCLXXIV', 'MMDCLXXV', 'MMDCLXXVI', 'MMDCLXXVII', 'MMDCLXXVIII', 'MMDCLXXIX', 'MMDCLXXX', 'MMDCLXXXI', 'MMDCLXXXII', 'MMDCLXXXIII', 'MMDCLXXXIV', 'MMDCLXXXV', 'MMDCLXXXVI', 'MMDCLXXXVII', 'MMDCLXXXVIII', 'MMDCLXXXIX', 'MMDCXC', 'MMDCXCI', 'MMDCXCII', 'MMDCXCIII', 'MMDCXCIV', 'MMDCXCV', 'MMDCXCVI', 'MMDCXCVII', 'MMDCXCVIII', 'MMDCXCIX', 'MMDCC', 'MMDCCI', 'MMDCCII', 'MMDCCIII', 'MMDCCIV', 'MMDCCV', 'MMDCCVI', 'MMDCCVII', 'MMDCCVIII', 'MMDCCIX', 'MMDCCX', 'MMDCCXI', 'MMDCCXII', 'MMDCCXIII', 'MMDCCXIV', 'MMDCCXV', 'MMDCCXVI', 'MMDCCXVII', 'MMDCCXVIII', 'MMDCCXIX', 'MMDCCXX', 'MMDCCXXI', 'MMDCCXXII', 'MMDCCXXIII', 'MMDCCXXIV', 'MMDCCXXV', 'MMDCCXXVI', 'MMDCCXXVII', 'MMDCCXXVIII', 'MMDCCXXIX', 'MMDCCXXX', 'MMDCCXXXI', 'MMDCCXXXII', 'MMDCCXXXIII', 'MMDCCXXXIV', 'MMDCCXXXV', 'MMDCCXXXVI', 'MMDCCXXXVII', 'MMDCCXXXVIII', 'MMDCCXXXIX', 'MMDCCXL', 'MMDCCXLI', 'MMDCCXLII', 'MMDCCXLIII', 'MMDCCXLIV', 'MMDCCXLV', 'MMDCCXLVI', 'MMDCCXLVII', 'MMDCCXLVIII', 'MMDCCXLIX', 'MMDCCL', 'MMDCCLI', 'MMDCCLII', 'MMDCCLIII', 'MMDCCLIV', 'MMDCCLV', 'MMDCCLVI', 'MMDCCLVII', 'MMDCCLVIII', 'MMDCCLIX', 'MMDCCLX', 'MMDCCLXI', 'MMDCCLXII', 'MMDCCLXIII', 'MMDCCLXIV', 'MMDCCLXV', 'MMDCCLXVI', 'MMDCCLXVII', 'MMDCCLXVIII', 'MMDCCLXIX', 'MMDCCLXX', 'MMDCCLXXI', 'MMDCCLXXII', 'MMDCCLXXIII', 'MMDCCLXXIV', 'MMDCCLXXV', 'MMDCCLXXVI', 'MMDCCLXXVII', 'MMDCCLXXVIII', 'MMDCCLXXIX', 'MMDCCLXXX', 'MMDCCLXXXI', 'MMDCCLXXXII', 'MMDCCLXXXIII', 'MMDCCLXXXIV', 'MMDCCLXXXV', 'MMDCCLXXXVI', 'MMDCCLXXXVII', 'MMDCCLXXXVIII', 'MMDCCLXXXIX', 'MMDCCXC', 'MMDCCXCI', 'MMDCCXCII', 'MMDCCXCIII', 'MMDCCXCIV', 'MMDCCXCV', 'MMDCCXCVI', 'MMDCCXCVII', 'MMDCCXCVIII', 'MMDCCXCIX', 'MMDCCC', 'MMDCCCI', 'MMDCCCII', 'MMDCCCIII', 'MMDCCCIV', 'MMDCCCV', 'MMDCCCVI', 'MMDCCCVII', 'MMDCCCVIII', 'MMDCCCIX', 'MMDCCCX', 'MMDCCCXI', 'MMDCCCXII', 'MMDCCCXIII', 'MMDCCCXIV', 'MMDCCCXV', 'MMDCCCXVI', 'MMDCCCXVII', 'MMDCCCXVIII', 'MMDCCCXIX', 'MMDCCCXX', 'MMDCCCXXI', 'MMDCCCXXII', 'MMDCCCXXIII', 'MMDCCCXXIV', 'MMDCCCXXV', 'MMDCCCXXVI', 'MMDCCCXXVII', 'MMDCCCXXVIII', 'MMDCCCXXIX', 'MMDCCCXXX', 'MMDCCCXXXI', 'MMDCCCXXXII', 'MMDCCCXXXIII', 'MMDCCCXXXIV', 'MMDCCCXXXV', 'MMDCCCXXXVI', 'MMDCCCXXXVII', 'MMDCCCXXXVIII', 'MMDCCCXXXIX', 'MMDCCCXL', 'MMDCCCXLI', 'MMDCCCXLII', 'MMDCCCXLIII', 'MMDCCCXLIV', 'MMDCCCXLV', 'MMDCCCXLVI', 'MMDCCCXLVII', 'MMDCCCXLVIII', 'MMDCCCXLIX', 'MMDCCCL', 'MMDCCCLI', 'MMDCCCLII', 'MMDCCCLIII', 'MMDCCCLIV', 'MMDCCCLV', 'MMDCCCLVI', 'MMDCCCLVII', 'MMDCCCLVIII', 'MMDCCCLIX', 'MMDCCCLX', 'MMDCCCLXI', 'MMDCCCLXII', 'MMDCCCLXIII', 'MMDCCCLXIV', 'MMDCCCLXV', 'MMDCCCLXVI', 'MMDCCCLXVII', 'MMDCCCLXVIII', 'MMDCCCLXIX', 'MMDCCCLXX', 'MMDCCCLXXI', 'MMDCCCLXXII', 'MMDCCCLXXIII', 'MMDCCCLXXIV', 'MMDCCCLXXV', 'MMDCCCLXXVI', 'MMDCCCLXXVII', 'MMDCCCLXXVIII', 'MMDCCCLXXIX', 'MMDCCCLXXX', 'MMDCCCLXXXI', 'MMDCCCLXXXII', 'MMDCCCLXXXIII', 'MMDCCCLXXXIV', 'MMDCCCLXXXV', 'MMDCCCLXXXVI', 'MMDCCCLXXXVII', 'MMDCCCLXXXVIII', 'MMDCCCLXXXIX', 'MMDCCCXC', 'MMDCCCXCI', 'MMDCCCXCII', 'MMDCCCXCIII', 'MMDCCCXCIV', 'MMDCCCXCV', 'MMDCCCXCVI', 'MMDCCCXCVII', 'MMDCCCXCVIII', 'MMDCCCXCIX', 'MMCM', 'MMCMI', 'MMCMII', 'MMCMIII', 'MMCMIV', 'MMCMV', 'MMCMVI', 'MMCMVII', 'MMCMVIII', 'MMCMIX', 'MMCMX', 'MMCMXI', 'MMCMXII', 'MMCMXIII', 'MMCMXIV', 'MMCMXV', 'MMCMXVI', 'MMCMXVII', 'MMCMXVIII', 'MMCMXIX', 'MMCMXX', 'MMCMXXI', 'MMCMXXII', 'MMCMXXIII', 'MMCMXXIV', 'MMCMXXV', 'MMCMXXVI', 'MMCMXXVII', 'MMCMXXVIII', 'MMCMXXIX', 'MMCMXXX', 'MMCMXXXI', 'MMCMXXXII', 'MMCMXXXIII', 'MMCMXXXIV', 'MMCMXXXV', 'MMCMXXXVI', 'MMCMXXXVII', 'MMCMXXXVIII', 'MMCMXXXIX', 'MMCMXL', 'MMCMXLI', 'MMCMXLII', 'MMCMXLIII', 'MMCMXLIV', 'MMCMXLV', 'MMCMXLVI', 'MMCMXLVII', 'MMCMXLVIII', 'MMCMXLIX', 'MMCML', 'MMCMLI', 'MMCMLII', 'MMCMLIII', 'MMCMLIV', 'MMCMLV', 'MMCMLVI', 'MMCMLVII', 'MMCMLVIII', 'MMCMLIX', 'MMCMLX', 'MMCMLXI', 'MMCMLXII', 'MMCMLXIII', 'MMCMLXIV', 'MMCMLXV', 'MMCMLXVI', 'MMCMLXVII', 'MMCMLXVIII', 'MMCMLXIX', 'MMCMLXX', 'MMCMLXXI', 'MMCMLXXII', 'MMCMLXXIII', 'MMCMLXXIV', 'MMCMLXXV', 'MMCMLXXVI', 'MMCMLXXVII', 'MMCMLXXVIII', 'MMCMLXXIX', 'MMCMLXXX', 'MMCMLXXXI', 'MMCMLXXXII', 'MMCMLXXXIII', 'MMCMLXXXIV', 'MMCMLXXXV', 'MMCMLXXXVI', 'MMCMLXXXVII', 'MMCMLXXXVIII', 'MMCMLXXXIX', 'MMCMXC', 'MMCMXCI', 'MMCMXCII', 'MMCMXCIII', 'MMCMXCIV', 'MMCMXCV', 'MMCMXCVI', 'MMCMXCVII', 'MMCMXCVIII', 'MMCMXCIX', 'MMM', 'MMMI', 'MMMII', 'MMMIII', 'MMMIV', 'MMMV', 'MMMVI', 'MMMVII', 'MMMVIII', 'MMMIX', 'MMMX', 'MMMXI', 'MMMXII', 'MMMXIII', 'MMMXIV', 'MMMXV', 'MMMXVI', 'MMMXVII', 'MMMXVIII', 'MMMXIX', 'MMMXX', 'MMMXXI', 'MMMXXII', 'MMMXXIII', 'MMMXXIV', 'MMMXXV', 'MMMXXVI', 'MMMXXVII', 'MMMXXVIII', 'MMMXXIX', 'MMMXXX', 'MMMXXXI', 'MMMXXXII', 'MMMXXXIII', 'MMMXXXIV', 'MMMXXXV', 'MMMXXXVI', 'MMMXXXVII', 'MMMXXXVIII', 'MMMXXXIX', 'MMMXL', 'MMMXLI', 'MMMXLII', 'MMMXLIII', 'MMMXLIV', 'MMMXLV', 'MMMXLVI', 'MMMXLVII', 'MMMXLVIII', 'MMMXLIX', 'MMML', 'MMMLI', 'MMMLII', 'MMMLIII', 'MMMLIV', 'MMMLV', 'MMMLVI', 'MMMLVII', 'MMMLVIII', 'MMMLIX', 'MMMLX', 'MMMLXI', 'MMMLXII', 'MMMLXIII', 'MMMLXIV', 'MMMLXV', 'MMMLXVI', 'MMMLXVII', 'MMMLXVIII', 'MMMLXIX', 'MMMLXX', 'MMMLXXI', 'MMMLXXII', 'MMMLXXIII', 'MMMLXXIV', 'MMMLXXV', 'MMMLXXVI', 'MMMLXXVII', 'MMMLXXVIII', 'MMMLXXIX', 'MMMLXXX', 'MMMLXXXI', 'MMMLXXXII', 'MMMLXXXIII', 'MMMLXXXIV', 'MMMLXXXV', 'MMMLXXXVI', 'MMMLXXXVII', 'MMMLXXXVIII', 'MMMLXXXIX', 'MMMXC', 'MMMXCI', 'MMMXCII', 'MMMXCIII', 'MMMXCIV', 'MMMXCV', 'MMMXCVI', 'MMMXCVII', 'MMMXCVIII', 'MMMXCIX', 'MMMC', 'MMMCI', 'MMMCII', 'MMMCIII', 'MMMCIV', 'MMMCV', 'MMMCVI', 'MMMCVII', 'MMMCVIII', 'MMMCIX', 'MMMCX', 'MMMCXI', 'MMMCXII', 'MMMCXIII', 'MMMCXIV', 'MMMCXV', 'MMMCXVI', 'MMMCXVII', 'MMMCXVIII', 'MMMCXIX', 'MMMCXX', 'MMMCXXI', 'MMMCXXII', 'MMMCXXIII', 'MMMCXXIV', 'MMMCXXV', 'MMMCXXVI', 'MMMCXXVII', 'MMMCXXVIII', 'MMMCXXIX', 'MMMCXXX', 'MMMCXXXI', 'MMMCXXXII', 'MMMCXXXIII', 'MMMCXXXIV', 'MMMCXXXV', 'MMMCXXXVI', 'MMMCXXXVII', 'MMMCXXXVIII', 'MMMCXXXIX', 'MMMCXL', 'MMMCXLI', 'MMMCXLII', 'MMMCXLIII', 'MMMCXLIV', 'MMMCXLV', 'MMMCXLVI', 'MMMCXLVII', 'MMMCXLVIII', 'MMMCXLIX', 'MMMCL', 'MMMCLI', 'MMMCLII', 'MMMCLIII', 'MMMCLIV', 'MMMCLV', 'MMMCLVI', 'MMMCLVII', 'MMMCLVIII', 'MMMCLIX', 'MMMCLX', 'MMMCLXI', 'MMMCLXII', 'MMMCLXIII', 'MMMCLXIV', 'MMMCLXV', 'MMMCLXVI', 'MMMCLXVII', 'MMMCLXVIII', 'MMMCLXIX', 'MMMCLXX', 'MMMCLXXI', 'MMMCLXXII', 'MMMCLXXIII', 'MMMCLXXIV', 'MMMCLXXV', 'MMMCLXXVI', 'MMMCLXXVII', 'MMMCLXXVIII', 'MMMCLXXIX', 'MMMCLXXX', 'MMMCLXXXI', 'MMMCLXXXII', 'MMMCLXXXIII', 'MMMCLXXXIV', 'MMMCLXXXV', 'MMMCLXXXVI', 'MMMCLXXXVII', 'MMMCLXXXVIII', 'MMMCLXXXIX', 'MMMCXC', 'MMMCXCI', 'MMMCXCII', 'MMMCXCIII', 'MMMCXCIV', 'MMMCXCV', 'MMMCXCVI', 'MMMCXCVII', 'MMMCXCVIII', 'MMMCXCIX', 'MMMCC', 'MMMCCI', 'MMMCCII', 'MMMCCIII', 'MMMCCIV', 'MMMCCV', 'MMMCCVI', 'MMMCCVII', 'MMMCCVIII', 'MMMCCIX', 'MMMCCX', 'MMMCCXI', 'MMMCCXII', 'MMMCCXIII', 'MMMCCXIV', 'MMMCCXV', 'MMMCCXVI', 'MMMCCXVII', 'MMMCCXVIII', 'MMMCCXIX', 'MMMCCXX', 'MMMCCXXI', 'MMMCCXXII', 'MMMCCXXIII', 'MMMCCXXIV', 'MMMCCXXV', 'MMMCCXXVI', 'MMMCCXXVII', 'MMMCCXXVIII', 'MMMCCXXIX', 'MMMCCXXX', 'MMMCCXXXI', 'MMMCCXXXII', 'MMMCCXXXIII', 'MMMCCXXXIV', 'MMMCCXXXV', 'MMMCCXXXVI', 'MMMCCXXXVII', 'MMMCCXXXVIII', 'MMMCCXXXIX', 'MMMCCXL', 'MMMCCXLI', 'MMMCCXLII', 'MMMCCXLIII', 'MMMCCXLIV', 'MMMCCXLV', 'MMMCCXLVI', 'MMMCCXLVII', 'MMMCCXLVIII', 'MMMCCXLIX', 'MMMCCL', 'MMMCCLI', 'MMMCCLII', 'MMMCCLIII', 'MMMCCLIV', 'MMMCCLV', 'MMMCCLVI', 'MMMCCLVII', 'MMMCCLVIII', 'MMMCCLIX', 'MMMCCLX', 'MMMCCLXI', 'MMMCCLXII', 'MMMCCLXIII', 'MMMCCLXIV', 'MMMCCLXV', 'MMMCCLXVI', 'MMMCCLXVII', 'MMMCCLXVIII', 'MMMCCLXIX', 'MMMCCLXX', 'MMMCCLXXI', 'MMMCCLXXII', 'MMMCCLXXIII', 'MMMCCLXXIV', 'MMMCCLXXV', 'MMMCCLXXVI', 'MMMCCLXXVII', 'MMMCCLXXVIII', 'MMMCCLXXIX', 'MMMCCLXXX', 'MMMCCLXXXI', 'MMMCCLXXXII', 'MMMCCLXXXIII', 'MMMCCLXXXIV', 'MMMCCLXXXV', 'MMMCCLXXXVI', 'MMMCCLXXXVII', 'MMMCCLXXXVIII', 'MMMCCLXXXIX', 'MMMCCXC', 'MMMCCXCI', 'MMMCCXCII', 'MMMCCXCIII', 'MMMCCXCIV', 'MMMCCXCV', 'MMMCCXCVI', 'MMMCCXCVII', 'MMMCCXCVIII', 'MMMCCXCIX', 'MMMCCC', 'MMMCCCI', 'MMMCCCII', 'MMMCCCIII', 'MMMCCCIV', 'MMMCCCV', 'MMMCCCVI', 'MMMCCCVII', 'MMMCCCVIII', 'MMMCCCIX', 'MMMCCCX', 'MMMCCCXI', 'MMMCCCXII', 'MMMCCCXIII', 'MMMCCCXIV', 'MMMCCCXV', 'MMMCCCXVI', 'MMMCCCXVII', 'MMMCCCXVIII', 'MMMCCCXIX', 'MMMCCCXX', 'MMMCCCXXI', 'MMMCCCXXII', 'MMMCCCXXIII', 'MMMCCCXXIV', 'MMMCCCXXV', 'MMMCCCXXVI', 'MMMCCCXXVII', 'MMMCCCXXVIII', 'MMMCCCXXIX', 'MMMCCCXXX', 'MMMCCCXXXI', 'MMMCCCXXXII', 'MMMCCCXXXIII', 'MMMCCCXXXIV', 'MMMCCCXXXV', 'MMMCCCXXXVI', 'MMMCCCXXXVII', 'MMMCCCXXXVIII', 'MMMCCCXXXIX', 'MMMCCCXL', 'MMMCCCXLI', 'MMMCCCXLII', 'MMMCCCXLIII', 'MMMCCCXLIV', 'MMMCCCXLV', 'MMMCCCXLVI', 'MMMCCCXLVII', 'MMMCCCXLVIII', 'MMMCCCXLIX', 'MMMCCCL', 'MMMCCCLI', 'MMMCCCLII', 'MMMCCCLIII', 'MMMCCCLIV', 'MMMCCCLV', 'MMMCCCLVI', 'MMMCCCLVII', 'MMMCCCLVIII', 'MMMCCCLIX', 'MMMCCCLX', 'MMMCCCLXI', 'MMMCCCLXII', 'MMMCCCLXIII', 'MMMCCCLXIV', 'MMMCCCLXV', 'MMMCCCLXVI', 'MMMCCCLXVII', 'MMMCCCLXVIII', 'MMMCCCLXIX', 'MMMCCCLXX', 'MMMCCCLXXI', 'MMMCCCLXXII', 'MMMCCCLXXIII', 'MMMCCCLXXIV', 'MMMCCCLXXV', 'MMMCCCLXXVI', 'MMMCCCLXXVII', 'MMMCCCLXXVIII', 'MMMCCCLXXIX', 'MMMCCCLXXX', 'MMMCCCLXXXI', 'MMMCCCLXXXII', 'MMMCCCLXXXIII', 'MMMCCCLXXXIV', 'MMMCCCLXXXV', 'MMMCCCLXXXVI', 'MMMCCCLXXXVII', 'MMMCCCLXXXVIII', 'MMMCCCLXXXIX', 'MMMCCCXC', 'MMMCCCXCI', 'MMMCCCXCII', 'MMMCCCXCIII', 'MMMCCCXCIV', 'MMMCCCXCV', 'MMMCCCXCVI', 'MMMCCCXCVII', 'MMMCCCXCVIII', 'MMMCCCXCIX', 'MMMCD', 'MMMCDI', 'MMMCDII', 'MMMCDIII', 'MMMCDIV', 'MMMCDV', 'MMMCDVI', 'MMMCDVII', 'MMMCDVIII', 'MMMCDIX', 'MMMCDX', 'MMMCDXI', 'MMMCDXII', 'MMMCDXIII', 'MMMCDXIV', 'MMMCDXV', 'MMMCDXVI', 'MMMCDXVII', 'MMMCDXVIII', 'MMMCDXIX', 'MMMCDXX', 'MMMCDXXI', 'MMMCDXXII', 'MMMCDXXIII', 'MMMCDXXIV', 'MMMCDXXV', 'MMMCDXXVI', 'MMMCDXXVII', 'MMMCDXXVIII', 'MMMCDXXIX', 'MMMCDXXX', 'MMMCDXXXI', 'MMMCDXXXII', 'MMMCDXXXIII', 'MMMCDXXXIV', 'MMMCDXXXV', 'MMMCDXXXVI', 'MMMCDXXXVII', 'MMMCDXXXVIII', 'MMMCDXXXIX', 'MMMCDXL', 'MMMCDXLI', 'MMMCDXLII', 'MMMCDXLIII', 'MMMCDXLIV', 'MMMCDXLV', 'MMMCDXLVI', 'MMMCDXLVII', 'MMMCDXLVIII', 'MMMCDXLIX', 'MMMCDL', 'MMMCDLI', 'MMMCDLII', 'MMMCDLIII', 'MMMCDLIV', 'MMMCDLV', 'MMMCDLVI', 'MMMCDLVII', 'MMMCDLVIII', 'MMMCDLIX', 'MMMCDLX', 'MMMCDLXI', 'MMMCDLXII', 'MMMCDLXIII', 'MMMCDLXIV', 'MMMCDLXV', 'MMMCDLXVI', 'MMMCDLXVII', 'MMMCDLXVIII', 'MMMCDLXIX', 'MMMCDLXX', 'MMMCDLXXI', 'MMMCDLXXII', 'MMMCDLXXIII', 'MMMCDLXXIV', 'MMMCDLXXV', 'MMMCDLXXVI', 'MMMCDLXXVII', 'MMMCDLXXVIII', 'MMMCDLXXIX', 'MMMCDLXXX', 'MMMCDLXXXI', 'MMMCDLXXXII', 'MMMCDLXXXIII', 'MMMCDLXXXIV', 'MMMCDLXXXV', 'MMMCDLXXXVI', 'MMMCDLXXXVII', 'MMMCDLXXXVIII', 'MMMCDLXXXIX', 'MMMCDXC', 'MMMCDXCI', 'MMMCDXCII', 'MMMCDXCIII', 'MMMCDXCIV', 'MMMCDXCV', 'MMMCDXCVI', 'MMMCDXCVII', 'MMMCDXCVIII', 'MMMCDXCIX', 'MMMD', 'MMMDI', 'MMMDII', 'MMMDIII', 'MMMDIV', 'MMMDV', 'MMMDVI', 'MMMDVII', 'MMMDVIII', 'MMMDIX', 'MMMDX', 'MMMDXI', 'MMMDXII', 'MMMDXIII', 'MMMDXIV', 'MMMDXV', 'MMMDXVI', 'MMMDXVII', 'MMMDXVIII', 'MMMDXIX', 'MMMDXX', 'MMMDXXI', 'MMMDXXII', 'MMMDXXIII', 'MMMDXXIV', 'MMMDXXV', 'MMMDXXVI', 'MMMDXXVII', 'MMMDXXVIII', 'MMMDXXIX', 'MMMDXXX', 'MMMDXXXI', 'MMMDXXXII', 'MMMDXXXIII', 'MMMDXXXIV', 'MMMDXXXV', 'MMMDXXXVI', 'MMMDXXXVII', 'MMMDXXXVIII', 'MMMDXXXIX', 'MMMDXL', 'MMMDXLI', 'MMMDXLII', 'MMMDXLIII', 'MMMDXLIV', 'MMMDXLV', 'MMMDXLVI', 'MMMDXLVII', 'MMMDXLVIII', 'MMMDXLIX', 'MMMDL', 'MMMDLI', 'MMMDLII', 'MMMDLIII', 'MMMDLIV', 'MMMDLV', 'MMMDLVI', 'MMMDLVII', 'MMMDLVIII', 'MMMDLIX', 'MMMDLX', 'MMMDLXI', 'MMMDLXII', 'MMMDLXIII', 'MMMDLXIV', 'MMMDLXV', 'MMMDLXVI', 'MMMDLXVII', 'MMMDLXVIII', 'MMMDLXIX', 'MMMDLXX', 'MMMDLXXI', 'MMMDLXXII', 'MMMDLXXIII', 'MMMDLXXIV', 'MMMDLXXV', 'MMMDLXXVI', 'MMMDLXXVII', 'MMMDLXXVIII', 'MMMDLXXIX', 'MMMDLXXX', 'MMMDLXXXI', 'MMMDLXXXII', 'MMMDLXXXIII', 'MMMDLXXXIV', 'MMMDLXXXV', 'MMMDLXXXVI', 'MMMDLXXXVII', 'MMMDLXXXVIII', 'MMMDLXXXIX', 'MMMDXC', 'MMMDXCI', 'MMMDXCII', 'MMMDXCIII', 'MMMDXCIV', 'MMMDXCV', 'MMMDXCVI', 'MMMDXCVII', 'MMMDXCVIII', 'MMMDXCIX', 'MMMDC', 'MMMDCI', 'MMMDCII', 'MMMDCIII', 'MMMDCIV', 'MMMDCV', 'MMMDCVI', 'MMMDCVII', 'MMMDCVIII', 'MMMDCIX', 'MMMDCX', 'MMMDCXI', 'MMMDCXII', 'MMMDCXIII', 'MMMDCXIV', 'MMMDCXV', 'MMMDCXVI', 'MMMDCXVII', 'MMMDCXVIII', 'MMMDCXIX', 'MMMDCXX', 'MMMDCXXI', 'MMMDCXXII', 'MMMDCXXIII', 'MMMDCXXIV', 'MMMDCXXV', 'MMMDCXXVI', 'MMMDCXXVII', 'MMMDCXXVIII', 'MMMDCXXIX', 'MMMDCXXX', 'MMMDCXXXI', 'MMMDCXXXII', 'MMMDCXXXIII', 'MMMDCXXXIV', 'MMMDCXXXV', 'MMMDCXXXVI', 'MMMDCXXXVII', 'MMMDCXXXVIII', 'MMMDCXXXIX', 'MMMDCXL', 'MMMDCXLI', 'MMMDCXLII', 'MMMDCXLIII', 'MMMDCXLIV', 'MMMDCXLV', 'MMMDCXLVI', 'MMMDCXLVII', 'MMMDCXLVIII', 'MMMDCXLIX', 'MMMDCL', 'MMMDCLI', 'MMMDCLII', 'MMMDCLIII', 'MMMDCLIV', 'MMMDCLV', 'MMMDCLVI', 'MMMDCLVII', 'MMMDCLVIII', 'MMMDCLIX', 'MMMDCLX', 'MMMDCLXI', 'MMMDCLXII', 'MMMDCLXIII', 'MMMDCLXIV', 'MMMDCLXV', 'MMMDCLXVI', 'MMMDCLXVII', 'MMMDCLXVIII', 'MMMDCLXIX', 'MMMDCLXX', 'MMMDCLXXI', 'MMMDCLXXII', 'MMMDCLXXIII', 'MMMDCLXXIV', 'MMMDCLXXV', 'MMMDCLXXVI', 'MMMDCLXXVII', 'MMMDCLXXVIII', 'MMMDCLXXIX', 'MMMDCLXXX', 'MMMDCLXXXI', 'MMMDCLXXXII', 'MMMDCLXXXIII', 'MMMDCLXXXIV', 'MMMDCLXXXV', 'MMMDCLXXXVI', 'MMMDCLXXXVII', 'MMMDCLXXXVIII', 'MMMDCLXXXIX', 'MMMDCXC', 'MMMDCXCI', 'MMMDCXCII', 'MMMDCXCIII', 'MMMDCXCIV', 'MMMDCXCV', 'MMMDCXCVI', 'MMMDCXCVII', 'MMMDCXCVIII', 'MMMDCXCIX', 'MMMDCC', 'MMMDCCI', 'MMMDCCII', 'MMMDCCIII', 'MMMDCCIV', 'MMMDCCV', 'MMMDCCVI', 'MMMDCCVII', 'MMMDCCVIII', 'MMMDCCIX', 'MMMDCCX', 'MMMDCCXI', 'MMMDCCXII', 'MMMDCCXIII', 'MMMDCCXIV', 'MMMDCCXV', 'MMMDCCXVI', 'MMMDCCXVII', 'MMMDCCXVIII', 'MMMDCCXIX', 'MMMDCCXX', 'MMMDCCXXI', 'MMMDCCXXII', 'MMMDCCXXIII', 'MMMDCCXXIV', 'MMMDCCXXV', 'MMMDCCXXVI', 'MMMDCCXXVII', 'MMMDCCXXVIII', 'MMMDCCXXIX', 'MMMDCCXXX', 'MMMDCCXXXI', 'MMMDCCXXXII', 'MMMDCCXXXIII', 'MMMDCCXXXIV', 'MMMDCCXXXV', 'MMMDCCXXXVI', 'MMMDCCXXXVII', 'MMMDCCXXXVIII', 'MMMDCCXXXIX', 'MMMDCCXL', 'MMMDCCXLI', 'MMMDCCXLII', 'MMMDCCXLIII', 'MMMDCCXLIV', 'MMMDCCXLV', 'MMMDCCXLVI', 'MMMDCCXLVII', 'MMMDCCXLVIII', 'MMMDCCXLIX', 'MMMDCCL', 'MMMDCCLI', 'MMMDCCLII', 'MMMDCCLIII', 'MMMDCCLIV', 'MMMDCCLV', 'MMMDCCLVI', 'MMMDCCLVII', 'MMMDCCLVIII', 'MMMDCCLIX', 'MMMDCCLX', 'MMMDCCLXI', 'MMMDCCLXII', 'MMMDCCLXIII', 'MMMDCCLXIV', 'MMMDCCLXV', 'MMMDCCLXVI', 'MMMDCCLXVII', 'MMMDCCLXVIII', 'MMMDCCLXIX', 'MMMDCCLXX', 'MMMDCCLXXI', 'MMMDCCLXXII', 'MMMDCCLXXIII', 'MMMDCCLXXIV', 'MMMDCCLXXV', 'MMMDCCLXXVI', 'MMMDCCLXXVII', 'MMMDCCLXXVIII', 'MMMDCCLXXIX', 'MMMDCCLXXX', 'MMMDCCLXXXI', 'MMMDCCLXXXII', 'MMMDCCLXXXIII', 'MMMDCCLXXXIV', 'MMMDCCLXXXV', 'MMMDCCLXXXVI', 'MMMDCCLXXXVII', 'MMMDCCLXXXVIII', 'MMMDCCLXXXIX', 'MMMDCCXC', 'MMMDCCXCI', 'MMMDCCXCII', 'MMMDCCXCIII', 'MMMDCCXCIV', 'MMMDCCXCV', 'MMMDCCXCVI', 'MMMDCCXCVII', 'MMMDCCXCVIII', 'MMMDCCXCIX', 'MMMDCCC', 'MMMDCCCI', 'MMMDCCCII', 'MMMDCCCIII', 'MMMDCCCIV', 'MMMDCCCV', 'MMMDCCCVI', 'MMMDCCCVII', 'MMMDCCCVIII', 'MMMDCCCIX', 'MMMDCCCX', 'MMMDCCCXI', 'MMMDCCCXII', 'MMMDCCCXIII', 'MMMDCCCXIV', 'MMMDCCCXV', 'MMMDCCCXVI', 'MMMDCCCXVII', 'MMMDCCCXVIII', 'MMMDCCCXIX', 'MMMDCCCXX', 'MMMDCCCXXI', 'MMMDCCCXXII', 'MMMDCCCXXIII', 'MMMDCCCXXIV', 'MMMDCCCXXV', 'MMMDCCCXXVI', 'MMMDCCCXXVII', 'MMMDCCCXXVIII', 'MMMDCCCXXIX', 'MMMDCCCXXX', 'MMMDCCCXXXI', 'MMMDCCCXXXII', 'MMMDCCCXXXIII', 'MMMDCCCXXXIV', 'MMMDCCCXXXV', 'MMMDCCCXXXVI', 'MMMDCCCXXXVII', 'MMMDCCCXXXVIII', 'MMMDCCCXXXIX', 'MMMDCCCXL', 'MMMDCCCXLI', 'MMMDCCCXLII', 'MMMDCCCXLIII', 'MMMDCCCXLIV', 'MMMDCCCXLV', 'MMMDCCCXLVI', 'MMMDCCCXLVII', 'MMMDCCCXLVIII', 'MMMDCCCXLIX', 'MMMDCCCL', 'MMMDCCCLI', 'MMMDCCCLII', 'MMMDCCCLIII', 'MMMDCCCLIV', 'MMMDCCCLV', 'MMMDCCCLVI', 'MMMDCCCLVII', 'MMMDCCCLVIII', 'MMMDCCCLIX', 'MMMDCCCLX', 'MMMDCCCLXI', 'MMMDCCCLXII', 'MMMDCCCLXIII', 'MMMDCCCLXIV', 'MMMDCCCLXV', 'MMMDCCCLXVI', 'MMMDCCCLXVII', 'MMMDCCCLXVIII', 'MMMDCCCLXIX', 'MMMDCCCLXX', 'MMMDCCCLXXI', 'MMMDCCCLXXII', 'MMMDCCCLXXIII', 'MMMDCCCLXXIV', 'MMMDCCCLXXV', 'MMMDCCCLXXVI', 'MMMDCCCLXXVII', 'MMMDCCCLXXVIII', 'MMMDCCCLXXIX', 'MMMDCCCLXXX', 'MMMDCCCLXXXI', 'MMMDCCCLXXXII', 'MMMDCCCLXXXIII', 'MMMDCCCLXXXIV', 'MMMDCCCLXXXV', 'MMMDCCCLXXXVI', 'MMMDCCCLXXXVII', 'MMMDCCCLXXXVIII', 'MMMDCCCLXXXIX', 'MMMDCCCXC', 'MMMDCCCXCI', 'MMMDCCCXCII', 'MMMDCCCXCIII', 'MMMDCCCXCIV', 'MMMDCCCXCV', 'MMMDCCCXCVI', 'MMMDCCCXCVII', 'MMMDCCCXCVIII', 'MMMDCCCXCIX', 'MMMCM', 'MMMCMI', 'MMMCMII', 'MMMCMIII', 'MMMCMIV', 'MMMCMV', 'MMMCMVI', 'MMMCMVII', 'MMMCMVIII', 'MMMCMIX', 'MMMCMX', 'MMMCMXI', 'MMMCMXII', 'MMMCMXIII', 'MMMCMXIV', 'MMMCMXV', 'MMMCMXVI', 'MMMCMXVII', 'MMMCMXVIII', 'MMMCMXIX', 'MMMCMXX', 'MMMCMXXI', 'MMMCMXXII', 'MMMCMXXIII', 'MMMCMXXIV', 'MMMCMXXV', 'MMMCMXXVI', 'MMMCMXXVII', 'MMMCMXXVIII', 'MMMCMXXIX', 'MMMCMXXX', 'MMMCMXXXI', 'MMMCMXXXII', 'MMMCMXXXIII', 'MMMCMXXXIV', 'MMMCMXXXV', 'MMMCMXXXVI', 'MMMCMXXXVII', 'MMMCMXXXVIII', 'MMMCMXXXIX', 'MMMCMXL', 'MMMCMXLI', 'MMMCMXLII', 'MMMCMXLIII', 'MMMCMXLIV', 'MMMCMXLV', 'MMMCMXLVI', 'MMMCMXLVII', 'MMMCMXLVIII', 'MMMCMXLIX', 'MMMCML', 'MMMCMLI', 'MMMCMLII', 'MMMCMLIII', 'MMMCMLIV', 'MMMCMLV', 'MMMCMLVI', 'MMMCMLVII', 'MMMCMLVIII', 'MMMCMLIX', 'MMMCMLX', 'MMMCMLXI', 'MMMCMLXII', 'MMMCMLXIII', 'MMMCMLXIV', 'MMMCMLXV', 'MMMCMLXVI', 'MMMCMLXVII', 'MMMCMLXVIII', 'MMMCMLXIX', 'MMMCMLXX', 'MMMCMLXXI', 'MMMCMLXXII', 'MMMCMLXXIII', 'MMMCMLXXIV', 'MMMCMLXXV', 'MMMCMLXXVI', 'MMMCMLXXVII', 'MMMCMLXXVIII', 'MMMCMLXXIX', 'MMMCMLXXX', 'MMMCMLXXXI', 'MMMCMLXXXII', 'MMMCMLXXXIII', 'MMMCMLXXXIV', 'MMMCMLXXXV', 'MMMCMLXXXVI', 'MMMCMLXXXVII', 'MMMCMLXXXVIII', 'MMMCMLXXXIX', 'MMMCMXC', 'MMMCMXCI', 'MMMCMXCII', 'MMMCMXCIII', 'MMMCMXCIV', 'MMMCMXCV', 'MMMCMXCVI', 'MMMCMXCVII', 'MMMCMXCVIII', 'MMMCMXCIX', ] -export const mode1 = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI', 'XII', 'XIII', 'XIV', 'XV', 'XVI', 'XVII', 'XVIII', 'XIX', 'XX', 'XXI', 'XXII', 'XXIII', 'XXIV', 'XXV', 'XXVI', 'XXVII', 'XXVIII', 'XXIX', 'XXX', 'XXXI', 'XXXII', 'XXXIII', 'XXXIV', 'XXXV', 'XXXVI', 'XXXVII', 'XXXVIII', 'XXXIX', 'XL', 'XLI', 'XLII', 'XLIII', 'XLIV', 'VL', 'VLI', 'VLII', 'VLIII', 'VLIV', 'L', 'LI', 'LII', 'LIII', 'LIV', 'LV', 'LVI', 'LVII', 'LVIII', 'LIX', 'LX', 'LXI', 'LXII', 'LXIII', 'LXIV', 'LXV', 'LXVI', 'LXVII', 'LXVIII', 'LXIX', 'LXX', 'LXXI', 'LXXII', 'LXXIII', 'LXXIV', 'LXXV', 'LXXVI', 'LXXVII', 'LXXVIII', 'LXXIX', 'LXXX', 'LXXXI', 'LXXXII', 'LXXXIII', 'LXXXIV', 'LXXXV', 'LXXXVI', 'LXXXVII', 'LXXXVIII', 'LXXXIX', 'XC', 'XCI', 'XCII', 'XCIII', 'XCIV', 'VC', 'VCI', 'VCII', 'VCIII', 'VCIV', 'C', 'CI', 'CII', 'CIII', 'CIV', 'CV', 'CVI', 'CVII', 'CVIII', 'CIX', 'CX', 'CXI', 'CXII', 'CXIII', 'CXIV', 'CXV', 'CXVI', 'CXVII', 'CXVIII', 'CXIX', 'CXX', 'CXXI', 'CXXII', 'CXXIII', 'CXXIV', 'CXXV', 'CXXVI', 'CXXVII', 'CXXVIII', 'CXXIX', 'CXXX', 'CXXXI', 'CXXXII', 'CXXXIII', 'CXXXIV', 'CXXXV', 'CXXXVI', 'CXXXVII', 'CXXXVIII', 'CXXXIX', 'CXL', 'CXLI', 'CXLII', 'CXLIII', 'CXLIV', 'CVL', 'CVLI', 'CVLII', 'CVLIII', 'CVLIV', 'CL', 'CLI', 'CLII', 'CLIII', 'CLIV', 'CLV', 'CLVI', 'CLVII', 'CLVIII', 'CLIX', 'CLX', 'CLXI', 'CLXII', 'CLXIII', 'CLXIV', 'CLXV', 'CLXVI', 'CLXVII', 'CLXVIII', 'CLXIX', 'CLXX', 'CLXXI', 'CLXXII', 'CLXXIII', 'CLXXIV', 'CLXXV', 'CLXXVI', 'CLXXVII', 'CLXXVIII', 'CLXXIX', 'CLXXX', 'CLXXXI', 'CLXXXII', 'CLXXXIII', 'CLXXXIV', 'CLXXXV', 'CLXXXVI', 'CLXXXVII', 'CLXXXVIII', 'CLXXXIX', 'CXC', 'CXCI', 'CXCII', 'CXCIII', 'CXCIV', 'CVC', 'CVCI', 'CVCII', 'CVCIII', 'CVCIV', 'CC', 'CCI', 'CCII', 'CCIII', 'CCIV', 'CCV', 'CCVI', 'CCVII', 'CCVIII', 'CCIX', 'CCX', 'CCXI', 'CCXII', 'CCXIII', 'CCXIV', 'CCXV', 'CCXVI', 'CCXVII', 'CCXVIII', 'CCXIX', 'CCXX', 'CCXXI', 'CCXXII', 'CCXXIII', 'CCXXIV', 'CCXXV', 'CCXXVI', 'CCXXVII', 'CCXXVIII', 'CCXXIX', 'CCXXX', 'CCXXXI', 'CCXXXII', 'CCXXXIII', 'CCXXXIV', 'CCXXXV', 'CCXXXVI', 'CCXXXVII', 'CCXXXVIII', 'CCXXXIX', 'CCXL', 'CCXLI', 'CCXLII', 'CCXLIII', 'CCXLIV', 'CCVL', 'CCVLI', 'CCVLII', 'CCVLIII', 'CCVLIV', 'CCL', 'CCLI', 'CCLII', 'CCLIII', 'CCLIV', 'CCLV', 'CCLVI', 'CCLVII', 'CCLVIII', 'CCLIX', 'CCLX', 'CCLXI', 'CCLXII', 'CCLXIII', 'CCLXIV', 'CCLXV', 'CCLXVI', 'CCLXVII', 'CCLXVIII', 'CCLXIX', 'CCLXX', 'CCLXXI', 'CCLXXII', 'CCLXXIII', 'CCLXXIV', 'CCLXXV', 'CCLXXVI', 'CCLXXVII', 'CCLXXVIII', 'CCLXXIX', 'CCLXXX', 'CCLXXXI', 'CCLXXXII', 'CCLXXXIII', 'CCLXXXIV', 'CCLXXXV', 'CCLXXXVI', 'CCLXXXVII', 'CCLXXXVIII', 'CCLXXXIX', 'CCXC', 'CCXCI', 'CCXCII', 'CCXCIII', 'CCXCIV', 'CCVC', 'CCVCI', 'CCVCII', 'CCVCIII', 'CCVCIV', 'CCC', 'CCCI', 'CCCII', 'CCCIII', 'CCCIV', 'CCCV', 'CCCVI', 'CCCVII', 'CCCVIII', 'CCCIX', 'CCCX', 'CCCXI', 'CCCXII', 'CCCXIII', 'CCCXIV', 'CCCXV', 'CCCXVI', 'CCCXVII', 'CCCXVIII', 'CCCXIX', 'CCCXX', 'CCCXXI', 'CCCXXII', 'CCCXXIII', 'CCCXXIV', 'CCCXXV', 'CCCXXVI', 'CCCXXVII', 'CCCXXVIII', 'CCCXXIX', 'CCCXXX', 'CCCXXXI', 'CCCXXXII', 'CCCXXXIII', 'CCCXXXIV', 'CCCXXXV', 'CCCXXXVI', 'CCCXXXVII', 'CCCXXXVIII', 'CCCXXXIX', 'CCCXL', 'CCCXLI', 'CCCXLII', 'CCCXLIII', 'CCCXLIV', 'CCCVL', 'CCCVLI', 'CCCVLII', 'CCCVLIII', 'CCCVLIV', 'CCCL', 'CCCLI', 'CCCLII', 'CCCLIII', 'CCCLIV', 'CCCLV', 'CCCLVI', 'CCCLVII', 'CCCLVIII', 'CCCLIX', 'CCCLX', 'CCCLXI', 'CCCLXII', 'CCCLXIII', 'CCCLXIV', 'CCCLXV', 'CCCLXVI', 'CCCLXVII', 'CCCLXVIII', 'CCCLXIX', 'CCCLXX', 'CCCLXXI', 'CCCLXXII', 'CCCLXXIII', 'CCCLXXIV', 'CCCLXXV', 'CCCLXXVI', 'CCCLXXVII', 'CCCLXXVIII', 'CCCLXXIX', 'CCCLXXX', 'CCCLXXXI', 'CCCLXXXII', 'CCCLXXXIII', 'CCCLXXXIV', 'CCCLXXXV', 'CCCLXXXVI', 'CCCLXXXVII', 'CCCLXXXVIII', 'CCCLXXXIX', 'CCCXC', 'CCCXCI', 'CCCXCII', 'CCCXCIII', 'CCCXCIV', 'CCCVC', 'CCCVCI', 'CCCVCII', 'CCCVCIII', 'CCCVCIV', 'CD', 'CDI', 'CDII', 'CDIII', 'CDIV', 'CDV', 'CDVI', 'CDVII', 'CDVIII', 'CDIX', 'CDX', 'CDXI', 'CDXII', 'CDXIII', 'CDXIV', 'CDXV', 'CDXVI', 'CDXVII', 'CDXVIII', 'CDXIX', 'CDXX', 'CDXXI', 'CDXXII', 'CDXXIII', 'CDXXIV', 'CDXXV', 'CDXXVI', 'CDXXVII', 'CDXXVIII', 'CDXXIX', 'CDXXX', 'CDXXXI', 'CDXXXII', 'CDXXXIII', 'CDXXXIV', 'CDXXXV', 'CDXXXVI', 'CDXXXVII', 'CDXXXVIII', 'CDXXXIX', 'CDXL', 'CDXLI', 'CDXLII', 'CDXLIII', 'CDXLIV', 'CDVL', 'CDVLI', 'CDVLII', 'CDVLIII', 'CDVLIV', 'LD', 'LDI', 'LDII', 'LDIII', 'LDIV', 'LDV', 'LDVI', 'LDVII', 'LDVIII', 'LDIX', 'LDX', 'LDXI', 'LDXII', 'LDXIII', 'LDXIV', 'LDXV', 'LDXVI', 'LDXVII', 'LDXVIII', 'LDXIX', 'LDXX', 'LDXXI', 'LDXXII', 'LDXXIII', 'LDXXIV', 'LDXXV', 'LDXXVI', 'LDXXVII', 'LDXXVIII', 'LDXXIX', 'LDXXX', 'LDXXXI', 'LDXXXII', 'LDXXXIII', 'LDXXXIV', 'LDXXXV', 'LDXXXVI', 'LDXXXVII', 'LDXXXVIII', 'LDXXXIX', 'LDXL', 'LDXLI', 'LDXLII', 'LDXLIII', 'LDXLIV', 'LDVL', 'LDVLI', 'LDVLII', 'LDVLIII', 'LDVLIV', 'D', 'DI', 'DII', 'DIII', 'DIV', 'DV', 'DVI', 'DVII', 'DVIII', 'DIX', 'DX', 'DXI', 'DXII', 'DXIII', 'DXIV', 'DXV', 'DXVI', 'DXVII', 'DXVIII', 'DXIX', 'DXX', 'DXXI', 'DXXII', 'DXXIII', 'DXXIV', 'DXXV', 'DXXVI', 'DXXVII', 'DXXVIII', 'DXXIX', 'DXXX', 'DXXXI', 'DXXXII', 'DXXXIII', 'DXXXIV', 'DXXXV', 'DXXXVI', 'DXXXVII', 'DXXXVIII', 'DXXXIX', 'DXL', 'DXLI', 'DXLII', 'DXLIII', 'DXLIV', 'DVL', 'DVLI', 'DVLII', 'DVLIII', 'DVLIV', 'DL', 'DLI', 'DLII', 'DLIII', 'DLIV', 'DLV', 'DLVI', 'DLVII', 'DLVIII', 'DLIX', 'DLX', 'DLXI', 'DLXII', 'DLXIII', 'DLXIV', 'DLXV', 'DLXVI', 'DLXVII', 'DLXVIII', 'DLXIX', 'DLXX', 'DLXXI', 'DLXXII', 'DLXXIII', 'DLXXIV', 'DLXXV', 'DLXXVI', 'DLXXVII', 'DLXXVIII', 'DLXXIX', 'DLXXX', 'DLXXXI', 'DLXXXII', 'DLXXXIII', 'DLXXXIV', 'DLXXXV', 'DLXXXVI', 'DLXXXVII', 'DLXXXVIII', 'DLXXXIX', 'DXC', 'DXCI', 'DXCII', 'DXCIII', 'DXCIV', 'DVC', 'DVCI', 'DVCII', 'DVCIII', 'DVCIV', 'DC', 'DCI', 'DCII', 'DCIII', 'DCIV', 'DCV', 'DCVI', 'DCVII', 'DCVIII', 'DCIX', 'DCX', 'DCXI', 'DCXII', 'DCXIII', 'DCXIV', 'DCXV', 'DCXVI', 'DCXVII', 'DCXVIII', 'DCXIX', 'DCXX', 'DCXXI', 'DCXXII', 'DCXXIII', 'DCXXIV', 'DCXXV', 'DCXXVI', 'DCXXVII', 'DCXXVIII', 'DCXXIX', 'DCXXX', 'DCXXXI', 'DCXXXII', 'DCXXXIII', 'DCXXXIV', 'DCXXXV', 'DCXXXVI', 'DCXXXVII', 'DCXXXVIII', 'DCXXXIX', 'DCXL', 'DCXLI', 'DCXLII', 'DCXLIII', 'DCXLIV', 'DCVL', 'DCVLI', 'DCVLII', 'DCVLIII', 'DCVLIV', 'DCL', 'DCLI', 'DCLII', 'DCLIII', 'DCLIV', 'DCLV', 'DCLVI', 'DCLVII', 'DCLVIII', 'DCLIX', 'DCLX', 'DCLXI', 'DCLXII', 'DCLXIII', 'DCLXIV', 'DCLXV', 'DCLXVI', 'DCLXVII', 'DCLXVIII', 'DCLXIX', 'DCLXX', 'DCLXXI', 'DCLXXII', 'DCLXXIII', 'DCLXXIV', 'DCLXXV', 'DCLXXVI', 'DCLXXVII', 'DCLXXVIII', 'DCLXXIX', 'DCLXXX', 'DCLXXXI', 'DCLXXXII', 'DCLXXXIII', 'DCLXXXIV', 'DCLXXXV', 'DCLXXXVI', 'DCLXXXVII', 'DCLXXXVIII', 'DCLXXXIX', 'DCXC', 'DCXCI', 'DCXCII', 'DCXCIII', 'DCXCIV', 'DCVC', 'DCVCI', 'DCVCII', 'DCVCIII', 'DCVCIV', 'DCC', 'DCCI', 'DCCII', 'DCCIII', 'DCCIV', 'DCCV', 'DCCVI', 'DCCVII', 'DCCVIII', 'DCCIX', 'DCCX', 'DCCXI', 'DCCXII', 'DCCXIII', 'DCCXIV', 'DCCXV', 'DCCXVI', 'DCCXVII', 'DCCXVIII', 'DCCXIX', 'DCCXX', 'DCCXXI', 'DCCXXII', 'DCCXXIII', 'DCCXXIV', 'DCCXXV', 'DCCXXVI', 'DCCXXVII', 'DCCXXVIII', 'DCCXXIX', 'DCCXXX', 'DCCXXXI', 'DCCXXXII', 'DCCXXXIII', 'DCCXXXIV', 'DCCXXXV', 'DCCXXXVI', 'DCCXXXVII', 'DCCXXXVIII', 'DCCXXXIX', 'DCCXL', 'DCCXLI', 'DCCXLII', 'DCCXLIII', 'DCCXLIV', 'DCCVL', 'DCCVLI', 'DCCVLII', 'DCCVLIII', 'DCCVLIV', 'DCCL', 'DCCLI', 'DCCLII', 'DCCLIII', 'DCCLIV', 'DCCLV', 'DCCLVI', 'DCCLVII', 'DCCLVIII', 'DCCLIX', 'DCCLX', 'DCCLXI', 'DCCLXII', 'DCCLXIII', 'DCCLXIV', 'DCCLXV', 'DCCLXVI', 'DCCLXVII', 'DCCLXVIII', 'DCCLXIX', 'DCCLXX', 'DCCLXXI', 'DCCLXXII', 'DCCLXXIII', 'DCCLXXIV', 'DCCLXXV', 'DCCLXXVI', 'DCCLXXVII', 'DCCLXXVIII', 'DCCLXXIX', 'DCCLXXX', 'DCCLXXXI', 'DCCLXXXII', 'DCCLXXXIII', 'DCCLXXXIV', 'DCCLXXXV', 'DCCLXXXVI', 'DCCLXXXVII', 'DCCLXXXVIII', 'DCCLXXXIX', 'DCCXC', 'DCCXCI', 'DCCXCII', 'DCCXCIII', 'DCCXCIV', 'DCCVC', 'DCCVCI', 'DCCVCII', 'DCCVCIII', 'DCCVCIV', 'DCCC', 'DCCCI', 'DCCCII', 'DCCCIII', 'DCCCIV', 'DCCCV', 'DCCCVI', 'DCCCVII', 'DCCCVIII', 'DCCCIX', 'DCCCX', 'DCCCXI', 'DCCCXII', 'DCCCXIII', 'DCCCXIV', 'DCCCXV', 'DCCCXVI', 'DCCCXVII', 'DCCCXVIII', 'DCCCXIX', 'DCCCXX', 'DCCCXXI', 'DCCCXXII', 'DCCCXXIII', 'DCCCXXIV', 'DCCCXXV', 'DCCCXXVI', 'DCCCXXVII', 'DCCCXXVIII', 'DCCCXXIX', 'DCCCXXX', 'DCCCXXXI', 'DCCCXXXII', 'DCCCXXXIII', 'DCCCXXXIV', 'DCCCXXXV', 'DCCCXXXVI', 'DCCCXXXVII', 'DCCCXXXVIII', 'DCCCXXXIX', 'DCCCXL', 'DCCCXLI', 'DCCCXLII', 'DCCCXLIII', 'DCCCXLIV', 'DCCCVL', 'DCCCVLI', 'DCCCVLII', 'DCCCVLIII', 'DCCCVLIV', 'DCCCL', 'DCCCLI', 'DCCCLII', 'DCCCLIII', 'DCCCLIV', 'DCCCLV', 'DCCCLVI', 'DCCCLVII', 'DCCCLVIII', 'DCCCLIX', 'DCCCLX', 'DCCCLXI', 'DCCCLXII', 'DCCCLXIII', 'DCCCLXIV', 'DCCCLXV', 'DCCCLXVI', 'DCCCLXVII', 'DCCCLXVIII', 'DCCCLXIX', 'DCCCLXX', 'DCCCLXXI', 'DCCCLXXII', 'DCCCLXXIII', 'DCCCLXXIV', 'DCCCLXXV', 'DCCCLXXVI', 'DCCCLXXVII', 'DCCCLXXVIII', 'DCCCLXXIX', 'DCCCLXXX', 'DCCCLXXXI', 'DCCCLXXXII', 'DCCCLXXXIII', 'DCCCLXXXIV', 'DCCCLXXXV', 'DCCCLXXXVI', 'DCCCLXXXVII', 'DCCCLXXXVIII', 'DCCCLXXXIX', 'DCCCXC', 'DCCCXCI', 'DCCCXCII', 'DCCCXCIII', 'DCCCXCIV', 'DCCCVC', 'DCCCVCI', 'DCCCVCII', 'DCCCVCIII', 'DCCCVCIV', 'CM', 'CMI', 'CMII', 'CMIII', 'CMIV', 'CMV', 'CMVI', 'CMVII', 'CMVIII', 'CMIX', 'CMX', 'CMXI', 'CMXII', 'CMXIII', 'CMXIV', 'CMXV', 'CMXVI', 'CMXVII', 'CMXVIII', 'CMXIX', 'CMXX', 'CMXXI', 'CMXXII', 'CMXXIII', 'CMXXIV', 'CMXXV', 'CMXXVI', 'CMXXVII', 'CMXXVIII', 'CMXXIX', 'CMXXX', 'CMXXXI', 'CMXXXII', 'CMXXXIII', 'CMXXXIV', 'CMXXXV', 'CMXXXVI', 'CMXXXVII', 'CMXXXVIII', 'CMXXXIX', 'CMXL', 'CMXLI', 'CMXLII', 'CMXLIII', 'CMXLIV', 'CMVL', 'CMVLI', 'CMVLII', 'CMVLIII', 'CMVLIV', 'LM', 'LMI', 'LMII', 'LMIII', 'LMIV', 'LMV', 'LMVI', 'LMVII', 'LMVIII', 'LMIX', 'LMX', 'LMXI', 'LMXII', 'LMXIII', 'LMXIV', 'LMXV', 'LMXVI', 'LMXVII', 'LMXVIII', 'LMXIX', 'LMXX', 'LMXXI', 'LMXXII', 'LMXXIII', 'LMXXIV', 'LMXXV', 'LMXXVI', 'LMXXVII', 'LMXXVIII', 'LMXXIX', 'LMXXX', 'LMXXXI', 'LMXXXII', 'LMXXXIII', 'LMXXXIV', 'LMXXXV', 'LMXXXVI', 'LMXXXVII', 'LMXXXVIII', 'LMXXXIX', 'LMXL', 'LMXLI', 'LMXLII', 'LMXLIII', 'LMXLIV', 'LMVL', 'LMVLI', 'LMVLII', 'LMVLIII', 'LMVLIV', 'M', 'MI', 'MII', 'MIII', 'MIV', 'MV', 'MVI', 'MVII', 'MVIII', 'MIX', 'MX', 'MXI', 'MXII', 'MXIII', 'MXIV', 'MXV', 'MXVI', 'MXVII', 'MXVIII', 'MXIX', 'MXX', 'MXXI', 'MXXII', 'MXXIII', 'MXXIV', 'MXXV', 'MXXVI', 'MXXVII', 'MXXVIII', 'MXXIX', 'MXXX', 'MXXXI', 'MXXXII', 'MXXXIII', 'MXXXIV', 'MXXXV', 'MXXXVI', 'MXXXVII', 'MXXXVIII', 'MXXXIX', 'MXL', 'MXLI', 'MXLII', 'MXLIII', 'MXLIV', 'MVL', 'MVLI', 'MVLII', 'MVLIII', 'MVLIV', 'ML', 'MLI', 'MLII', 'MLIII', 'MLIV', 'MLV', 'MLVI', 'MLVII', 'MLVIII', 'MLIX', 'MLX', 'MLXI', 'MLXII', 'MLXIII', 'MLXIV', 'MLXV', 'MLXVI', 'MLXVII', 'MLXVIII', 'MLXIX', 'MLXX', 'MLXXI', 'MLXXII', 'MLXXIII', 'MLXXIV', 'MLXXV', 'MLXXVI', 'MLXXVII', 'MLXXVIII', 'MLXXIX', 'MLXXX', 'MLXXXI', 'MLXXXII', 'MLXXXIII', 'MLXXXIV', 'MLXXXV', 'MLXXXVI', 'MLXXXVII', 'MLXXXVIII', 'MLXXXIX', 'MXC', 'MXCI', 'MXCII', 'MXCIII', 'MXCIV', 'MVC', 'MVCI', 'MVCII', 'MVCIII', 'MVCIV', 'MC', 'MCI', 'MCII', 'MCIII', 'MCIV', 'MCV', 'MCVI', 'MCVII', 'MCVIII', 'MCIX', 'MCX', 'MCXI', 'MCXII', 'MCXIII', 'MCXIV', 'MCXV', 'MCXVI', 'MCXVII', 'MCXVIII', 'MCXIX', 'MCXX', 'MCXXI', 'MCXXII', 'MCXXIII', 'MCXXIV', 'MCXXV', 'MCXXVI', 'MCXXVII', 'MCXXVIII', 'MCXXIX', 'MCXXX', 'MCXXXI', 'MCXXXII', 'MCXXXIII', 'MCXXXIV', 'MCXXXV', 'MCXXXVI', 'MCXXXVII', 'MCXXXVIII', 'MCXXXIX', 'MCXL', 'MCXLI', 'MCXLII', 'MCXLIII', 'MCXLIV', 'MCVL', 'MCVLI', 'MCVLII', 'MCVLIII', 'MCVLIV', 'MCL', 'MCLI', 'MCLII', 'MCLIII', 'MCLIV', 'MCLV', 'MCLVI', 'MCLVII', 'MCLVIII', 'MCLIX', 'MCLX', 'MCLXI', 'MCLXII', 'MCLXIII', 'MCLXIV', 'MCLXV', 'MCLXVI', 'MCLXVII', 'MCLXVIII', 'MCLXIX', 'MCLXX', 'MCLXXI', 'MCLXXII', 'MCLXXIII', 'MCLXXIV', 'MCLXXV', 'MCLXXVI', 'MCLXXVII', 'MCLXXVIII', 'MCLXXIX', 'MCLXXX', 'MCLXXXI', 'MCLXXXII', 'MCLXXXIII', 'MCLXXXIV', 'MCLXXXV', 'MCLXXXVI', 'MCLXXXVII', 'MCLXXXVIII', 'MCLXXXIX', 'MCXC', 'MCXCI', 'MCXCII', 'MCXCIII', 'MCXCIV', 'MCVC', 'MCVCI', 'MCVCII', 'MCVCIII', 'MCVCIV', 'MCC', 'MCCI', 'MCCII', 'MCCIII', 'MCCIV', 'MCCV', 'MCCVI', 'MCCVII', 'MCCVIII', 'MCCIX', 'MCCX', 'MCCXI', 'MCCXII', 'MCCXIII', 'MCCXIV', 'MCCXV', 'MCCXVI', 'MCCXVII', 'MCCXVIII', 'MCCXIX', 'MCCXX', 'MCCXXI', 'MCCXXII', 'MCCXXIII', 'MCCXXIV', 'MCCXXV', 'MCCXXVI', 'MCCXXVII', 'MCCXXVIII', 'MCCXXIX', 'MCCXXX', 'MCCXXXI', 'MCCXXXII', 'MCCXXXIII', 'MCCXXXIV', 'MCCXXXV', 'MCCXXXVI', 'MCCXXXVII', 'MCCXXXVIII', 'MCCXXXIX', 'MCCXL', 'MCCXLI', 'MCCXLII', 'MCCXLIII', 'MCCXLIV', 'MCCVL', 'MCCVLI', 'MCCVLII', 'MCCVLIII', 'MCCVLIV', 'MCCL', 'MCCLI', 'MCCLII', 'MCCLIII', 'MCCLIV', 'MCCLV', 'MCCLVI', 'MCCLVII', 'MCCLVIII', 'MCCLIX', 'MCCLX', 'MCCLXI', 'MCCLXII', 'MCCLXIII', 'MCCLXIV', 'MCCLXV', 'MCCLXVI', 'MCCLXVII', 'MCCLXVIII', 'MCCLXIX', 'MCCLXX', 'MCCLXXI', 'MCCLXXII', 'MCCLXXIII', 'MCCLXXIV', 'MCCLXXV', 'MCCLXXVI', 'MCCLXXVII', 'MCCLXXVIII', 'MCCLXXIX', 'MCCLXXX', 'MCCLXXXI', 'MCCLXXXII', 'MCCLXXXIII', 'MCCLXXXIV', 'MCCLXXXV', 'MCCLXXXVI', 'MCCLXXXVII', 'MCCLXXXVIII', 'MCCLXXXIX', 'MCCXC', 'MCCXCI', 'MCCXCII', 'MCCXCIII', 'MCCXCIV', 'MCCVC', 'MCCVCI', 'MCCVCII', 'MCCVCIII', 'MCCVCIV', 'MCCC', 'MCCCI', 'MCCCII', 'MCCCIII', 'MCCCIV', 'MCCCV', 'MCCCVI', 'MCCCVII', 'MCCCVIII', 'MCCCIX', 'MCCCX', 'MCCCXI', 'MCCCXII', 'MCCCXIII', 'MCCCXIV', 'MCCCXV', 'MCCCXVI', 'MCCCXVII', 'MCCCXVIII', 'MCCCXIX', 'MCCCXX', 'MCCCXXI', 'MCCCXXII', 'MCCCXXIII', 'MCCCXXIV', 'MCCCXXV', 'MCCCXXVI', 'MCCCXXVII', 'MCCCXXVIII', 'MCCCXXIX', 'MCCCXXX', 'MCCCXXXI', 'MCCCXXXII', 'MCCCXXXIII', 'MCCCXXXIV', 'MCCCXXXV', 'MCCCXXXVI', 'MCCCXXXVII', 'MCCCXXXVIII', 'MCCCXXXIX', 'MCCCXL', 'MCCCXLI', 'MCCCXLII', 'MCCCXLIII', 'MCCCXLIV', 'MCCCVL', 'MCCCVLI', 'MCCCVLII', 'MCCCVLIII', 'MCCCVLIV', 'MCCCL', 'MCCCLI', 'MCCCLII', 'MCCCLIII', 'MCCCLIV', 'MCCCLV', 'MCCCLVI', 'MCCCLVII', 'MCCCLVIII', 'MCCCLIX', 'MCCCLX', 'MCCCLXI', 'MCCCLXII', 'MCCCLXIII', 'MCCCLXIV', 'MCCCLXV', 'MCCCLXVI', 'MCCCLXVII', 'MCCCLXVIII', 'MCCCLXIX', 'MCCCLXX', 'MCCCLXXI', 'MCCCLXXII', 'MCCCLXXIII', 'MCCCLXXIV', 'MCCCLXXV', 'MCCCLXXVI', 'MCCCLXXVII', 'MCCCLXXVIII', 'MCCCLXXIX', 'MCCCLXXX', 'MCCCLXXXI', 'MCCCLXXXII', 'MCCCLXXXIII', 'MCCCLXXXIV', 'MCCCLXXXV', 'MCCCLXXXVI', 'MCCCLXXXVII', 'MCCCLXXXVIII', 'MCCCLXXXIX', 'MCCCXC', 'MCCCXCI', 'MCCCXCII', 'MCCCXCIII', 'MCCCXCIV', 'MCCCVC', 'MCCCVCI', 'MCCCVCII', 'MCCCVCIII', 'MCCCVCIV', 'MCD', 'MCDI', 'MCDII', 'MCDIII', 'MCDIV', 'MCDV', 'MCDVI', 'MCDVII', 'MCDVIII', 'MCDIX', 'MCDX', 'MCDXI', 'MCDXII', 'MCDXIII', 'MCDXIV', 'MCDXV', 'MCDXVI', 'MCDXVII', 'MCDXVIII', 'MCDXIX', 'MCDXX', 'MCDXXI', 'MCDXXII', 'MCDXXIII', 'MCDXXIV', 'MCDXXV', 'MCDXXVI', 'MCDXXVII', 'MCDXXVIII', 'MCDXXIX', 'MCDXXX', 'MCDXXXI', 'MCDXXXII', 'MCDXXXIII', 'MCDXXXIV', 'MCDXXXV', 'MCDXXXVI', 'MCDXXXVII', 'MCDXXXVIII', 'MCDXXXIX', 'MCDXL', 'MCDXLI', 'MCDXLII', 'MCDXLIII', 'MCDXLIV', 'MCDVL', 'MCDVLI', 'MCDVLII', 'MCDVLIII', 'MCDVLIV', 'MLD', 'MLDI', 'MLDII', 'MLDIII', 'MLDIV', 'MLDV', 'MLDVI', 'MLDVII', 'MLDVIII', 'MLDIX', 'MLDX', 'MLDXI', 'MLDXII', 'MLDXIII', 'MLDXIV', 'MLDXV', 'MLDXVI', 'MLDXVII', 'MLDXVIII', 'MLDXIX', 'MLDXX', 'MLDXXI', 'MLDXXII', 'MLDXXIII', 'MLDXXIV', 'MLDXXV', 'MLDXXVI', 'MLDXXVII', 'MLDXXVIII', 'MLDXXIX', 'MLDXXX', 'MLDXXXI', 'MLDXXXII', 'MLDXXXIII', 'MLDXXXIV', 'MLDXXXV', 'MLDXXXVI', 'MLDXXXVII', 'MLDXXXVIII', 'MLDXXXIX', 'MLDXL', 'MLDXLI', 'MLDXLII', 'MLDXLIII', 'MLDXLIV', 'MLDVL', 'MLDVLI', 'MLDVLII', 'MLDVLIII', 'MLDVLIV', 'MD', 'MDI', 'MDII', 'MDIII', 'MDIV', 'MDV', 'MDVI', 'MDVII', 'MDVIII', 'MDIX', 'MDX', 'MDXI', 'MDXII', 'MDXIII', 'MDXIV', 'MDXV', 'MDXVI', 'MDXVII', 'MDXVIII', 'MDXIX', 'MDXX', 'MDXXI', 'MDXXII', 'MDXXIII', 'MDXXIV', 'MDXXV', 'MDXXVI', 'MDXXVII', 'MDXXVIII', 'MDXXIX', 'MDXXX', 'MDXXXI', 'MDXXXII', 'MDXXXIII', 'MDXXXIV', 'MDXXXV', 'MDXXXVI', 'MDXXXVII', 'MDXXXVIII', 'MDXXXIX', 'MDXL', 'MDXLI', 'MDXLII', 'MDXLIII', 'MDXLIV', 'MDVL', 'MDVLI', 'MDVLII', 'MDVLIII', 'MDVLIV', 'MDL', 'MDLI', 'MDLII', 'MDLIII', 'MDLIV', 'MDLV', 'MDLVI', 'MDLVII', 'MDLVIII', 'MDLIX', 'MDLX', 'MDLXI', 'MDLXII', 'MDLXIII', 'MDLXIV', 'MDLXV', 'MDLXVI', 'MDLXVII', 'MDLXVIII', 'MDLXIX', 'MDLXX', 'MDLXXI', 'MDLXXII', 'MDLXXIII', 'MDLXXIV', 'MDLXXV', 'MDLXXVI', 'MDLXXVII', 'MDLXXVIII', 'MDLXXIX', 'MDLXXX', 'MDLXXXI', 'MDLXXXII', 'MDLXXXIII', 'MDLXXXIV', 'MDLXXXV', 'MDLXXXVI', 'MDLXXXVII', 'MDLXXXVIII', 'MDLXXXIX', 'MDXC', 'MDXCI', 'MDXCII', 'MDXCIII', 'MDXCIV', 'MDVC', 'MDVCI', 'MDVCII', 'MDVCIII', 'MDVCIV', 'MDC', 'MDCI', 'MDCII', 'MDCIII', 'MDCIV', 'MDCV', 'MDCVI', 'MDCVII', 'MDCVIII', 'MDCIX', 'MDCX', 'MDCXI', 'MDCXII', 'MDCXIII', 'MDCXIV', 'MDCXV', 'MDCXVI', 'MDCXVII', 'MDCXVIII', 'MDCXIX', 'MDCXX', 'MDCXXI', 'MDCXXII', 'MDCXXIII', 'MDCXXIV', 'MDCXXV', 'MDCXXVI', 'MDCXXVII', 'MDCXXVIII', 'MDCXXIX', 'MDCXXX', 'MDCXXXI', 'MDCXXXII', 'MDCXXXIII', 'MDCXXXIV', 'MDCXXXV', 'MDCXXXVI', 'MDCXXXVII', 'MDCXXXVIII', 'MDCXXXIX', 'MDCXL', 'MDCXLI', 'MDCXLII', 'MDCXLIII', 'MDCXLIV', 'MDCVL', 'MDCVLI', 'MDCVLII', 'MDCVLIII', 'MDCVLIV', 'MDCL', 'MDCLI', 'MDCLII', 'MDCLIII', 'MDCLIV', 'MDCLV', 'MDCLVI', 'MDCLVII', 'MDCLVIII', 'MDCLIX', 'MDCLX', 'MDCLXI', 'MDCLXII', 'MDCLXIII', 'MDCLXIV', 'MDCLXV', 'MDCLXVI', 'MDCLXVII', 'MDCLXVIII', 'MDCLXIX', 'MDCLXX', 'MDCLXXI', 'MDCLXXII', 'MDCLXXIII', 'MDCLXXIV', 'MDCLXXV', 'MDCLXXVI', 'MDCLXXVII', 'MDCLXXVIII', 'MDCLXXIX', 'MDCLXXX', 'MDCLXXXI', 'MDCLXXXII', 'MDCLXXXIII', 'MDCLXXXIV', 'MDCLXXXV', 'MDCLXXXVI', 'MDCLXXXVII', 'MDCLXXXVIII', 'MDCLXXXIX', 'MDCXC', 'MDCXCI', 'MDCXCII', 'MDCXCIII', 'MDCXCIV', 'MDCVC', 'MDCVCI', 'MDCVCII', 'MDCVCIII', 'MDCVCIV', 'MDCC', 'MDCCI', 'MDCCII', 'MDCCIII', 'MDCCIV', 'MDCCV', 'MDCCVI', 'MDCCVII', 'MDCCVIII', 'MDCCIX', 'MDCCX', 'MDCCXI', 'MDCCXII', 'MDCCXIII', 'MDCCXIV', 'MDCCXV', 'MDCCXVI', 'MDCCXVII', 'MDCCXVIII', 'MDCCXIX', 'MDCCXX', 'MDCCXXI', 'MDCCXXII', 'MDCCXXIII', 'MDCCXXIV', 'MDCCXXV', 'MDCCXXVI', 'MDCCXXVII', 'MDCCXXVIII', 'MDCCXXIX', 'MDCCXXX', 'MDCCXXXI', 'MDCCXXXII', 'MDCCXXXIII', 'MDCCXXXIV', 'MDCCXXXV', 'MDCCXXXVI', 'MDCCXXXVII', 'MDCCXXXVIII', 'MDCCXXXIX', 'MDCCXL', 'MDCCXLI', 'MDCCXLII', 'MDCCXLIII', 'MDCCXLIV', 'MDCCVL', 'MDCCVLI', 'MDCCVLII', 'MDCCVLIII', 'MDCCVLIV', 'MDCCL', 'MDCCLI', 'MDCCLII', 'MDCCLIII', 'MDCCLIV', 'MDCCLV', 'MDCCLVI', 'MDCCLVII', 'MDCCLVIII', 'MDCCLIX', 'MDCCLX', 'MDCCLXI', 'MDCCLXII', 'MDCCLXIII', 'MDCCLXIV', 'MDCCLXV', 'MDCCLXVI', 'MDCCLXVII', 'MDCCLXVIII', 'MDCCLXIX', 'MDCCLXX', 'MDCCLXXI', 'MDCCLXXII', 'MDCCLXXIII', 'MDCCLXXIV', 'MDCCLXXV', 'MDCCLXXVI', 'MDCCLXXVII', 'MDCCLXXVIII', 'MDCCLXXIX', 'MDCCLXXX', 'MDCCLXXXI', 'MDCCLXXXII', 'MDCCLXXXIII', 'MDCCLXXXIV', 'MDCCLXXXV', 'MDCCLXXXVI', 'MDCCLXXXVII', 'MDCCLXXXVIII', 'MDCCLXXXIX', 'MDCCXC', 'MDCCXCI', 'MDCCXCII', 'MDCCXCIII', 'MDCCXCIV', 'MDCCVC', 'MDCCVCI', 'MDCCVCII', 'MDCCVCIII', 'MDCCVCIV', 'MDCCC', 'MDCCCI', 'MDCCCII', 'MDCCCIII', 'MDCCCIV', 'MDCCCV', 'MDCCCVI', 'MDCCCVII', 'MDCCCVIII', 'MDCCCIX', 'MDCCCX', 'MDCCCXI', 'MDCCCXII', 'MDCCCXIII', 'MDCCCXIV', 'MDCCCXV', 'MDCCCXVI', 'MDCCCXVII', 'MDCCCXVIII', 'MDCCCXIX', 'MDCCCXX', 'MDCCCXXI', 'MDCCCXXII', 'MDCCCXXIII', 'MDCCCXXIV', 'MDCCCXXV', 'MDCCCXXVI', 'MDCCCXXVII', 'MDCCCXXVIII', 'MDCCCXXIX', 'MDCCCXXX', 'MDCCCXXXI', 'MDCCCXXXII', 'MDCCCXXXIII', 'MDCCCXXXIV', 'MDCCCXXXV', 'MDCCCXXXVI', 'MDCCCXXXVII', 'MDCCCXXXVIII', 'MDCCCXXXIX', 'MDCCCXL', 'MDCCCXLI', 'MDCCCXLII', 'MDCCCXLIII', 'MDCCCXLIV', 'MDCCCVL', 'MDCCCVLI', 'MDCCCVLII', 'MDCCCVLIII', 'MDCCCVLIV', 'MDCCCL', 'MDCCCLI', 'MDCCCLII', 'MDCCCLIII', 'MDCCCLIV', 'MDCCCLV', 'MDCCCLVI', 'MDCCCLVII', 'MDCCCLVIII', 'MDCCCLIX', 'MDCCCLX', 'MDCCCLXI', 'MDCCCLXII', 'MDCCCLXIII', 'MDCCCLXIV', 'MDCCCLXV', 'MDCCCLXVI', 'MDCCCLXVII', 'MDCCCLXVIII', 'MDCCCLXIX', 'MDCCCLXX', 'MDCCCLXXI', 'MDCCCLXXII', 'MDCCCLXXIII', 'MDCCCLXXIV', 'MDCCCLXXV', 'MDCCCLXXVI', 'MDCCCLXXVII', 'MDCCCLXXVIII', 'MDCCCLXXIX', 'MDCCCLXXX', 'MDCCCLXXXI', 'MDCCCLXXXII', 'MDCCCLXXXIII', 'MDCCCLXXXIV', 'MDCCCLXXXV', 'MDCCCLXXXVI', 'MDCCCLXXXVII', 'MDCCCLXXXVIII', 'MDCCCLXXXIX', 'MDCCCXC', 'MDCCCXCI', 'MDCCCXCII', 'MDCCCXCIII', 'MDCCCXCIV', 'MDCCCVC', 'MDCCCVCI', 'MDCCCVCII', 'MDCCCVCIII', 'MDCCCVCIV', 'MCM', 'MCMI', 'MCMII', 'MCMIII', 'MCMIV', 'MCMV', 'MCMVI', 'MCMVII', 'MCMVIII', 'MCMIX', 'MCMX', 'MCMXI', 'MCMXII', 'MCMXIII', 'MCMXIV', 'MCMXV', 'MCMXVI', 'MCMXVII', 'MCMXVIII', 'MCMXIX', 'MCMXX', 'MCMXXI', 'MCMXXII', 'MCMXXIII', 'MCMXXIV', 'MCMXXV', 'MCMXXVI', 'MCMXXVII', 'MCMXXVIII', 'MCMXXIX', 'MCMXXX', 'MCMXXXI', 'MCMXXXII', 'MCMXXXIII', 'MCMXXXIV', 'MCMXXXV', 'MCMXXXVI', 'MCMXXXVII', 'MCMXXXVIII', 'MCMXXXIX', 'MCMXL', 'MCMXLI', 'MCMXLII', 'MCMXLIII', 'MCMXLIV', 'MCMVL', 'MCMVLI', 'MCMVLII', 'MCMVLIII', 'MCMVLIV', 'MLM', 'MLMI', 'MLMII', 'MLMIII', 'MLMIV', 'MLMV', 'MLMVI', 'MLMVII', 'MLMVIII', 'MLMIX', 'MLMX', 'MLMXI', 'MLMXII', 'MLMXIII', 'MLMXIV', 'MLMXV', 'MLMXVI', 'MLMXVII', 'MLMXVIII', 'MLMXIX', 'MLMXX', 'MLMXXI', 'MLMXXII', 'MLMXXIII', 'MLMXXIV', 'MLMXXV', 'MLMXXVI', 'MLMXXVII', 'MLMXXVIII', 'MLMXXIX', 'MLMXXX', 'MLMXXXI', 'MLMXXXII', 'MLMXXXIII', 'MLMXXXIV', 'MLMXXXV', 'MLMXXXVI', 'MLMXXXVII', 'MLMXXXVIII', 'MLMXXXIX', 'MLMXL', 'MLMXLI', 'MLMXLII', 'MLMXLIII', 'MLMXLIV', 'MLMVL', 'MLMVLI', 'MLMVLII', 'MLMVLIII', 'MLMVLIV', 'MM', 'MMI', 'MMII', 'MMIII', 'MMIV', 'MMV', 'MMVI', 'MMVII', 'MMVIII', 'MMIX', 'MMX', 'MMXI', 'MMXII', 'MMXIII', 'MMXIV', 'MMXV', 'MMXVI', 'MMXVII', 'MMXVIII', 'MMXIX', 'MMXX', 'MMXXI', 'MMXXII', 'MMXXIII', 'MMXXIV', 'MMXXV', 'MMXXVI', 'MMXXVII', 'MMXXVIII', 'MMXXIX', 'MMXXX', 'MMXXXI', 'MMXXXII', 'MMXXXIII', 'MMXXXIV', 'MMXXXV', 'MMXXXVI', 'MMXXXVII', 'MMXXXVIII', 'MMXXXIX', 'MMXL', 'MMXLI', 'MMXLII', 'MMXLIII', 'MMXLIV', 'MMVL', 'MMVLI', 'MMVLII', 'MMVLIII', 'MMVLIV', 'MML', 'MMLI', 'MMLII', 'MMLIII', 'MMLIV', 'MMLV', 'MMLVI', 'MMLVII', 'MMLVIII', 'MMLIX', 'MMLX', 'MMLXI', 'MMLXII', 'MMLXIII', 'MMLXIV', 'MMLXV', 'MMLXVI', 'MMLXVII', 'MMLXVIII', 'MMLXIX', 'MMLXX', 'MMLXXI', 'MMLXXII', 'MMLXXIII', 'MMLXXIV', 'MMLXXV', 'MMLXXVI', 'MMLXXVII', 'MMLXXVIII', 'MMLXXIX', 'MMLXXX', 'MMLXXXI', 'MMLXXXII', 'MMLXXXIII', 'MMLXXXIV', 'MMLXXXV', 'MMLXXXVI', 'MMLXXXVII', 'MMLXXXVIII', 'MMLXXXIX', 'MMXC', 'MMXCI', 'MMXCII', 'MMXCIII', 'MMXCIV', 'MMVC', 'MMVCI', 'MMVCII', 'MMVCIII', 'MMVCIV', 'MMC', 'MMCI', 'MMCII', 'MMCIII', 'MMCIV', 'MMCV', 'MMCVI', 'MMCVII', 'MMCVIII', 'MMCIX', 'MMCX', 'MMCXI', 'MMCXII', 'MMCXIII', 'MMCXIV', 'MMCXV', 'MMCXVI', 'MMCXVII', 'MMCXVIII', 'MMCXIX', 'MMCXX', 'MMCXXI', 'MMCXXII', 'MMCXXIII', 'MMCXXIV', 'MMCXXV', 'MMCXXVI', 'MMCXXVII', 'MMCXXVIII', 'MMCXXIX', 'MMCXXX', 'MMCXXXI', 'MMCXXXII', 'MMCXXXIII', 'MMCXXXIV', 'MMCXXXV', 'MMCXXXVI', 'MMCXXXVII', 'MMCXXXVIII', 'MMCXXXIX', 'MMCXL', 'MMCXLI', 'MMCXLII', 'MMCXLIII', 'MMCXLIV', 'MMCVL', 'MMCVLI', 'MMCVLII', 'MMCVLIII', 'MMCVLIV', 'MMCL', 'MMCLI', 'MMCLII', 'MMCLIII', 'MMCLIV', 'MMCLV', 'MMCLVI', 'MMCLVII', 'MMCLVIII', 'MMCLIX', 'MMCLX', 'MMCLXI', 'MMCLXII', 'MMCLXIII', 'MMCLXIV', 'MMCLXV', 'MMCLXVI', 'MMCLXVII', 'MMCLXVIII', 'MMCLXIX', 'MMCLXX', 'MMCLXXI', 'MMCLXXII', 'MMCLXXIII', 'MMCLXXIV', 'MMCLXXV', 'MMCLXXVI', 'MMCLXXVII', 'MMCLXXVIII', 'MMCLXXIX', 'MMCLXXX', 'MMCLXXXI', 'MMCLXXXII', 'MMCLXXXIII', 'MMCLXXXIV', 'MMCLXXXV', 'MMCLXXXVI', 'MMCLXXXVII', 'MMCLXXXVIII', 'MMCLXXXIX', 'MMCXC', 'MMCXCI', 'MMCXCII', 'MMCXCIII', 'MMCXCIV', 'MMCVC', 'MMCVCI', 'MMCVCII', 'MMCVCIII', 'MMCVCIV', 'MMCC', 'MMCCI', 'MMCCII', 'MMCCIII', 'MMCCIV', 'MMCCV', 'MMCCVI', 'MMCCVII', 'MMCCVIII', 'MMCCIX', 'MMCCX', 'MMCCXI', 'MMCCXII', 'MMCCXIII', 'MMCCXIV', 'MMCCXV', 'MMCCXVI', 'MMCCXVII', 'MMCCXVIII', 'MMCCXIX', 'MMCCXX', 'MMCCXXI', 'MMCCXXII', 'MMCCXXIII', 'MMCCXXIV', 'MMCCXXV', 'MMCCXXVI', 'MMCCXXVII', 'MMCCXXVIII', 'MMCCXXIX', 'MMCCXXX', 'MMCCXXXI', 'MMCCXXXII', 'MMCCXXXIII', 'MMCCXXXIV', 'MMCCXXXV', 'MMCCXXXVI', 'MMCCXXXVII', 'MMCCXXXVIII', 'MMCCXXXIX', 'MMCCXL', 'MMCCXLI', 'MMCCXLII', 'MMCCXLIII', 'MMCCXLIV', 'MMCCVL', 'MMCCVLI', 'MMCCVLII', 'MMCCVLIII', 'MMCCVLIV', 'MMCCL', 'MMCCLI', 'MMCCLII', 'MMCCLIII', 'MMCCLIV', 'MMCCLV', 'MMCCLVI', 'MMCCLVII', 'MMCCLVIII', 'MMCCLIX', 'MMCCLX', 'MMCCLXI', 'MMCCLXII', 'MMCCLXIII', 'MMCCLXIV', 'MMCCLXV', 'MMCCLXVI', 'MMCCLXVII', 'MMCCLXVIII', 'MMCCLXIX', 'MMCCLXX', 'MMCCLXXI', 'MMCCLXXII', 'MMCCLXXIII', 'MMCCLXXIV', 'MMCCLXXV', 'MMCCLXXVI', 'MMCCLXXVII', 'MMCCLXXVIII', 'MMCCLXXIX', 'MMCCLXXX', 'MMCCLXXXI', 'MMCCLXXXII', 'MMCCLXXXIII', 'MMCCLXXXIV', 'MMCCLXXXV', 'MMCCLXXXVI', 'MMCCLXXXVII', 'MMCCLXXXVIII', 'MMCCLXXXIX', 'MMCCXC', 'MMCCXCI', 'MMCCXCII', 'MMCCXCIII', 'MMCCXCIV', 'MMCCVC', 'MMCCVCI', 'MMCCVCII', 'MMCCVCIII', 'MMCCVCIV', 'MMCCC', 'MMCCCI', 'MMCCCII', 'MMCCCIII', 'MMCCCIV', 'MMCCCV', 'MMCCCVI', 'MMCCCVII', 'MMCCCVIII', 'MMCCCIX', 'MMCCCX', 'MMCCCXI', 'MMCCCXII', 'MMCCCXIII', 'MMCCCXIV', 'MMCCCXV', 'MMCCCXVI', 'MMCCCXVII', 'MMCCCXVIII', 'MMCCCXIX', 'MMCCCXX', 'MMCCCXXI', 'MMCCCXXII', 'MMCCCXXIII', 'MMCCCXXIV', 'MMCCCXXV', 'MMCCCXXVI', 'MMCCCXXVII', 'MMCCCXXVIII', 'MMCCCXXIX', 'MMCCCXXX', 'MMCCCXXXI', 'MMCCCXXXII', 'MMCCCXXXIII', 'MMCCCXXXIV', 'MMCCCXXXV', 'MMCCCXXXVI', 'MMCCCXXXVII', 'MMCCCXXXVIII', 'MMCCCXXXIX', 'MMCCCXL', 'MMCCCXLI', 'MMCCCXLII', 'MMCCCXLIII', 'MMCCCXLIV', 'MMCCCVL', 'MMCCCVLI', 'MMCCCVLII', 'MMCCCVLIII', 'MMCCCVLIV', 'MMCCCL', 'MMCCCLI', 'MMCCCLII', 'MMCCCLIII', 'MMCCCLIV', 'MMCCCLV', 'MMCCCLVI', 'MMCCCLVII', 'MMCCCLVIII', 'MMCCCLIX', 'MMCCCLX', 'MMCCCLXI', 'MMCCCLXII', 'MMCCCLXIII', 'MMCCCLXIV', 'MMCCCLXV', 'MMCCCLXVI', 'MMCCCLXVII', 'MMCCCLXVIII', 'MMCCCLXIX', 'MMCCCLXX', 'MMCCCLXXI', 'MMCCCLXXII', 'MMCCCLXXIII', 'MMCCCLXXIV', 'MMCCCLXXV', 'MMCCCLXXVI', 'MMCCCLXXVII', 'MMCCCLXXVIII', 'MMCCCLXXIX', 'MMCCCLXXX', 'MMCCCLXXXI', 'MMCCCLXXXII', 'MMCCCLXXXIII', 'MMCCCLXXXIV', 'MMCCCLXXXV', 'MMCCCLXXXVI', 'MMCCCLXXXVII', 'MMCCCLXXXVIII', 'MMCCCLXXXIX', 'MMCCCXC', 'MMCCCXCI', 'MMCCCXCII', 'MMCCCXCIII', 'MMCCCXCIV', 'MMCCCVC', 'MMCCCVCI', 'MMCCCVCII', 'MMCCCVCIII', 'MMCCCVCIV', 'MMCD', 'MMCDI', 'MMCDII', 'MMCDIII', 'MMCDIV', 'MMCDV', 'MMCDVI', 'MMCDVII', 'MMCDVIII', 'MMCDIX', 'MMCDX', 'MMCDXI', 'MMCDXII', 'MMCDXIII', 'MMCDXIV', 'MMCDXV', 'MMCDXVI', 'MMCDXVII', 'MMCDXVIII', 'MMCDXIX', 'MMCDXX', 'MMCDXXI', 'MMCDXXII', 'MMCDXXIII', 'MMCDXXIV', 'MMCDXXV', 'MMCDXXVI', 'MMCDXXVII', 'MMCDXXVIII', 'MMCDXXIX', 'MMCDXXX', 'MMCDXXXI', 'MMCDXXXII', 'MMCDXXXIII', 'MMCDXXXIV', 'MMCDXXXV', 'MMCDXXXVI', 'MMCDXXXVII', 'MMCDXXXVIII', 'MMCDXXXIX', 'MMCDXL', 'MMCDXLI', 'MMCDXLII', 'MMCDXLIII', 'MMCDXLIV', 'MMCDVL', 'MMCDVLI', 'MMCDVLII', 'MMCDVLIII', 'MMCDVLIV', 'MMLD', 'MMLDI', 'MMLDII', 'MMLDIII', 'MMLDIV', 'MMLDV', 'MMLDVI', 'MMLDVII', 'MMLDVIII', 'MMLDIX', 'MMLDX', 'MMLDXI', 'MMLDXII', 'MMLDXIII', 'MMLDXIV', 'MMLDXV', 'MMLDXVI', 'MMLDXVII', 'MMLDXVIII', 'MMLDXIX', 'MMLDXX', 'MMLDXXI', 'MMLDXXII', 'MMLDXXIII', 'MMLDXXIV', 'MMLDXXV', 'MMLDXXVI', 'MMLDXXVII', 'MMLDXXVIII', 'MMLDXXIX', 'MMLDXXX', 'MMLDXXXI', 'MMLDXXXII', 'MMLDXXXIII', 'MMLDXXXIV', 'MMLDXXXV', 'MMLDXXXVI', 'MMLDXXXVII', 'MMLDXXXVIII', 'MMLDXXXIX', 'MMLDXL', 'MMLDXLI', 'MMLDXLII', 'MMLDXLIII', 'MMLDXLIV', 'MMLDVL', 'MMLDVLI', 'MMLDVLII', 'MMLDVLIII', 'MMLDVLIV', 'MMD', 'MMDI', 'MMDII', 'MMDIII', 'MMDIV', 'MMDV', 'MMDVI', 'MMDVII', 'MMDVIII', 'MMDIX', 'MMDX', 'MMDXI', 'MMDXII', 'MMDXIII', 'MMDXIV', 'MMDXV', 'MMDXVI', 'MMDXVII', 'MMDXVIII', 'MMDXIX', 'MMDXX', 'MMDXXI', 'MMDXXII', 'MMDXXIII', 'MMDXXIV', 'MMDXXV', 'MMDXXVI', 'MMDXXVII', 'MMDXXVIII', 'MMDXXIX', 'MMDXXX', 'MMDXXXI', 'MMDXXXII', 'MMDXXXIII', 'MMDXXXIV', 'MMDXXXV', 'MMDXXXVI', 'MMDXXXVII', 'MMDXXXVIII', 'MMDXXXIX', 'MMDXL', 'MMDXLI', 'MMDXLII', 'MMDXLIII', 'MMDXLIV', 'MMDVL', 'MMDVLI', 'MMDVLII', 'MMDVLIII', 'MMDVLIV', 'MMDL', 'MMDLI', 'MMDLII', 'MMDLIII', 'MMDLIV', 'MMDLV', 'MMDLVI', 'MMDLVII', 'MMDLVIII', 'MMDLIX', 'MMDLX', 'MMDLXI', 'MMDLXII', 'MMDLXIII', 'MMDLXIV', 'MMDLXV', 'MMDLXVI', 'MMDLXVII', 'MMDLXVIII', 'MMDLXIX', 'MMDLXX', 'MMDLXXI', 'MMDLXXII', 'MMDLXXIII', 'MMDLXXIV', 'MMDLXXV', 'MMDLXXVI', 'MMDLXXVII', 'MMDLXXVIII', 'MMDLXXIX', 'MMDLXXX', 'MMDLXXXI', 'MMDLXXXII', 'MMDLXXXIII', 'MMDLXXXIV', 'MMDLXXXV', 'MMDLXXXVI', 'MMDLXXXVII', 'MMDLXXXVIII', 'MMDLXXXIX', 'MMDXC', 'MMDXCI', 'MMDXCII', 'MMDXCIII', 'MMDXCIV', 'MMDVC', 'MMDVCI', 'MMDVCII', 'MMDVCIII', 'MMDVCIV', 'MMDC', 'MMDCI', 'MMDCII', 'MMDCIII', 'MMDCIV', 'MMDCV', 'MMDCVI', 'MMDCVII', 'MMDCVIII', 'MMDCIX', 'MMDCX', 'MMDCXI', 'MMDCXII', 'MMDCXIII', 'MMDCXIV', 'MMDCXV', 'MMDCXVI', 'MMDCXVII', 'MMDCXVIII', 'MMDCXIX', 'MMDCXX', 'MMDCXXI', 'MMDCXXII', 'MMDCXXIII', 'MMDCXXIV', 'MMDCXXV', 'MMDCXXVI', 'MMDCXXVII', 'MMDCXXVIII', 'MMDCXXIX', 'MMDCXXX', 'MMDCXXXI', 'MMDCXXXII', 'MMDCXXXIII', 'MMDCXXXIV', 'MMDCXXXV', 'MMDCXXXVI', 'MMDCXXXVII', 'MMDCXXXVIII', 'MMDCXXXIX', 'MMDCXL', 'MMDCXLI', 'MMDCXLII', 'MMDCXLIII', 'MMDCXLIV', 'MMDCVL', 'MMDCVLI', 'MMDCVLII', 'MMDCVLIII', 'MMDCVLIV', 'MMDCL', 'MMDCLI', 'MMDCLII', 'MMDCLIII', 'MMDCLIV', 'MMDCLV', 'MMDCLVI', 'MMDCLVII', 'MMDCLVIII', 'MMDCLIX', 'MMDCLX', 'MMDCLXI', 'MMDCLXII', 'MMDCLXIII', 'MMDCLXIV', 'MMDCLXV', 'MMDCLXVI', 'MMDCLXVII', 'MMDCLXVIII', 'MMDCLXIX', 'MMDCLXX', 'MMDCLXXI', 'MMDCLXXII', 'MMDCLXXIII', 'MMDCLXXIV', 'MMDCLXXV', 'MMDCLXXVI', 'MMDCLXXVII', 'MMDCLXXVIII', 'MMDCLXXIX', 'MMDCLXXX', 'MMDCLXXXI', 'MMDCLXXXII', 'MMDCLXXXIII', 'MMDCLXXXIV', 'MMDCLXXXV', 'MMDCLXXXVI', 'MMDCLXXXVII', 'MMDCLXXXVIII', 'MMDCLXXXIX', 'MMDCXC', 'MMDCXCI', 'MMDCXCII', 'MMDCXCIII', 'MMDCXCIV', 'MMDCVC', 'MMDCVCI', 'MMDCVCII', 'MMDCVCIII', 'MMDCVCIV', 'MMDCC', 'MMDCCI', 'MMDCCII', 'MMDCCIII', 'MMDCCIV', 'MMDCCV', 'MMDCCVI', 'MMDCCVII', 'MMDCCVIII', 'MMDCCIX', 'MMDCCX', 'MMDCCXI', 'MMDCCXII', 'MMDCCXIII', 'MMDCCXIV', 'MMDCCXV', 'MMDCCXVI', 'MMDCCXVII', 'MMDCCXVIII', 'MMDCCXIX', 'MMDCCXX', 'MMDCCXXI', 'MMDCCXXII', 'MMDCCXXIII', 'MMDCCXXIV', 'MMDCCXXV', 'MMDCCXXVI', 'MMDCCXXVII', 'MMDCCXXVIII', 'MMDCCXXIX', 'MMDCCXXX', 'MMDCCXXXI', 'MMDCCXXXII', 'MMDCCXXXIII', 'MMDCCXXXIV', 'MMDCCXXXV', 'MMDCCXXXVI', 'MMDCCXXXVII', 'MMDCCXXXVIII', 'MMDCCXXXIX', 'MMDCCXL', 'MMDCCXLI', 'MMDCCXLII', 'MMDCCXLIII', 'MMDCCXLIV', 'MMDCCVL', 'MMDCCVLI', 'MMDCCVLII', 'MMDCCVLIII', 'MMDCCVLIV', 'MMDCCL', 'MMDCCLI', 'MMDCCLII', 'MMDCCLIII', 'MMDCCLIV', 'MMDCCLV', 'MMDCCLVI', 'MMDCCLVII', 'MMDCCLVIII', 'MMDCCLIX', 'MMDCCLX', 'MMDCCLXI', 'MMDCCLXII', 'MMDCCLXIII', 'MMDCCLXIV', 'MMDCCLXV', 'MMDCCLXVI', 'MMDCCLXVII', 'MMDCCLXVIII', 'MMDCCLXIX', 'MMDCCLXX', 'MMDCCLXXI', 'MMDCCLXXII', 'MMDCCLXXIII', 'MMDCCLXXIV', 'MMDCCLXXV', 'MMDCCLXXVI', 'MMDCCLXXVII', 'MMDCCLXXVIII', 'MMDCCLXXIX', 'MMDCCLXXX', 'MMDCCLXXXI', 'MMDCCLXXXII', 'MMDCCLXXXIII', 'MMDCCLXXXIV', 'MMDCCLXXXV', 'MMDCCLXXXVI', 'MMDCCLXXXVII', 'MMDCCLXXXVIII', 'MMDCCLXXXIX', 'MMDCCXC', 'MMDCCXCI', 'MMDCCXCII', 'MMDCCXCIII', 'MMDCCXCIV', 'MMDCCVC', 'MMDCCVCI', 'MMDCCVCII', 'MMDCCVCIII', 'MMDCCVCIV', 'MMDCCC', 'MMDCCCI', 'MMDCCCII', 'MMDCCCIII', 'MMDCCCIV', 'MMDCCCV', 'MMDCCCVI', 'MMDCCCVII', 'MMDCCCVIII', 'MMDCCCIX', 'MMDCCCX', 'MMDCCCXI', 'MMDCCCXII', 'MMDCCCXIII', 'MMDCCCXIV', 'MMDCCCXV', 'MMDCCCXVI', 'MMDCCCXVII', 'MMDCCCXVIII', 'MMDCCCXIX', 'MMDCCCXX', 'MMDCCCXXI', 'MMDCCCXXII', 'MMDCCCXXIII', 'MMDCCCXXIV', 'MMDCCCXXV', 'MMDCCCXXVI', 'MMDCCCXXVII', 'MMDCCCXXVIII', 'MMDCCCXXIX', 'MMDCCCXXX', 'MMDCCCXXXI', 'MMDCCCXXXII', 'MMDCCCXXXIII', 'MMDCCCXXXIV', 'MMDCCCXXXV', 'MMDCCCXXXVI', 'MMDCCCXXXVII', 'MMDCCCXXXVIII', 'MMDCCCXXXIX', 'MMDCCCXL', 'MMDCCCXLI', 'MMDCCCXLII', 'MMDCCCXLIII', 'MMDCCCXLIV', 'MMDCCCVL', 'MMDCCCVLI', 'MMDCCCVLII', 'MMDCCCVLIII', 'MMDCCCVLIV', 'MMDCCCL', 'MMDCCCLI', 'MMDCCCLII', 'MMDCCCLIII', 'MMDCCCLIV', 'MMDCCCLV', 'MMDCCCLVI', 'MMDCCCLVII', 'MMDCCCLVIII', 'MMDCCCLIX', 'MMDCCCLX', 'MMDCCCLXI', 'MMDCCCLXII', 'MMDCCCLXIII', 'MMDCCCLXIV', 'MMDCCCLXV', 'MMDCCCLXVI', 'MMDCCCLXVII', 'MMDCCCLXVIII', 'MMDCCCLXIX', 'MMDCCCLXX', 'MMDCCCLXXI', 'MMDCCCLXXII', 'MMDCCCLXXIII', 'MMDCCCLXXIV', 'MMDCCCLXXV', 'MMDCCCLXXVI', 'MMDCCCLXXVII', 'MMDCCCLXXVIII', 'MMDCCCLXXIX', 'MMDCCCLXXX', 'MMDCCCLXXXI', 'MMDCCCLXXXII', 'MMDCCCLXXXIII', 'MMDCCCLXXXIV', 'MMDCCCLXXXV', 'MMDCCCLXXXVI', 'MMDCCCLXXXVII', 'MMDCCCLXXXVIII', 'MMDCCCLXXXIX', 'MMDCCCXC', 'MMDCCCXCI', 'MMDCCCXCII', 'MMDCCCXCIII', 'MMDCCCXCIV', 'MMDCCCVC', 'MMDCCCVCI', 'MMDCCCVCII', 'MMDCCCVCIII', 'MMDCCCVCIV', 'MMCM', 'MMCMI', 'MMCMII', 'MMCMIII', 'MMCMIV', 'MMCMV', 'MMCMVI', 'MMCMVII', 'MMCMVIII', 'MMCMIX', 'MMCMX', 'MMCMXI', 'MMCMXII', 'MMCMXIII', 'MMCMXIV', 'MMCMXV', 'MMCMXVI', 'MMCMXVII', 'MMCMXVIII', 'MMCMXIX', 'MMCMXX', 'MMCMXXI', 'MMCMXXII', 'MMCMXXIII', 'MMCMXXIV', 'MMCMXXV', 'MMCMXXVI', 'MMCMXXVII', 'MMCMXXVIII', 'MMCMXXIX', 'MMCMXXX', 'MMCMXXXI', 'MMCMXXXII', 'MMCMXXXIII', 'MMCMXXXIV', 'MMCMXXXV', 'MMCMXXXVI', 'MMCMXXXVII', 'MMCMXXXVIII', 'MMCMXXXIX', 'MMCMXL', 'MMCMXLI', 'MMCMXLII', 'MMCMXLIII', 'MMCMXLIV', 'MMCMVL', 'MMCMVLI', 'MMCMVLII', 'MMCMVLIII', 'MMCMVLIV', 'MMLM', 'MMLMI', 'MMLMII', 'MMLMIII', 'MMLMIV', 'MMLMV', 'MMLMVI', 'MMLMVII', 'MMLMVIII', 'MMLMIX', 'MMLMX', 'MMLMXI', 'MMLMXII', 'MMLMXIII', 'MMLMXIV', 'MMLMXV', 'MMLMXVI', 'MMLMXVII', 'MMLMXVIII', 'MMLMXIX', 'MMLMXX', 'MMLMXXI', 'MMLMXXII', 'MMLMXXIII', 'MMLMXXIV', 'MMLMXXV', 'MMLMXXVI', 'MMLMXXVII', 'MMLMXXVIII', 'MMLMXXIX', 'MMLMXXX', 'MMLMXXXI', 'MMLMXXXII', 'MMLMXXXIII', 'MMLMXXXIV', 'MMLMXXXV', 'MMLMXXXVI', 'MMLMXXXVII', 'MMLMXXXVIII', 'MMLMXXXIX', 'MMLMXL', 'MMLMXLI', 'MMLMXLII', 'MMLMXLIII', 'MMLMXLIV', 'MMLMVL', 'MMLMVLI', 'MMLMVLII', 'MMLMVLIII', 'MMLMVLIV', 'MMM', 'MMMI', 'MMMII', 'MMMIII', 'MMMIV', 'MMMV', 'MMMVI', 'MMMVII', 'MMMVIII', 'MMMIX', 'MMMX', 'MMMXI', 'MMMXII', 'MMMXIII', 'MMMXIV', 'MMMXV', 'MMMXVI', 'MMMXVII', 'MMMXVIII', 'MMMXIX', 'MMMXX', 'MMMXXI', 'MMMXXII', 'MMMXXIII', 'MMMXXIV', 'MMMXXV', 'MMMXXVI', 'MMMXXVII', 'MMMXXVIII', 'MMMXXIX', 'MMMXXX', 'MMMXXXI', 'MMMXXXII', 'MMMXXXIII', 'MMMXXXIV', 'MMMXXXV', 'MMMXXXVI', 'MMMXXXVII', 'MMMXXXVIII', 'MMMXXXIX', 'MMMXL', 'MMMXLI', 'MMMXLII', 'MMMXLIII', 'MMMXLIV', 'MMMVL', 'MMMVLI', 'MMMVLII', 'MMMVLIII', 'MMMVLIV', 'MMML', 'MMMLI', 'MMMLII', 'MMMLIII', 'MMMLIV', 'MMMLV', 'MMMLVI', 'MMMLVII', 'MMMLVIII', 'MMMLIX', 'MMMLX', 'MMMLXI', 'MMMLXII', 'MMMLXIII', 'MMMLXIV', 'MMMLXV', 'MMMLXVI', 'MMMLXVII', 'MMMLXVIII', 'MMMLXIX', 'MMMLXX', 'MMMLXXI', 'MMMLXXII', 'MMMLXXIII', 'MMMLXXIV', 'MMMLXXV', 'MMMLXXVI', 'MMMLXXVII', 'MMMLXXVIII', 'MMMLXXIX', 'MMMLXXX', 'MMMLXXXI', 'MMMLXXXII', 'MMMLXXXIII', 'MMMLXXXIV', 'MMMLXXXV', 'MMMLXXXVI', 'MMMLXXXVII', 'MMMLXXXVIII', 'MMMLXXXIX', 'MMMXC', 'MMMXCI', 'MMMXCII', 'MMMXCIII', 'MMMXCIV', 'MMMVC', 'MMMVCI', 'MMMVCII', 'MMMVCIII', 'MMMVCIV', 'MMMC', 'MMMCI', 'MMMCII', 'MMMCIII', 'MMMCIV', 'MMMCV', 'MMMCVI', 'MMMCVII', 'MMMCVIII', 'MMMCIX', 'MMMCX', 'MMMCXI', 'MMMCXII', 'MMMCXIII', 'MMMCXIV', 'MMMCXV', 'MMMCXVI', 'MMMCXVII', 'MMMCXVIII', 'MMMCXIX', 'MMMCXX', 'MMMCXXI', 'MMMCXXII', 'MMMCXXIII', 'MMMCXXIV', 'MMMCXXV', 'MMMCXXVI', 'MMMCXXVII', 'MMMCXXVIII', 'MMMCXXIX', 'MMMCXXX', 'MMMCXXXI', 'MMMCXXXII', 'MMMCXXXIII', 'MMMCXXXIV', 'MMMCXXXV', 'MMMCXXXVI', 'MMMCXXXVII', 'MMMCXXXVIII', 'MMMCXXXIX', 'MMMCXL', 'MMMCXLI', 'MMMCXLII', 'MMMCXLIII', 'MMMCXLIV', 'MMMCVL', 'MMMCVLI', 'MMMCVLII', 'MMMCVLIII', 'MMMCVLIV', 'MMMCL', 'MMMCLI', 'MMMCLII', 'MMMCLIII', 'MMMCLIV', 'MMMCLV', 'MMMCLVI', 'MMMCLVII', 'MMMCLVIII', 'MMMCLIX', 'MMMCLX', 'MMMCLXI', 'MMMCLXII', 'MMMCLXIII', 'MMMCLXIV', 'MMMCLXV', 'MMMCLXVI', 'MMMCLXVII', 'MMMCLXVIII', 'MMMCLXIX', 'MMMCLXX', 'MMMCLXXI', 'MMMCLXXII', 'MMMCLXXIII', 'MMMCLXXIV', 'MMMCLXXV', 'MMMCLXXVI', 'MMMCLXXVII', 'MMMCLXXVIII', 'MMMCLXXIX', 'MMMCLXXX', 'MMMCLXXXI', 'MMMCLXXXII', 'MMMCLXXXIII', 'MMMCLXXXIV', 'MMMCLXXXV', 'MMMCLXXXVI', 'MMMCLXXXVII', 'MMMCLXXXVIII', 'MMMCLXXXIX', 'MMMCXC', 'MMMCXCI', 'MMMCXCII', 'MMMCXCIII', 'MMMCXCIV', 'MMMCVC', 'MMMCVCI', 'MMMCVCII', 'MMMCVCIII', 'MMMCVCIV', 'MMMCC', 'MMMCCI', 'MMMCCII', 'MMMCCIII', 'MMMCCIV', 'MMMCCV', 'MMMCCVI', 'MMMCCVII', 'MMMCCVIII', 'MMMCCIX', 'MMMCCX', 'MMMCCXI', 'MMMCCXII', 'MMMCCXIII', 'MMMCCXIV', 'MMMCCXV', 'MMMCCXVI', 'MMMCCXVII', 'MMMCCXVIII', 'MMMCCXIX', 'MMMCCXX', 'MMMCCXXI', 'MMMCCXXII', 'MMMCCXXIII', 'MMMCCXXIV', 'MMMCCXXV', 'MMMCCXXVI', 'MMMCCXXVII', 'MMMCCXXVIII', 'MMMCCXXIX', 'MMMCCXXX', 'MMMCCXXXI', 'MMMCCXXXII', 'MMMCCXXXIII', 'MMMCCXXXIV', 'MMMCCXXXV', 'MMMCCXXXVI', 'MMMCCXXXVII', 'MMMCCXXXVIII', 'MMMCCXXXIX', 'MMMCCXL', 'MMMCCXLI', 'MMMCCXLII', 'MMMCCXLIII', 'MMMCCXLIV', 'MMMCCVL', 'MMMCCVLI', 'MMMCCVLII', 'MMMCCVLIII', 'MMMCCVLIV', 'MMMCCL', 'MMMCCLI', 'MMMCCLII', 'MMMCCLIII', 'MMMCCLIV', 'MMMCCLV', 'MMMCCLVI', 'MMMCCLVII', 'MMMCCLVIII', 'MMMCCLIX', 'MMMCCLX', 'MMMCCLXI', 'MMMCCLXII', 'MMMCCLXIII', 'MMMCCLXIV', 'MMMCCLXV', 'MMMCCLXVI', 'MMMCCLXVII', 'MMMCCLXVIII', 'MMMCCLXIX', 'MMMCCLXX', 'MMMCCLXXI', 'MMMCCLXXII', 'MMMCCLXXIII', 'MMMCCLXXIV', 'MMMCCLXXV', 'MMMCCLXXVI', 'MMMCCLXXVII', 'MMMCCLXXVIII', 'MMMCCLXXIX', 'MMMCCLXXX', 'MMMCCLXXXI', 'MMMCCLXXXII', 'MMMCCLXXXIII', 'MMMCCLXXXIV', 'MMMCCLXXXV', 'MMMCCLXXXVI', 'MMMCCLXXXVII', 'MMMCCLXXXVIII', 'MMMCCLXXXIX', 'MMMCCXC', 'MMMCCXCI', 'MMMCCXCII', 'MMMCCXCIII', 'MMMCCXCIV', 'MMMCCVC', 'MMMCCVCI', 'MMMCCVCII', 'MMMCCVCIII', 'MMMCCVCIV', 'MMMCCC', 'MMMCCCI', 'MMMCCCII', 'MMMCCCIII', 'MMMCCCIV', 'MMMCCCV', 'MMMCCCVI', 'MMMCCCVII', 'MMMCCCVIII', 'MMMCCCIX', 'MMMCCCX', 'MMMCCCXI', 'MMMCCCXII', 'MMMCCCXIII', 'MMMCCCXIV', 'MMMCCCXV', 'MMMCCCXVI', 'MMMCCCXVII', 'MMMCCCXVIII', 'MMMCCCXIX', 'MMMCCCXX', 'MMMCCCXXI', 'MMMCCCXXII', 'MMMCCCXXIII', 'MMMCCCXXIV', 'MMMCCCXXV', 'MMMCCCXXVI', 'MMMCCCXXVII', 'MMMCCCXXVIII', 'MMMCCCXXIX', 'MMMCCCXXX', 'MMMCCCXXXI', 'MMMCCCXXXII', 'MMMCCCXXXIII', 'MMMCCCXXXIV', 'MMMCCCXXXV', 'MMMCCCXXXVI', 'MMMCCCXXXVII', 'MMMCCCXXXVIII', 'MMMCCCXXXIX', 'MMMCCCXL', 'MMMCCCXLI', 'MMMCCCXLII', 'MMMCCCXLIII', 'MMMCCCXLIV', 'MMMCCCVL', 'MMMCCCVLI', 'MMMCCCVLII', 'MMMCCCVLIII', 'MMMCCCVLIV', 'MMMCCCL', 'MMMCCCLI', 'MMMCCCLII', 'MMMCCCLIII', 'MMMCCCLIV', 'MMMCCCLV', 'MMMCCCLVI', 'MMMCCCLVII', 'MMMCCCLVIII', 'MMMCCCLIX', 'MMMCCCLX', 'MMMCCCLXI', 'MMMCCCLXII', 'MMMCCCLXIII', 'MMMCCCLXIV', 'MMMCCCLXV', 'MMMCCCLXVI', 'MMMCCCLXVII', 'MMMCCCLXVIII', 'MMMCCCLXIX', 'MMMCCCLXX', 'MMMCCCLXXI', 'MMMCCCLXXII', 'MMMCCCLXXIII', 'MMMCCCLXXIV', 'MMMCCCLXXV', 'MMMCCCLXXVI', 'MMMCCCLXXVII', 'MMMCCCLXXVIII', 'MMMCCCLXXIX', 'MMMCCCLXXX', 'MMMCCCLXXXI', 'MMMCCCLXXXII', 'MMMCCCLXXXIII', 'MMMCCCLXXXIV', 'MMMCCCLXXXV', 'MMMCCCLXXXVI', 'MMMCCCLXXXVII', 'MMMCCCLXXXVIII', 'MMMCCCLXXXIX', 'MMMCCCXC', 'MMMCCCXCI', 'MMMCCCXCII', 'MMMCCCXCIII', 'MMMCCCXCIV', 'MMMCCCVC', 'MMMCCCVCI', 'MMMCCCVCII', 'MMMCCCVCIII', 'MMMCCCVCIV', 'MMMCD', 'MMMCDI', 'MMMCDII', 'MMMCDIII', 'MMMCDIV', 'MMMCDV', 'MMMCDVI', 'MMMCDVII', 'MMMCDVIII', 'MMMCDIX', 'MMMCDX', 'MMMCDXI', 'MMMCDXII', 'MMMCDXIII', 'MMMCDXIV', 'MMMCDXV', 'MMMCDXVI', 'MMMCDXVII', 'MMMCDXVIII', 'MMMCDXIX', 'MMMCDXX', 'MMMCDXXI', 'MMMCDXXII', 'MMMCDXXIII', 'MMMCDXXIV', 'MMMCDXXV', 'MMMCDXXVI', 'MMMCDXXVII', 'MMMCDXXVIII', 'MMMCDXXIX', 'MMMCDXXX', 'MMMCDXXXI', 'MMMCDXXXII', 'MMMCDXXXIII', 'MMMCDXXXIV', 'MMMCDXXXV', 'MMMCDXXXVI', 'MMMCDXXXVII', 'MMMCDXXXVIII', 'MMMCDXXXIX', 'MMMCDXL', 'MMMCDXLI', 'MMMCDXLII', 'MMMCDXLIII', 'MMMCDXLIV', 'MMMCDVL', 'MMMCDVLI', 'MMMCDVLII', 'MMMCDVLIII', 'MMMCDVLIV', 'MMMLD', 'MMMLDI', 'MMMLDII', 'MMMLDIII', 'MMMLDIV', 'MMMLDV', 'MMMLDVI', 'MMMLDVII', 'MMMLDVIII', 'MMMLDIX', 'MMMLDX', 'MMMLDXI', 'MMMLDXII', 'MMMLDXIII', 'MMMLDXIV', 'MMMLDXV', 'MMMLDXVI', 'MMMLDXVII', 'MMMLDXVIII', 'MMMLDXIX', 'MMMLDXX', 'MMMLDXXI', 'MMMLDXXII', 'MMMLDXXIII', 'MMMLDXXIV', 'MMMLDXXV', 'MMMLDXXVI', 'MMMLDXXVII', 'MMMLDXXVIII', 'MMMLDXXIX', 'MMMLDXXX', 'MMMLDXXXI', 'MMMLDXXXII', 'MMMLDXXXIII', 'MMMLDXXXIV', 'MMMLDXXXV', 'MMMLDXXXVI', 'MMMLDXXXVII', 'MMMLDXXXVIII', 'MMMLDXXXIX', 'MMMLDXL', 'MMMLDXLI', 'MMMLDXLII', 'MMMLDXLIII', 'MMMLDXLIV', 'MMMLDVL', 'MMMLDVLI', 'MMMLDVLII', 'MMMLDVLIII', 'MMMLDVLIV', 'MMMD', 'MMMDI', 'MMMDII', 'MMMDIII', 'MMMDIV', 'MMMDV', 'MMMDVI', 'MMMDVII', 'MMMDVIII', 'MMMDIX', 'MMMDX', 'MMMDXI', 'MMMDXII', 'MMMDXIII', 'MMMDXIV', 'MMMDXV', 'MMMDXVI', 'MMMDXVII', 'MMMDXVIII', 'MMMDXIX', 'MMMDXX', 'MMMDXXI', 'MMMDXXII', 'MMMDXXIII', 'MMMDXXIV', 'MMMDXXV', 'MMMDXXVI', 'MMMDXXVII', 'MMMDXXVIII', 'MMMDXXIX', 'MMMDXXX', 'MMMDXXXI', 'MMMDXXXII', 'MMMDXXXIII', 'MMMDXXXIV', 'MMMDXXXV', 'MMMDXXXVI', 'MMMDXXXVII', 'MMMDXXXVIII', 'MMMDXXXIX', 'MMMDXL', 'MMMDXLI', 'MMMDXLII', 'MMMDXLIII', 'MMMDXLIV', 'MMMDVL', 'MMMDVLI', 'MMMDVLII', 'MMMDVLIII', 'MMMDVLIV', 'MMMDL', 'MMMDLI', 'MMMDLII', 'MMMDLIII', 'MMMDLIV', 'MMMDLV', 'MMMDLVI', 'MMMDLVII', 'MMMDLVIII', 'MMMDLIX', 'MMMDLX', 'MMMDLXI', 'MMMDLXII', 'MMMDLXIII', 'MMMDLXIV', 'MMMDLXV', 'MMMDLXVI', 'MMMDLXVII', 'MMMDLXVIII', 'MMMDLXIX', 'MMMDLXX', 'MMMDLXXI', 'MMMDLXXII', 'MMMDLXXIII', 'MMMDLXXIV', 'MMMDLXXV', 'MMMDLXXVI', 'MMMDLXXVII', 'MMMDLXXVIII', 'MMMDLXXIX', 'MMMDLXXX', 'MMMDLXXXI', 'MMMDLXXXII', 'MMMDLXXXIII', 'MMMDLXXXIV', 'MMMDLXXXV', 'MMMDLXXXVI', 'MMMDLXXXVII', 'MMMDLXXXVIII', 'MMMDLXXXIX', 'MMMDXC', 'MMMDXCI', 'MMMDXCII', 'MMMDXCIII', 'MMMDXCIV', 'MMMDVC', 'MMMDVCI', 'MMMDVCII', 'MMMDVCIII', 'MMMDVCIV', 'MMMDC', 'MMMDCI', 'MMMDCII', 'MMMDCIII', 'MMMDCIV', 'MMMDCV', 'MMMDCVI', 'MMMDCVII', 'MMMDCVIII', 'MMMDCIX', 'MMMDCX', 'MMMDCXI', 'MMMDCXII', 'MMMDCXIII', 'MMMDCXIV', 'MMMDCXV', 'MMMDCXVI', 'MMMDCXVII', 'MMMDCXVIII', 'MMMDCXIX', 'MMMDCXX', 'MMMDCXXI', 'MMMDCXXII', 'MMMDCXXIII', 'MMMDCXXIV', 'MMMDCXXV', 'MMMDCXXVI', 'MMMDCXXVII', 'MMMDCXXVIII', 'MMMDCXXIX', 'MMMDCXXX', 'MMMDCXXXI', 'MMMDCXXXII', 'MMMDCXXXIII', 'MMMDCXXXIV', 'MMMDCXXXV', 'MMMDCXXXVI', 'MMMDCXXXVII', 'MMMDCXXXVIII', 'MMMDCXXXIX', 'MMMDCXL', 'MMMDCXLI', 'MMMDCXLII', 'MMMDCXLIII', 'MMMDCXLIV', 'MMMDCVL', 'MMMDCVLI', 'MMMDCVLII', 'MMMDCVLIII', 'MMMDCVLIV', 'MMMDCL', 'MMMDCLI', 'MMMDCLII', 'MMMDCLIII', 'MMMDCLIV', 'MMMDCLV', 'MMMDCLVI', 'MMMDCLVII', 'MMMDCLVIII', 'MMMDCLIX', 'MMMDCLX', 'MMMDCLXI', 'MMMDCLXII', 'MMMDCLXIII', 'MMMDCLXIV', 'MMMDCLXV', 'MMMDCLXVI', 'MMMDCLXVII', 'MMMDCLXVIII', 'MMMDCLXIX', 'MMMDCLXX', 'MMMDCLXXI', 'MMMDCLXXII', 'MMMDCLXXIII', 'MMMDCLXXIV', 'MMMDCLXXV', 'MMMDCLXXVI', 'MMMDCLXXVII', 'MMMDCLXXVIII', 'MMMDCLXXIX', 'MMMDCLXXX', 'MMMDCLXXXI', 'MMMDCLXXXII', 'MMMDCLXXXIII', 'MMMDCLXXXIV', 'MMMDCLXXXV', 'MMMDCLXXXVI', 'MMMDCLXXXVII', 'MMMDCLXXXVIII', 'MMMDCLXXXIX', 'MMMDCXC', 'MMMDCXCI', 'MMMDCXCII', 'MMMDCXCIII', 'MMMDCXCIV', 'MMMDCVC', 'MMMDCVCI', 'MMMDCVCII', 'MMMDCVCIII', 'MMMDCVCIV', 'MMMDCC', 'MMMDCCI', 'MMMDCCII', 'MMMDCCIII', 'MMMDCCIV', 'MMMDCCV', 'MMMDCCVI', 'MMMDCCVII', 'MMMDCCVIII', 'MMMDCCIX', 'MMMDCCX', 'MMMDCCXI', 'MMMDCCXII', 'MMMDCCXIII', 'MMMDCCXIV', 'MMMDCCXV', 'MMMDCCXVI', 'MMMDCCXVII', 'MMMDCCXVIII', 'MMMDCCXIX', 'MMMDCCXX', 'MMMDCCXXI', 'MMMDCCXXII', 'MMMDCCXXIII', 'MMMDCCXXIV', 'MMMDCCXXV', 'MMMDCCXXVI', 'MMMDCCXXVII', 'MMMDCCXXVIII', 'MMMDCCXXIX', 'MMMDCCXXX', 'MMMDCCXXXI', 'MMMDCCXXXII', 'MMMDCCXXXIII', 'MMMDCCXXXIV', 'MMMDCCXXXV', 'MMMDCCXXXVI', 'MMMDCCXXXVII', 'MMMDCCXXXVIII', 'MMMDCCXXXIX', 'MMMDCCXL', 'MMMDCCXLI', 'MMMDCCXLII', 'MMMDCCXLIII', 'MMMDCCXLIV', 'MMMDCCVL', 'MMMDCCVLI', 'MMMDCCVLII', 'MMMDCCVLIII', 'MMMDCCVLIV', 'MMMDCCL', 'MMMDCCLI', 'MMMDCCLII', 'MMMDCCLIII', 'MMMDCCLIV', 'MMMDCCLV', 'MMMDCCLVI', 'MMMDCCLVII', 'MMMDCCLVIII', 'MMMDCCLIX', 'MMMDCCLX', 'MMMDCCLXI', 'MMMDCCLXII', 'MMMDCCLXIII', 'MMMDCCLXIV', 'MMMDCCLXV', 'MMMDCCLXVI', 'MMMDCCLXVII', 'MMMDCCLXVIII', 'MMMDCCLXIX', 'MMMDCCLXX', 'MMMDCCLXXI', 'MMMDCCLXXII', 'MMMDCCLXXIII', 'MMMDCCLXXIV', 'MMMDCCLXXV', 'MMMDCCLXXVI', 'MMMDCCLXXVII', 'MMMDCCLXXVIII', 'MMMDCCLXXIX', 'MMMDCCLXXX', 'MMMDCCLXXXI', 'MMMDCCLXXXII', 'MMMDCCLXXXIII', 'MMMDCCLXXXIV', 'MMMDCCLXXXV', 'MMMDCCLXXXVI', 'MMMDCCLXXXVII', 'MMMDCCLXXXVIII', 'MMMDCCLXXXIX', 'MMMDCCXC', 'MMMDCCXCI', 'MMMDCCXCII', 'MMMDCCXCIII', 'MMMDCCXCIV', 'MMMDCCVC', 'MMMDCCVCI', 'MMMDCCVCII', 'MMMDCCVCIII', 'MMMDCCVCIV', 'MMMDCCC', 'MMMDCCCI', 'MMMDCCCII', 'MMMDCCCIII', 'MMMDCCCIV', 'MMMDCCCV', 'MMMDCCCVI', 'MMMDCCCVII', 'MMMDCCCVIII', 'MMMDCCCIX', 'MMMDCCCX', 'MMMDCCCXI', 'MMMDCCCXII', 'MMMDCCCXIII', 'MMMDCCCXIV', 'MMMDCCCXV', 'MMMDCCCXVI', 'MMMDCCCXVII', 'MMMDCCCXVIII', 'MMMDCCCXIX', 'MMMDCCCXX', 'MMMDCCCXXI', 'MMMDCCCXXII', 'MMMDCCCXXIII', 'MMMDCCCXXIV', 'MMMDCCCXXV', 'MMMDCCCXXVI', 'MMMDCCCXXVII', 'MMMDCCCXXVIII', 'MMMDCCCXXIX', 'MMMDCCCXXX', 'MMMDCCCXXXI', 'MMMDCCCXXXII', 'MMMDCCCXXXIII', 'MMMDCCCXXXIV', 'MMMDCCCXXXV', 'MMMDCCCXXXVI', 'MMMDCCCXXXVII', 'MMMDCCCXXXVIII', 'MMMDCCCXXXIX', 'MMMDCCCXL', 'MMMDCCCXLI', 'MMMDCCCXLII', 'MMMDCCCXLIII', 'MMMDCCCXLIV', 'MMMDCCCVL', 'MMMDCCCVLI', 'MMMDCCCVLII', 'MMMDCCCVLIII', 'MMMDCCCVLIV', 'MMMDCCCL', 'MMMDCCCLI', 'MMMDCCCLII', 'MMMDCCCLIII', 'MMMDCCCLIV', 'MMMDCCCLV', 'MMMDCCCLVI', 'MMMDCCCLVII', 'MMMDCCCLVIII', 'MMMDCCCLIX', 'MMMDCCCLX', 'MMMDCCCLXI', 'MMMDCCCLXII', 'MMMDCCCLXIII', 'MMMDCCCLXIV', 'MMMDCCCLXV', 'MMMDCCCLXVI', 'MMMDCCCLXVII', 'MMMDCCCLXVIII', 'MMMDCCCLXIX', 'MMMDCCCLXX', 'MMMDCCCLXXI', 'MMMDCCCLXXII', 'MMMDCCCLXXIII', 'MMMDCCCLXXIV', 'MMMDCCCLXXV', 'MMMDCCCLXXVI', 'MMMDCCCLXXVII', 'MMMDCCCLXXVIII', 'MMMDCCCLXXIX', 'MMMDCCCLXXX', 'MMMDCCCLXXXI', 'MMMDCCCLXXXII', 'MMMDCCCLXXXIII', 'MMMDCCCLXXXIV', 'MMMDCCCLXXXV', 'MMMDCCCLXXXVI', 'MMMDCCCLXXXVII', 'MMMDCCCLXXXVIII', 'MMMDCCCLXXXIX', 'MMMDCCCXC', 'MMMDCCCXCI', 'MMMDCCCXCII', 'MMMDCCCXCIII', 'MMMDCCCXCIV', 'MMMDCCCVC', 'MMMDCCCVCI', 'MMMDCCCVCII', 'MMMDCCCVCIII', 'MMMDCCCVCIV', 'MMMCM', 'MMMCMI', 'MMMCMII', 'MMMCMIII', 'MMMCMIV', 'MMMCMV', 'MMMCMVI', 'MMMCMVII', 'MMMCMVIII', 'MMMCMIX', 'MMMCMX', 'MMMCMXI', 'MMMCMXII', 'MMMCMXIII', 'MMMCMXIV', 'MMMCMXV', 'MMMCMXVI', 'MMMCMXVII', 'MMMCMXVIII', 'MMMCMXIX', 'MMMCMXX', 'MMMCMXXI', 'MMMCMXXII', 'MMMCMXXIII', 'MMMCMXXIV', 'MMMCMXXV', 'MMMCMXXVI', 'MMMCMXXVII', 'MMMCMXXVIII', 'MMMCMXXIX', 'MMMCMXXX', 'MMMCMXXXI', 'MMMCMXXXII', 'MMMCMXXXIII', 'MMMCMXXXIV', 'MMMCMXXXV', 'MMMCMXXXVI', 'MMMCMXXXVII', 'MMMCMXXXVIII', 'MMMCMXXXIX', 'MMMCMXL', 'MMMCMXLI', 'MMMCMXLII', 'MMMCMXLIII', 'MMMCMXLIV', 'MMMCMVL', 'MMMCMVLI', 'MMMCMVLII', 'MMMCMVLIII', 'MMMCMVLIV', 'MMMLM', 'MMMLMI', 'MMMLMII', 'MMMLMIII', 'MMMLMIV', 'MMMLMV', 'MMMLMVI', 'MMMLMVII', 'MMMLMVIII', 'MMMLMIX', 'MMMLMX', 'MMMLMXI', 'MMMLMXII', 'MMMLMXIII', 'MMMLMXIV', 'MMMLMXV', 'MMMLMXVI', 'MMMLMXVII', 'MMMLMXVIII', 'MMMLMXIX', 'MMMLMXX', 'MMMLMXXI', 'MMMLMXXII', 'MMMLMXXIII', 'MMMLMXXIV', 'MMMLMXXV', 'MMMLMXXVI', 'MMMLMXXVII', 'MMMLMXXVIII', 'MMMLMXXIX', 'MMMLMXXX', 'MMMLMXXXI', 'MMMLMXXXII', 'MMMLMXXXIII', 'MMMLMXXXIV', 'MMMLMXXXV', 'MMMLMXXXVI', 'MMMLMXXXVII', 'MMMLMXXXVIII', 'MMMLMXXXIX', 'MMMLMXL', 'MMMLMXLI', 'MMMLMXLII', 'MMMLMXLIII', 'MMMLMXLIV', 'MMMLMVL', 'MMMLMVLI', 'MMMLMVLII', 'MMMLMVLIII', 'MMMLMVLIV', ] -export const mode2 = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI', 'XII', 'XIII', 'XIV', 'XV', 'XVI', 'XVII', 'XVIII', 'XIX', 'XX', 'XXI', 'XXII', 'XXIII', 'XXIV', 'XXV', 'XXVI', 'XXVII', 'XXVIII', 'XXIX', 'XXX', 'XXXI', 'XXXII', 'XXXIII', 'XXXIV', 'XXXV', 'XXXVI', 'XXXVII', 'XXXVIII', 'XXXIX', 'XL', 'XLI', 'XLII', 'XLIII', 'XLIV', 'VL', 'VLI', 'VLII', 'VLIII', 'IL', 'L', 'LI', 'LII', 'LIII', 'LIV', 'LV', 'LVI', 'LVII', 'LVIII', 'LIX', 'LX', 'LXI', 'LXII', 'LXIII', 'LXIV', 'LXV', 'LXVI', 'LXVII', 'LXVIII', 'LXIX', 'LXX', 'LXXI', 'LXXII', 'LXXIII', 'LXXIV', 'LXXV', 'LXXVI', 'LXXVII', 'LXXVIII', 'LXXIX', 'LXXX', 'LXXXI', 'LXXXII', 'LXXXIII', 'LXXXIV', 'LXXXV', 'LXXXVI', 'LXXXVII', 'LXXXVIII', 'LXXXIX', 'XC', 'XCI', 'XCII', 'XCIII', 'XCIV', 'VC', 'VCI', 'VCII', 'VCIII', 'IC', 'C', 'CI', 'CII', 'CIII', 'CIV', 'CV', 'CVI', 'CVII', 'CVIII', 'CIX', 'CX', 'CXI', 'CXII', 'CXIII', 'CXIV', 'CXV', 'CXVI', 'CXVII', 'CXVIII', 'CXIX', 'CXX', 'CXXI', 'CXXII', 'CXXIII', 'CXXIV', 'CXXV', 'CXXVI', 'CXXVII', 'CXXVIII', 'CXXIX', 'CXXX', 'CXXXI', 'CXXXII', 'CXXXIII', 'CXXXIV', 'CXXXV', 'CXXXVI', 'CXXXVII', 'CXXXVIII', 'CXXXIX', 'CXL', 'CXLI', 'CXLII', 'CXLIII', 'CXLIV', 'CVL', 'CVLI', 'CVLII', 'CVLIII', 'CIL', 'CL', 'CLI', 'CLII', 'CLIII', 'CLIV', 'CLV', 'CLVI', 'CLVII', 'CLVIII', 'CLIX', 'CLX', 'CLXI', 'CLXII', 'CLXIII', 'CLXIV', 'CLXV', 'CLXVI', 'CLXVII', 'CLXVIII', 'CLXIX', 'CLXX', 'CLXXI', 'CLXXII', 'CLXXIII', 'CLXXIV', 'CLXXV', 'CLXXVI', 'CLXXVII', 'CLXXVIII', 'CLXXIX', 'CLXXX', 'CLXXXI', 'CLXXXII', 'CLXXXIII', 'CLXXXIV', 'CLXXXV', 'CLXXXVI', 'CLXXXVII', 'CLXXXVIII', 'CLXXXIX', 'CXC', 'CXCI', 'CXCII', 'CXCIII', 'CXCIV', 'CVC', 'CVCI', 'CVCII', 'CVCIII', 'CIC', 'CC', 'CCI', 'CCII', 'CCIII', 'CCIV', 'CCV', 'CCVI', 'CCVII', 'CCVIII', 'CCIX', 'CCX', 'CCXI', 'CCXII', 'CCXIII', 'CCXIV', 'CCXV', 'CCXVI', 'CCXVII', 'CCXVIII', 'CCXIX', 'CCXX', 'CCXXI', 'CCXXII', 'CCXXIII', 'CCXXIV', 'CCXXV', 'CCXXVI', 'CCXXVII', 'CCXXVIII', 'CCXXIX', 'CCXXX', 'CCXXXI', 'CCXXXII', 'CCXXXIII', 'CCXXXIV', 'CCXXXV', 'CCXXXVI', 'CCXXXVII', 'CCXXXVIII', 'CCXXXIX', 'CCXL', 'CCXLI', 'CCXLII', 'CCXLIII', 'CCXLIV', 'CCVL', 'CCVLI', 'CCVLII', 'CCVLIII', 'CCIL', 'CCL', 'CCLI', 'CCLII', 'CCLIII', 'CCLIV', 'CCLV', 'CCLVI', 'CCLVII', 'CCLVIII', 'CCLIX', 'CCLX', 'CCLXI', 'CCLXII', 'CCLXIII', 'CCLXIV', 'CCLXV', 'CCLXVI', 'CCLXVII', 'CCLXVIII', 'CCLXIX', 'CCLXX', 'CCLXXI', 'CCLXXII', 'CCLXXIII', 'CCLXXIV', 'CCLXXV', 'CCLXXVI', 'CCLXXVII', 'CCLXXVIII', 'CCLXXIX', 'CCLXXX', 'CCLXXXI', 'CCLXXXII', 'CCLXXXIII', 'CCLXXXIV', 'CCLXXXV', 'CCLXXXVI', 'CCLXXXVII', 'CCLXXXVIII', 'CCLXXXIX', 'CCXC', 'CCXCI', 'CCXCII', 'CCXCIII', 'CCXCIV', 'CCVC', 'CCVCI', 'CCVCII', 'CCVCIII', 'CCIC', 'CCC', 'CCCI', 'CCCII', 'CCCIII', 'CCCIV', 'CCCV', 'CCCVI', 'CCCVII', 'CCCVIII', 'CCCIX', 'CCCX', 'CCCXI', 'CCCXII', 'CCCXIII', 'CCCXIV', 'CCCXV', 'CCCXVI', 'CCCXVII', 'CCCXVIII', 'CCCXIX', 'CCCXX', 'CCCXXI', 'CCCXXII', 'CCCXXIII', 'CCCXXIV', 'CCCXXV', 'CCCXXVI', 'CCCXXVII', 'CCCXXVIII', 'CCCXXIX', 'CCCXXX', 'CCCXXXI', 'CCCXXXII', 'CCCXXXIII', 'CCCXXXIV', 'CCCXXXV', 'CCCXXXVI', 'CCCXXXVII', 'CCCXXXVIII', 'CCCXXXIX', 'CCCXL', 'CCCXLI', 'CCCXLII', 'CCCXLIII', 'CCCXLIV', 'CCCVL', 'CCCVLI', 'CCCVLII', 'CCCVLIII', 'CCCIL', 'CCCL', 'CCCLI', 'CCCLII', 'CCCLIII', 'CCCLIV', 'CCCLV', 'CCCLVI', 'CCCLVII', 'CCCLVIII', 'CCCLIX', 'CCCLX', 'CCCLXI', 'CCCLXII', 'CCCLXIII', 'CCCLXIV', 'CCCLXV', 'CCCLXVI', 'CCCLXVII', 'CCCLXVIII', 'CCCLXIX', 'CCCLXX', 'CCCLXXI', 'CCCLXXII', 'CCCLXXIII', 'CCCLXXIV', 'CCCLXXV', 'CCCLXXVI', 'CCCLXXVII', 'CCCLXXVIII', 'CCCLXXIX', 'CCCLXXX', 'CCCLXXXI', 'CCCLXXXII', 'CCCLXXXIII', 'CCCLXXXIV', 'CCCLXXXV', 'CCCLXXXVI', 'CCCLXXXVII', 'CCCLXXXVIII', 'CCCLXXXIX', 'CCCXC', 'CCCXCI', 'CCCXCII', 'CCCXCIII', 'CCCXCIV', 'CCCVC', 'CCCVCI', 'CCCVCII', 'CCCVCIII', 'CCCIC', 'CD', 'CDI', 'CDII', 'CDIII', 'CDIV', 'CDV', 'CDVI', 'CDVII', 'CDVIII', 'CDIX', 'CDX', 'CDXI', 'CDXII', 'CDXIII', 'CDXIV', 'CDXV', 'CDXVI', 'CDXVII', 'CDXVIII', 'CDXIX', 'CDXX', 'CDXXI', 'CDXXII', 'CDXXIII', 'CDXXIV', 'CDXXV', 'CDXXVI', 'CDXXVII', 'CDXXVIII', 'CDXXIX', 'CDXXX', 'CDXXXI', 'CDXXXII', 'CDXXXIII', 'CDXXXIV', 'CDXXXV', 'CDXXXVI', 'CDXXXVII', 'CDXXXVIII', 'CDXXXIX', 'CDXL', 'CDXLI', 'CDXLII', 'CDXLIII', 'CDXLIV', 'CDVL', 'CDVLI', 'CDVLII', 'CDVLIII', 'CDIL', 'LD', 'LDI', 'LDII', 'LDIII', 'LDIV', 'LDV', 'LDVI', 'LDVII', 'LDVIII', 'LDIX', 'LDX', 'LDXI', 'LDXII', 'LDXIII', 'LDXIV', 'LDXV', 'LDXVI', 'LDXVII', 'LDXVIII', 'LDXIX', 'LDXX', 'LDXXI', 'LDXXII', 'LDXXIII', 'LDXXIV', 'LDXXV', 'LDXXVI', 'LDXXVII', 'LDXXVIII', 'LDXXIX', 'LDXXX', 'LDXXXI', 'LDXXXII', 'LDXXXIII', 'LDXXXIV', 'LDXXXV', 'LDXXXVI', 'LDXXXVII', 'LDXXXVIII', 'LDXXXIX', 'XD', 'XDI', 'XDII', 'XDIII', 'XDIV', 'XDV', 'XDVI', 'XDVII', 'XDVIII', 'XDIX', 'D', 'DI', 'DII', 'DIII', 'DIV', 'DV', 'DVI', 'DVII', 'DVIII', 'DIX', 'DX', 'DXI', 'DXII', 'DXIII', 'DXIV', 'DXV', 'DXVI', 'DXVII', 'DXVIII', 'DXIX', 'DXX', 'DXXI', 'DXXII', 'DXXIII', 'DXXIV', 'DXXV', 'DXXVI', 'DXXVII', 'DXXVIII', 'DXXIX', 'DXXX', 'DXXXI', 'DXXXII', 'DXXXIII', 'DXXXIV', 'DXXXV', 'DXXXVI', 'DXXXVII', 'DXXXVIII', 'DXXXIX', 'DXL', 'DXLI', 'DXLII', 'DXLIII', 'DXLIV', 'DVL', 'DVLI', 'DVLII', 'DVLIII', 'DIL', 'DL', 'DLI', 'DLII', 'DLIII', 'DLIV', 'DLV', 'DLVI', 'DLVII', 'DLVIII', 'DLIX', 'DLX', 'DLXI', 'DLXII', 'DLXIII', 'DLXIV', 'DLXV', 'DLXVI', 'DLXVII', 'DLXVIII', 'DLXIX', 'DLXX', 'DLXXI', 'DLXXII', 'DLXXIII', 'DLXXIV', 'DLXXV', 'DLXXVI', 'DLXXVII', 'DLXXVIII', 'DLXXIX', 'DLXXX', 'DLXXXI', 'DLXXXII', 'DLXXXIII', 'DLXXXIV', 'DLXXXV', 'DLXXXVI', 'DLXXXVII', 'DLXXXVIII', 'DLXXXIX', 'DXC', 'DXCI', 'DXCII', 'DXCIII', 'DXCIV', 'DVC', 'DVCI', 'DVCII', 'DVCIII', 'DIC', 'DC', 'DCI', 'DCII', 'DCIII', 'DCIV', 'DCV', 'DCVI', 'DCVII', 'DCVIII', 'DCIX', 'DCX', 'DCXI', 'DCXII', 'DCXIII', 'DCXIV', 'DCXV', 'DCXVI', 'DCXVII', 'DCXVIII', 'DCXIX', 'DCXX', 'DCXXI', 'DCXXII', 'DCXXIII', 'DCXXIV', 'DCXXV', 'DCXXVI', 'DCXXVII', 'DCXXVIII', 'DCXXIX', 'DCXXX', 'DCXXXI', 'DCXXXII', 'DCXXXIII', 'DCXXXIV', 'DCXXXV', 'DCXXXVI', 'DCXXXVII', 'DCXXXVIII', 'DCXXXIX', 'DCXL', 'DCXLI', 'DCXLII', 'DCXLIII', 'DCXLIV', 'DCVL', 'DCVLI', 'DCVLII', 'DCVLIII', 'DCIL', 'DCL', 'DCLI', 'DCLII', 'DCLIII', 'DCLIV', 'DCLV', 'DCLVI', 'DCLVII', 'DCLVIII', 'DCLIX', 'DCLX', 'DCLXI', 'DCLXII', 'DCLXIII', 'DCLXIV', 'DCLXV', 'DCLXVI', 'DCLXVII', 'DCLXVIII', 'DCLXIX', 'DCLXX', 'DCLXXI', 'DCLXXII', 'DCLXXIII', 'DCLXXIV', 'DCLXXV', 'DCLXXVI', 'DCLXXVII', 'DCLXXVIII', 'DCLXXIX', 'DCLXXX', 'DCLXXXI', 'DCLXXXII', 'DCLXXXIII', 'DCLXXXIV', 'DCLXXXV', 'DCLXXXVI', 'DCLXXXVII', 'DCLXXXVIII', 'DCLXXXIX', 'DCXC', 'DCXCI', 'DCXCII', 'DCXCIII', 'DCXCIV', 'DCVC', 'DCVCI', 'DCVCII', 'DCVCIII', 'DCIC', 'DCC', 'DCCI', 'DCCII', 'DCCIII', 'DCCIV', 'DCCV', 'DCCVI', 'DCCVII', 'DCCVIII', 'DCCIX', 'DCCX', 'DCCXI', 'DCCXII', 'DCCXIII', 'DCCXIV', 'DCCXV', 'DCCXVI', 'DCCXVII', 'DCCXVIII', 'DCCXIX', 'DCCXX', 'DCCXXI', 'DCCXXII', 'DCCXXIII', 'DCCXXIV', 'DCCXXV', 'DCCXXVI', 'DCCXXVII', 'DCCXXVIII', 'DCCXXIX', 'DCCXXX', 'DCCXXXI', 'DCCXXXII', 'DCCXXXIII', 'DCCXXXIV', 'DCCXXXV', 'DCCXXXVI', 'DCCXXXVII', 'DCCXXXVIII', 'DCCXXXIX', 'DCCXL', 'DCCXLI', 'DCCXLII', 'DCCXLIII', 'DCCXLIV', 'DCCVL', 'DCCVLI', 'DCCVLII', 'DCCVLIII', 'DCCIL', 'DCCL', 'DCCLI', 'DCCLII', 'DCCLIII', 'DCCLIV', 'DCCLV', 'DCCLVI', 'DCCLVII', 'DCCLVIII', 'DCCLIX', 'DCCLX', 'DCCLXI', 'DCCLXII', 'DCCLXIII', 'DCCLXIV', 'DCCLXV', 'DCCLXVI', 'DCCLXVII', 'DCCLXVIII', 'DCCLXIX', 'DCCLXX', 'DCCLXXI', 'DCCLXXII', 'DCCLXXIII', 'DCCLXXIV', 'DCCLXXV', 'DCCLXXVI', 'DCCLXXVII', 'DCCLXXVIII', 'DCCLXXIX', 'DCCLXXX', 'DCCLXXXI', 'DCCLXXXII', 'DCCLXXXIII', 'DCCLXXXIV', 'DCCLXXXV', 'DCCLXXXVI', 'DCCLXXXVII', 'DCCLXXXVIII', 'DCCLXXXIX', 'DCCXC', 'DCCXCI', 'DCCXCII', 'DCCXCIII', 'DCCXCIV', 'DCCVC', 'DCCVCI', 'DCCVCII', 'DCCVCIII', 'DCCIC', 'DCCC', 'DCCCI', 'DCCCII', 'DCCCIII', 'DCCCIV', 'DCCCV', 'DCCCVI', 'DCCCVII', 'DCCCVIII', 'DCCCIX', 'DCCCX', 'DCCCXI', 'DCCCXII', 'DCCCXIII', 'DCCCXIV', 'DCCCXV', 'DCCCXVI', 'DCCCXVII', 'DCCCXVIII', 'DCCCXIX', 'DCCCXX', 'DCCCXXI', 'DCCCXXII', 'DCCCXXIII', 'DCCCXXIV', 'DCCCXXV', 'DCCCXXVI', 'DCCCXXVII', 'DCCCXXVIII', 'DCCCXXIX', 'DCCCXXX', 'DCCCXXXI', 'DCCCXXXII', 'DCCCXXXIII', 'DCCCXXXIV', 'DCCCXXXV', 'DCCCXXXVI', 'DCCCXXXVII', 'DCCCXXXVIII', 'DCCCXXXIX', 'DCCCXL', 'DCCCXLI', 'DCCCXLII', 'DCCCXLIII', 'DCCCXLIV', 'DCCCVL', 'DCCCVLI', 'DCCCVLII', 'DCCCVLIII', 'DCCCIL', 'DCCCL', 'DCCCLI', 'DCCCLII', 'DCCCLIII', 'DCCCLIV', 'DCCCLV', 'DCCCLVI', 'DCCCLVII', 'DCCCLVIII', 'DCCCLIX', 'DCCCLX', 'DCCCLXI', 'DCCCLXII', 'DCCCLXIII', 'DCCCLXIV', 'DCCCLXV', 'DCCCLXVI', 'DCCCLXVII', 'DCCCLXVIII', 'DCCCLXIX', 'DCCCLXX', 'DCCCLXXI', 'DCCCLXXII', 'DCCCLXXIII', 'DCCCLXXIV', 'DCCCLXXV', 'DCCCLXXVI', 'DCCCLXXVII', 'DCCCLXXVIII', 'DCCCLXXIX', 'DCCCLXXX', 'DCCCLXXXI', 'DCCCLXXXII', 'DCCCLXXXIII', 'DCCCLXXXIV', 'DCCCLXXXV', 'DCCCLXXXVI', 'DCCCLXXXVII', 'DCCCLXXXVIII', 'DCCCLXXXIX', 'DCCCXC', 'DCCCXCI', 'DCCCXCII', 'DCCCXCIII', 'DCCCXCIV', 'DCCCVC', 'DCCCVCI', 'DCCCVCII', 'DCCCVCIII', 'DCCCIC', 'CM', 'CMI', 'CMII', 'CMIII', 'CMIV', 'CMV', 'CMVI', 'CMVII', 'CMVIII', 'CMIX', 'CMX', 'CMXI', 'CMXII', 'CMXIII', 'CMXIV', 'CMXV', 'CMXVI', 'CMXVII', 'CMXVIII', 'CMXIX', 'CMXX', 'CMXXI', 'CMXXII', 'CMXXIII', 'CMXXIV', 'CMXXV', 'CMXXVI', 'CMXXVII', 'CMXXVIII', 'CMXXIX', 'CMXXX', 'CMXXXI', 'CMXXXII', 'CMXXXIII', 'CMXXXIV', 'CMXXXV', 'CMXXXVI', 'CMXXXVII', 'CMXXXVIII', 'CMXXXIX', 'CMXL', 'CMXLI', 'CMXLII', 'CMXLIII', 'CMXLIV', 'CMVL', 'CMVLI', 'CMVLII', 'CMVLIII', 'CMIL', 'LM', 'LMI', 'LMII', 'LMIII', 'LMIV', 'LMV', 'LMVI', 'LMVII', 'LMVIII', 'LMIX', 'LMX', 'LMXI', 'LMXII', 'LMXIII', 'LMXIV', 'LMXV', 'LMXVI', 'LMXVII', 'LMXVIII', 'LMXIX', 'LMXX', 'LMXXI', 'LMXXII', 'LMXXIII', 'LMXXIV', 'LMXXV', 'LMXXVI', 'LMXXVII', 'LMXXVIII', 'LMXXIX', 'LMXXX', 'LMXXXI', 'LMXXXII', 'LMXXXIII', 'LMXXXIV', 'LMXXXV', 'LMXXXVI', 'LMXXXVII', 'LMXXXVIII', 'LMXXXIX', 'XM', 'XMI', 'XMII', 'XMIII', 'XMIV', 'XMV', 'XMVI', 'XMVII', 'XMVIII', 'XMIX', 'M', 'MI', 'MII', 'MIII', 'MIV', 'MV', 'MVI', 'MVII', 'MVIII', 'MIX', 'MX', 'MXI', 'MXII', 'MXIII', 'MXIV', 'MXV', 'MXVI', 'MXVII', 'MXVIII', 'MXIX', 'MXX', 'MXXI', 'MXXII', 'MXXIII', 'MXXIV', 'MXXV', 'MXXVI', 'MXXVII', 'MXXVIII', 'MXXIX', 'MXXX', 'MXXXI', 'MXXXII', 'MXXXIII', 'MXXXIV', 'MXXXV', 'MXXXVI', 'MXXXVII', 'MXXXVIII', 'MXXXIX', 'MXL', 'MXLI', 'MXLII', 'MXLIII', 'MXLIV', 'MVL', 'MVLI', 'MVLII', 'MVLIII', 'MIL', 'ML', 'MLI', 'MLII', 'MLIII', 'MLIV', 'MLV', 'MLVI', 'MLVII', 'MLVIII', 'MLIX', 'MLX', 'MLXI', 'MLXII', 'MLXIII', 'MLXIV', 'MLXV', 'MLXVI', 'MLXVII', 'MLXVIII', 'MLXIX', 'MLXX', 'MLXXI', 'MLXXII', 'MLXXIII', 'MLXXIV', 'MLXXV', 'MLXXVI', 'MLXXVII', 'MLXXVIII', 'MLXXIX', 'MLXXX', 'MLXXXI', 'MLXXXII', 'MLXXXIII', 'MLXXXIV', 'MLXXXV', 'MLXXXVI', 'MLXXXVII', 'MLXXXVIII', 'MLXXXIX', 'MXC', 'MXCI', 'MXCII', 'MXCIII', 'MXCIV', 'MVC', 'MVCI', 'MVCII', 'MVCIII', 'MIC', 'MC', 'MCI', 'MCII', 'MCIII', 'MCIV', 'MCV', 'MCVI', 'MCVII', 'MCVIII', 'MCIX', 'MCX', 'MCXI', 'MCXII', 'MCXIII', 'MCXIV', 'MCXV', 'MCXVI', 'MCXVII', 'MCXVIII', 'MCXIX', 'MCXX', 'MCXXI', 'MCXXII', 'MCXXIII', 'MCXXIV', 'MCXXV', 'MCXXVI', 'MCXXVII', 'MCXXVIII', 'MCXXIX', 'MCXXX', 'MCXXXI', 'MCXXXII', 'MCXXXIII', 'MCXXXIV', 'MCXXXV', 'MCXXXVI', 'MCXXXVII', 'MCXXXVIII', 'MCXXXIX', 'MCXL', 'MCXLI', 'MCXLII', 'MCXLIII', 'MCXLIV', 'MCVL', 'MCVLI', 'MCVLII', 'MCVLIII', 'MCIL', 'MCL', 'MCLI', 'MCLII', 'MCLIII', 'MCLIV', 'MCLV', 'MCLVI', 'MCLVII', 'MCLVIII', 'MCLIX', 'MCLX', 'MCLXI', 'MCLXII', 'MCLXIII', 'MCLXIV', 'MCLXV', 'MCLXVI', 'MCLXVII', 'MCLXVIII', 'MCLXIX', 'MCLXX', 'MCLXXI', 'MCLXXII', 'MCLXXIII', 'MCLXXIV', 'MCLXXV', 'MCLXXVI', 'MCLXXVII', 'MCLXXVIII', 'MCLXXIX', 'MCLXXX', 'MCLXXXI', 'MCLXXXII', 'MCLXXXIII', 'MCLXXXIV', 'MCLXXXV', 'MCLXXXVI', 'MCLXXXVII', 'MCLXXXVIII', 'MCLXXXIX', 'MCXC', 'MCXCI', 'MCXCII', 'MCXCIII', 'MCXCIV', 'MCVC', 'MCVCI', 'MCVCII', 'MCVCIII', 'MCIC', 'MCC', 'MCCI', 'MCCII', 'MCCIII', 'MCCIV', 'MCCV', 'MCCVI', 'MCCVII', 'MCCVIII', 'MCCIX', 'MCCX', 'MCCXI', 'MCCXII', 'MCCXIII', 'MCCXIV', 'MCCXV', 'MCCXVI', 'MCCXVII', 'MCCXVIII', 'MCCXIX', 'MCCXX', 'MCCXXI', 'MCCXXII', 'MCCXXIII', 'MCCXXIV', 'MCCXXV', 'MCCXXVI', 'MCCXXVII', 'MCCXXVIII', 'MCCXXIX', 'MCCXXX', 'MCCXXXI', 'MCCXXXII', 'MCCXXXIII', 'MCCXXXIV', 'MCCXXXV', 'MCCXXXVI', 'MCCXXXVII', 'MCCXXXVIII', 'MCCXXXIX', 'MCCXL', 'MCCXLI', 'MCCXLII', 'MCCXLIII', 'MCCXLIV', 'MCCVL', 'MCCVLI', 'MCCVLII', 'MCCVLIII', 'MCCIL', 'MCCL', 'MCCLI', 'MCCLII', 'MCCLIII', 'MCCLIV', 'MCCLV', 'MCCLVI', 'MCCLVII', 'MCCLVIII', 'MCCLIX', 'MCCLX', 'MCCLXI', 'MCCLXII', 'MCCLXIII', 'MCCLXIV', 'MCCLXV', 'MCCLXVI', 'MCCLXVII', 'MCCLXVIII', 'MCCLXIX', 'MCCLXX', 'MCCLXXI', 'MCCLXXII', 'MCCLXXIII', 'MCCLXXIV', 'MCCLXXV', 'MCCLXXVI', 'MCCLXXVII', 'MCCLXXVIII', 'MCCLXXIX', 'MCCLXXX', 'MCCLXXXI', 'MCCLXXXII', 'MCCLXXXIII', 'MCCLXXXIV', 'MCCLXXXV', 'MCCLXXXVI', 'MCCLXXXVII', 'MCCLXXXVIII', 'MCCLXXXIX', 'MCCXC', 'MCCXCI', 'MCCXCII', 'MCCXCIII', 'MCCXCIV', 'MCCVC', 'MCCVCI', 'MCCVCII', 'MCCVCIII', 'MCCIC', 'MCCC', 'MCCCI', 'MCCCII', 'MCCCIII', 'MCCCIV', 'MCCCV', 'MCCCVI', 'MCCCVII', 'MCCCVIII', 'MCCCIX', 'MCCCX', 'MCCCXI', 'MCCCXII', 'MCCCXIII', 'MCCCXIV', 'MCCCXV', 'MCCCXVI', 'MCCCXVII', 'MCCCXVIII', 'MCCCXIX', 'MCCCXX', 'MCCCXXI', 'MCCCXXII', 'MCCCXXIII', 'MCCCXXIV', 'MCCCXXV', 'MCCCXXVI', 'MCCCXXVII', 'MCCCXXVIII', 'MCCCXXIX', 'MCCCXXX', 'MCCCXXXI', 'MCCCXXXII', 'MCCCXXXIII', 'MCCCXXXIV', 'MCCCXXXV', 'MCCCXXXVI', 'MCCCXXXVII', 'MCCCXXXVIII', 'MCCCXXXIX', 'MCCCXL', 'MCCCXLI', 'MCCCXLII', 'MCCCXLIII', 'MCCCXLIV', 'MCCCVL', 'MCCCVLI', 'MCCCVLII', 'MCCCVLIII', 'MCCCIL', 'MCCCL', 'MCCCLI', 'MCCCLII', 'MCCCLIII', 'MCCCLIV', 'MCCCLV', 'MCCCLVI', 'MCCCLVII', 'MCCCLVIII', 'MCCCLIX', 'MCCCLX', 'MCCCLXI', 'MCCCLXII', 'MCCCLXIII', 'MCCCLXIV', 'MCCCLXV', 'MCCCLXVI', 'MCCCLXVII', 'MCCCLXVIII', 'MCCCLXIX', 'MCCCLXX', 'MCCCLXXI', 'MCCCLXXII', 'MCCCLXXIII', 'MCCCLXXIV', 'MCCCLXXV', 'MCCCLXXVI', 'MCCCLXXVII', 'MCCCLXXVIII', 'MCCCLXXIX', 'MCCCLXXX', 'MCCCLXXXI', 'MCCCLXXXII', 'MCCCLXXXIII', 'MCCCLXXXIV', 'MCCCLXXXV', 'MCCCLXXXVI', 'MCCCLXXXVII', 'MCCCLXXXVIII', 'MCCCLXXXIX', 'MCCCXC', 'MCCCXCI', 'MCCCXCII', 'MCCCXCIII', 'MCCCXCIV', 'MCCCVC', 'MCCCVCI', 'MCCCVCII', 'MCCCVCIII', 'MCCCIC', 'MCD', 'MCDI', 'MCDII', 'MCDIII', 'MCDIV', 'MCDV', 'MCDVI', 'MCDVII', 'MCDVIII', 'MCDIX', 'MCDX', 'MCDXI', 'MCDXII', 'MCDXIII', 'MCDXIV', 'MCDXV', 'MCDXVI', 'MCDXVII', 'MCDXVIII', 'MCDXIX', 'MCDXX', 'MCDXXI', 'MCDXXII', 'MCDXXIII', 'MCDXXIV', 'MCDXXV', 'MCDXXVI', 'MCDXXVII', 'MCDXXVIII', 'MCDXXIX', 'MCDXXX', 'MCDXXXI', 'MCDXXXII', 'MCDXXXIII', 'MCDXXXIV', 'MCDXXXV', 'MCDXXXVI', 'MCDXXXVII', 'MCDXXXVIII', 'MCDXXXIX', 'MCDXL', 'MCDXLI', 'MCDXLII', 'MCDXLIII', 'MCDXLIV', 'MCDVL', 'MCDVLI', 'MCDVLII', 'MCDVLIII', 'MCDIL', 'MLD', 'MLDI', 'MLDII', 'MLDIII', 'MLDIV', 'MLDV', 'MLDVI', 'MLDVII', 'MLDVIII', 'MLDIX', 'MLDX', 'MLDXI', 'MLDXII', 'MLDXIII', 'MLDXIV', 'MLDXV', 'MLDXVI', 'MLDXVII', 'MLDXVIII', 'MLDXIX', 'MLDXX', 'MLDXXI', 'MLDXXII', 'MLDXXIII', 'MLDXXIV', 'MLDXXV', 'MLDXXVI', 'MLDXXVII', 'MLDXXVIII', 'MLDXXIX', 'MLDXXX', 'MLDXXXI', 'MLDXXXII', 'MLDXXXIII', 'MLDXXXIV', 'MLDXXXV', 'MLDXXXVI', 'MLDXXXVII', 'MLDXXXVIII', 'MLDXXXIX', 'MXD', 'MXDI', 'MXDII', 'MXDIII', 'MXDIV', 'MXDV', 'MXDVI', 'MXDVII', 'MXDVIII', 'MXDIX', 'MD', 'MDI', 'MDII', 'MDIII', 'MDIV', 'MDV', 'MDVI', 'MDVII', 'MDVIII', 'MDIX', 'MDX', 'MDXI', 'MDXII', 'MDXIII', 'MDXIV', 'MDXV', 'MDXVI', 'MDXVII', 'MDXVIII', 'MDXIX', 'MDXX', 'MDXXI', 'MDXXII', 'MDXXIII', 'MDXXIV', 'MDXXV', 'MDXXVI', 'MDXXVII', 'MDXXVIII', 'MDXXIX', 'MDXXX', 'MDXXXI', 'MDXXXII', 'MDXXXIII', 'MDXXXIV', 'MDXXXV', 'MDXXXVI', 'MDXXXVII', 'MDXXXVIII', 'MDXXXIX', 'MDXL', 'MDXLI', 'MDXLII', 'MDXLIII', 'MDXLIV', 'MDVL', 'MDVLI', 'MDVLII', 'MDVLIII', 'MDIL', 'MDL', 'MDLI', 'MDLII', 'MDLIII', 'MDLIV', 'MDLV', 'MDLVI', 'MDLVII', 'MDLVIII', 'MDLIX', 'MDLX', 'MDLXI', 'MDLXII', 'MDLXIII', 'MDLXIV', 'MDLXV', 'MDLXVI', 'MDLXVII', 'MDLXVIII', 'MDLXIX', 'MDLXX', 'MDLXXI', 'MDLXXII', 'MDLXXIII', 'MDLXXIV', 'MDLXXV', 'MDLXXVI', 'MDLXXVII', 'MDLXXVIII', 'MDLXXIX', 'MDLXXX', 'MDLXXXI', 'MDLXXXII', 'MDLXXXIII', 'MDLXXXIV', 'MDLXXXV', 'MDLXXXVI', 'MDLXXXVII', 'MDLXXXVIII', 'MDLXXXIX', 'MDXC', 'MDXCI', 'MDXCII', 'MDXCIII', 'MDXCIV', 'MDVC', 'MDVCI', 'MDVCII', 'MDVCIII', 'MDIC', 'MDC', 'MDCI', 'MDCII', 'MDCIII', 'MDCIV', 'MDCV', 'MDCVI', 'MDCVII', 'MDCVIII', 'MDCIX', 'MDCX', 'MDCXI', 'MDCXII', 'MDCXIII', 'MDCXIV', 'MDCXV', 'MDCXVI', 'MDCXVII', 'MDCXVIII', 'MDCXIX', 'MDCXX', 'MDCXXI', 'MDCXXII', 'MDCXXIII', 'MDCXXIV', 'MDCXXV', 'MDCXXVI', 'MDCXXVII', 'MDCXXVIII', 'MDCXXIX', 'MDCXXX', 'MDCXXXI', 'MDCXXXII', 'MDCXXXIII', 'MDCXXXIV', 'MDCXXXV', 'MDCXXXVI', 'MDCXXXVII', 'MDCXXXVIII', 'MDCXXXIX', 'MDCXL', 'MDCXLI', 'MDCXLII', 'MDCXLIII', 'MDCXLIV', 'MDCVL', 'MDCVLI', 'MDCVLII', 'MDCVLIII', 'MDCIL', 'MDCL', 'MDCLI', 'MDCLII', 'MDCLIII', 'MDCLIV', 'MDCLV', 'MDCLVI', 'MDCLVII', 'MDCLVIII', 'MDCLIX', 'MDCLX', 'MDCLXI', 'MDCLXII', 'MDCLXIII', 'MDCLXIV', 'MDCLXV', 'MDCLXVI', 'MDCLXVII', 'MDCLXVIII', 'MDCLXIX', 'MDCLXX', 'MDCLXXI', 'MDCLXXII', 'MDCLXXIII', 'MDCLXXIV', 'MDCLXXV', 'MDCLXXVI', 'MDCLXXVII', 'MDCLXXVIII', 'MDCLXXIX', 'MDCLXXX', 'MDCLXXXI', 'MDCLXXXII', 'MDCLXXXIII', 'MDCLXXXIV', 'MDCLXXXV', 'MDCLXXXVI', 'MDCLXXXVII', 'MDCLXXXVIII', 'MDCLXXXIX', 'MDCXC', 'MDCXCI', 'MDCXCII', 'MDCXCIII', 'MDCXCIV', 'MDCVC', 'MDCVCI', 'MDCVCII', 'MDCVCIII', 'MDCIC', 'MDCC', 'MDCCI', 'MDCCII', 'MDCCIII', 'MDCCIV', 'MDCCV', 'MDCCVI', 'MDCCVII', 'MDCCVIII', 'MDCCIX', 'MDCCX', 'MDCCXI', 'MDCCXII', 'MDCCXIII', 'MDCCXIV', 'MDCCXV', 'MDCCXVI', 'MDCCXVII', 'MDCCXVIII', 'MDCCXIX', 'MDCCXX', 'MDCCXXI', 'MDCCXXII', 'MDCCXXIII', 'MDCCXXIV', 'MDCCXXV', 'MDCCXXVI', 'MDCCXXVII', 'MDCCXXVIII', 'MDCCXXIX', 'MDCCXXX', 'MDCCXXXI', 'MDCCXXXII', 'MDCCXXXIII', 'MDCCXXXIV', 'MDCCXXXV', 'MDCCXXXVI', 'MDCCXXXVII', 'MDCCXXXVIII', 'MDCCXXXIX', 'MDCCXL', 'MDCCXLI', 'MDCCXLII', 'MDCCXLIII', 'MDCCXLIV', 'MDCCVL', 'MDCCVLI', 'MDCCVLII', 'MDCCVLIII', 'MDCCIL', 'MDCCL', 'MDCCLI', 'MDCCLII', 'MDCCLIII', 'MDCCLIV', 'MDCCLV', 'MDCCLVI', 'MDCCLVII', 'MDCCLVIII', 'MDCCLIX', 'MDCCLX', 'MDCCLXI', 'MDCCLXII', 'MDCCLXIII', 'MDCCLXIV', 'MDCCLXV', 'MDCCLXVI', 'MDCCLXVII', 'MDCCLXVIII', 'MDCCLXIX', 'MDCCLXX', 'MDCCLXXI', 'MDCCLXXII', 'MDCCLXXIII', 'MDCCLXXIV', 'MDCCLXXV', 'MDCCLXXVI', 'MDCCLXXVII', 'MDCCLXXVIII', 'MDCCLXXIX', 'MDCCLXXX', 'MDCCLXXXI', 'MDCCLXXXII', 'MDCCLXXXIII', 'MDCCLXXXIV', 'MDCCLXXXV', 'MDCCLXXXVI', 'MDCCLXXXVII', 'MDCCLXXXVIII', 'MDCCLXXXIX', 'MDCCXC', 'MDCCXCI', 'MDCCXCII', 'MDCCXCIII', 'MDCCXCIV', 'MDCCVC', 'MDCCVCI', 'MDCCVCII', 'MDCCVCIII', 'MDCCIC', 'MDCCC', 'MDCCCI', 'MDCCCII', 'MDCCCIII', 'MDCCCIV', 'MDCCCV', 'MDCCCVI', 'MDCCCVII', 'MDCCCVIII', 'MDCCCIX', 'MDCCCX', 'MDCCCXI', 'MDCCCXII', 'MDCCCXIII', 'MDCCCXIV', 'MDCCCXV', 'MDCCCXVI', 'MDCCCXVII', 'MDCCCXVIII', 'MDCCCXIX', 'MDCCCXX', 'MDCCCXXI', 'MDCCCXXII', 'MDCCCXXIII', 'MDCCCXXIV', 'MDCCCXXV', 'MDCCCXXVI', 'MDCCCXXVII', 'MDCCCXXVIII', 'MDCCCXXIX', 'MDCCCXXX', 'MDCCCXXXI', 'MDCCCXXXII', 'MDCCCXXXIII', 'MDCCCXXXIV', 'MDCCCXXXV', 'MDCCCXXXVI', 'MDCCCXXXVII', 'MDCCCXXXVIII', 'MDCCCXXXIX', 'MDCCCXL', 'MDCCCXLI', 'MDCCCXLII', 'MDCCCXLIII', 'MDCCCXLIV', 'MDCCCVL', 'MDCCCVLI', 'MDCCCVLII', 'MDCCCVLIII', 'MDCCCIL', 'MDCCCL', 'MDCCCLI', 'MDCCCLII', 'MDCCCLIII', 'MDCCCLIV', 'MDCCCLV', 'MDCCCLVI', 'MDCCCLVII', 'MDCCCLVIII', 'MDCCCLIX', 'MDCCCLX', 'MDCCCLXI', 'MDCCCLXII', 'MDCCCLXIII', 'MDCCCLXIV', 'MDCCCLXV', 'MDCCCLXVI', 'MDCCCLXVII', 'MDCCCLXVIII', 'MDCCCLXIX', 'MDCCCLXX', 'MDCCCLXXI', 'MDCCCLXXII', 'MDCCCLXXIII', 'MDCCCLXXIV', 'MDCCCLXXV', 'MDCCCLXXVI', 'MDCCCLXXVII', 'MDCCCLXXVIII', 'MDCCCLXXIX', 'MDCCCLXXX', 'MDCCCLXXXI', 'MDCCCLXXXII', 'MDCCCLXXXIII', 'MDCCCLXXXIV', 'MDCCCLXXXV', 'MDCCCLXXXVI', 'MDCCCLXXXVII', 'MDCCCLXXXVIII', 'MDCCCLXXXIX', 'MDCCCXC', 'MDCCCXCI', 'MDCCCXCII', 'MDCCCXCIII', 'MDCCCXCIV', 'MDCCCVC', 'MDCCCVCI', 'MDCCCVCII', 'MDCCCVCIII', 'MDCCCIC', 'MCM', 'MCMI', 'MCMII', 'MCMIII', 'MCMIV', 'MCMV', 'MCMVI', 'MCMVII', 'MCMVIII', 'MCMIX', 'MCMX', 'MCMXI', 'MCMXII', 'MCMXIII', 'MCMXIV', 'MCMXV', 'MCMXVI', 'MCMXVII', 'MCMXVIII', 'MCMXIX', 'MCMXX', 'MCMXXI', 'MCMXXII', 'MCMXXIII', 'MCMXXIV', 'MCMXXV', 'MCMXXVI', 'MCMXXVII', 'MCMXXVIII', 'MCMXXIX', 'MCMXXX', 'MCMXXXI', 'MCMXXXII', 'MCMXXXIII', 'MCMXXXIV', 'MCMXXXV', 'MCMXXXVI', 'MCMXXXVII', 'MCMXXXVIII', 'MCMXXXIX', 'MCMXL', 'MCMXLI', 'MCMXLII', 'MCMXLIII', 'MCMXLIV', 'MCMVL', 'MCMVLI', 'MCMVLII', 'MCMVLIII', 'MCMIL', 'MLM', 'MLMI', 'MLMII', 'MLMIII', 'MLMIV', 'MLMV', 'MLMVI', 'MLMVII', 'MLMVIII', 'MLMIX', 'MLMX', 'MLMXI', 'MLMXII', 'MLMXIII', 'MLMXIV', 'MLMXV', 'MLMXVI', 'MLMXVII', 'MLMXVIII', 'MLMXIX', 'MLMXX', 'MLMXXI', 'MLMXXII', 'MLMXXIII', 'MLMXXIV', 'MLMXXV', 'MLMXXVI', 'MLMXXVII', 'MLMXXVIII', 'MLMXXIX', 'MLMXXX', 'MLMXXXI', 'MLMXXXII', 'MLMXXXIII', 'MLMXXXIV', 'MLMXXXV', 'MLMXXXVI', 'MLMXXXVII', 'MLMXXXVIII', 'MLMXXXIX', 'MXM', 'MXMI', 'MXMII', 'MXMIII', 'MXMIV', 'MXMV', 'MXMVI', 'MXMVII', 'MXMVIII', 'MXMIX', 'MM', 'MMI', 'MMII', 'MMIII', 'MMIV', 'MMV', 'MMVI', 'MMVII', 'MMVIII', 'MMIX', 'MMX', 'MMXI', 'MMXII', 'MMXIII', 'MMXIV', 'MMXV', 'MMXVI', 'MMXVII', 'MMXVIII', 'MMXIX', 'MMXX', 'MMXXI', 'MMXXII', 'MMXXIII', 'MMXXIV', 'MMXXV', 'MMXXVI', 'MMXXVII', 'MMXXVIII', 'MMXXIX', 'MMXXX', 'MMXXXI', 'MMXXXII', 'MMXXXIII', 'MMXXXIV', 'MMXXXV', 'MMXXXVI', 'MMXXXVII', 'MMXXXVIII', 'MMXXXIX', 'MMXL', 'MMXLI', 'MMXLII', 'MMXLIII', 'MMXLIV', 'MMVL', 'MMVLI', 'MMVLII', 'MMVLIII', 'MMIL', 'MML', 'MMLI', 'MMLII', 'MMLIII', 'MMLIV', 'MMLV', 'MMLVI', 'MMLVII', 'MMLVIII', 'MMLIX', 'MMLX', 'MMLXI', 'MMLXII', 'MMLXIII', 'MMLXIV', 'MMLXV', 'MMLXVI', 'MMLXVII', 'MMLXVIII', 'MMLXIX', 'MMLXX', 'MMLXXI', 'MMLXXII', 'MMLXXIII', 'MMLXXIV', 'MMLXXV', 'MMLXXVI', 'MMLXXVII', 'MMLXXVIII', 'MMLXXIX', 'MMLXXX', 'MMLXXXI', 'MMLXXXII', 'MMLXXXIII', 'MMLXXXIV', 'MMLXXXV', 'MMLXXXVI', 'MMLXXXVII', 'MMLXXXVIII', 'MMLXXXIX', 'MMXC', 'MMXCI', 'MMXCII', 'MMXCIII', 'MMXCIV', 'MMVC', 'MMVCI', 'MMVCII', 'MMVCIII', 'MMIC', 'MMC', 'MMCI', 'MMCII', 'MMCIII', 'MMCIV', 'MMCV', 'MMCVI', 'MMCVII', 'MMCVIII', 'MMCIX', 'MMCX', 'MMCXI', 'MMCXII', 'MMCXIII', 'MMCXIV', 'MMCXV', 'MMCXVI', 'MMCXVII', 'MMCXVIII', 'MMCXIX', 'MMCXX', 'MMCXXI', 'MMCXXII', 'MMCXXIII', 'MMCXXIV', 'MMCXXV', 'MMCXXVI', 'MMCXXVII', 'MMCXXVIII', 'MMCXXIX', 'MMCXXX', 'MMCXXXI', 'MMCXXXII', 'MMCXXXIII', 'MMCXXXIV', 'MMCXXXV', 'MMCXXXVI', 'MMCXXXVII', 'MMCXXXVIII', 'MMCXXXIX', 'MMCXL', 'MMCXLI', 'MMCXLII', 'MMCXLIII', 'MMCXLIV', 'MMCVL', 'MMCVLI', 'MMCVLII', 'MMCVLIII', 'MMCIL', 'MMCL', 'MMCLI', 'MMCLII', 'MMCLIII', 'MMCLIV', 'MMCLV', 'MMCLVI', 'MMCLVII', 'MMCLVIII', 'MMCLIX', 'MMCLX', 'MMCLXI', 'MMCLXII', 'MMCLXIII', 'MMCLXIV', 'MMCLXV', 'MMCLXVI', 'MMCLXVII', 'MMCLXVIII', 'MMCLXIX', 'MMCLXX', 'MMCLXXI', 'MMCLXXII', 'MMCLXXIII', 'MMCLXXIV', 'MMCLXXV', 'MMCLXXVI', 'MMCLXXVII', 'MMCLXXVIII', 'MMCLXXIX', 'MMCLXXX', 'MMCLXXXI', 'MMCLXXXII', 'MMCLXXXIII', 'MMCLXXXIV', 'MMCLXXXV', 'MMCLXXXVI', 'MMCLXXXVII', 'MMCLXXXVIII', 'MMCLXXXIX', 'MMCXC', 'MMCXCI', 'MMCXCII', 'MMCXCIII', 'MMCXCIV', 'MMCVC', 'MMCVCI', 'MMCVCII', 'MMCVCIII', 'MMCIC', 'MMCC', 'MMCCI', 'MMCCII', 'MMCCIII', 'MMCCIV', 'MMCCV', 'MMCCVI', 'MMCCVII', 'MMCCVIII', 'MMCCIX', 'MMCCX', 'MMCCXI', 'MMCCXII', 'MMCCXIII', 'MMCCXIV', 'MMCCXV', 'MMCCXVI', 'MMCCXVII', 'MMCCXVIII', 'MMCCXIX', 'MMCCXX', 'MMCCXXI', 'MMCCXXII', 'MMCCXXIII', 'MMCCXXIV', 'MMCCXXV', 'MMCCXXVI', 'MMCCXXVII', 'MMCCXXVIII', 'MMCCXXIX', 'MMCCXXX', 'MMCCXXXI', 'MMCCXXXII', 'MMCCXXXIII', 'MMCCXXXIV', 'MMCCXXXV', 'MMCCXXXVI', 'MMCCXXXVII', 'MMCCXXXVIII', 'MMCCXXXIX', 'MMCCXL', 'MMCCXLI', 'MMCCXLII', 'MMCCXLIII', 'MMCCXLIV', 'MMCCVL', 'MMCCVLI', 'MMCCVLII', 'MMCCVLIII', 'MMCCIL', 'MMCCL', 'MMCCLI', 'MMCCLII', 'MMCCLIII', 'MMCCLIV', 'MMCCLV', 'MMCCLVI', 'MMCCLVII', 'MMCCLVIII', 'MMCCLIX', 'MMCCLX', 'MMCCLXI', 'MMCCLXII', 'MMCCLXIII', 'MMCCLXIV', 'MMCCLXV', 'MMCCLXVI', 'MMCCLXVII', 'MMCCLXVIII', 'MMCCLXIX', 'MMCCLXX', 'MMCCLXXI', 'MMCCLXXII', 'MMCCLXXIII', 'MMCCLXXIV', 'MMCCLXXV', 'MMCCLXXVI', 'MMCCLXXVII', 'MMCCLXXVIII', 'MMCCLXXIX', 'MMCCLXXX', 'MMCCLXXXI', 'MMCCLXXXII', 'MMCCLXXXIII', 'MMCCLXXXIV', 'MMCCLXXXV', 'MMCCLXXXVI', 'MMCCLXXXVII', 'MMCCLXXXVIII', 'MMCCLXXXIX', 'MMCCXC', 'MMCCXCI', 'MMCCXCII', 'MMCCXCIII', 'MMCCXCIV', 'MMCCVC', 'MMCCVCI', 'MMCCVCII', 'MMCCVCIII', 'MMCCIC', 'MMCCC', 'MMCCCI', 'MMCCCII', 'MMCCCIII', 'MMCCCIV', 'MMCCCV', 'MMCCCVI', 'MMCCCVII', 'MMCCCVIII', 'MMCCCIX', 'MMCCCX', 'MMCCCXI', 'MMCCCXII', 'MMCCCXIII', 'MMCCCXIV', 'MMCCCXV', 'MMCCCXVI', 'MMCCCXVII', 'MMCCCXVIII', 'MMCCCXIX', 'MMCCCXX', 'MMCCCXXI', 'MMCCCXXII', 'MMCCCXXIII', 'MMCCCXXIV', 'MMCCCXXV', 'MMCCCXXVI', 'MMCCCXXVII', 'MMCCCXXVIII', 'MMCCCXXIX', 'MMCCCXXX', 'MMCCCXXXI', 'MMCCCXXXII', 'MMCCCXXXIII', 'MMCCCXXXIV', 'MMCCCXXXV', 'MMCCCXXXVI', 'MMCCCXXXVII', 'MMCCCXXXVIII', 'MMCCCXXXIX', 'MMCCCXL', 'MMCCCXLI', 'MMCCCXLII', 'MMCCCXLIII', 'MMCCCXLIV', 'MMCCCVL', 'MMCCCVLI', 'MMCCCVLII', 'MMCCCVLIII', 'MMCCCIL', 'MMCCCL', 'MMCCCLI', 'MMCCCLII', 'MMCCCLIII', 'MMCCCLIV', 'MMCCCLV', 'MMCCCLVI', 'MMCCCLVII', 'MMCCCLVIII', 'MMCCCLIX', 'MMCCCLX', 'MMCCCLXI', 'MMCCCLXII', 'MMCCCLXIII', 'MMCCCLXIV', 'MMCCCLXV', 'MMCCCLXVI', 'MMCCCLXVII', 'MMCCCLXVIII', 'MMCCCLXIX', 'MMCCCLXX', 'MMCCCLXXI', 'MMCCCLXXII', 'MMCCCLXXIII', 'MMCCCLXXIV', 'MMCCCLXXV', 'MMCCCLXXVI', 'MMCCCLXXVII', 'MMCCCLXXVIII', 'MMCCCLXXIX', 'MMCCCLXXX', 'MMCCCLXXXI', 'MMCCCLXXXII', 'MMCCCLXXXIII', 'MMCCCLXXXIV', 'MMCCCLXXXV', 'MMCCCLXXXVI', 'MMCCCLXXXVII', 'MMCCCLXXXVIII', 'MMCCCLXXXIX', 'MMCCCXC', 'MMCCCXCI', 'MMCCCXCII', 'MMCCCXCIII', 'MMCCCXCIV', 'MMCCCVC', 'MMCCCVCI', 'MMCCCVCII', 'MMCCCVCIII', 'MMCCCIC', 'MMCD', 'MMCDI', 'MMCDII', 'MMCDIII', 'MMCDIV', 'MMCDV', 'MMCDVI', 'MMCDVII', 'MMCDVIII', 'MMCDIX', 'MMCDX', 'MMCDXI', 'MMCDXII', 'MMCDXIII', 'MMCDXIV', 'MMCDXV', 'MMCDXVI', 'MMCDXVII', 'MMCDXVIII', 'MMCDXIX', 'MMCDXX', 'MMCDXXI', 'MMCDXXII', 'MMCDXXIII', 'MMCDXXIV', 'MMCDXXV', 'MMCDXXVI', 'MMCDXXVII', 'MMCDXXVIII', 'MMCDXXIX', 'MMCDXXX', 'MMCDXXXI', 'MMCDXXXII', 'MMCDXXXIII', 'MMCDXXXIV', 'MMCDXXXV', 'MMCDXXXVI', 'MMCDXXXVII', 'MMCDXXXVIII', 'MMCDXXXIX', 'MMCDXL', 'MMCDXLI', 'MMCDXLII', 'MMCDXLIII', 'MMCDXLIV', 'MMCDVL', 'MMCDVLI', 'MMCDVLII', 'MMCDVLIII', 'MMCDIL', 'MMLD', 'MMLDI', 'MMLDII', 'MMLDIII', 'MMLDIV', 'MMLDV', 'MMLDVI', 'MMLDVII', 'MMLDVIII', 'MMLDIX', 'MMLDX', 'MMLDXI', 'MMLDXII', 'MMLDXIII', 'MMLDXIV', 'MMLDXV', 'MMLDXVI', 'MMLDXVII', 'MMLDXVIII', 'MMLDXIX', 'MMLDXX', 'MMLDXXI', 'MMLDXXII', 'MMLDXXIII', 'MMLDXXIV', 'MMLDXXV', 'MMLDXXVI', 'MMLDXXVII', 'MMLDXXVIII', 'MMLDXXIX', 'MMLDXXX', 'MMLDXXXI', 'MMLDXXXII', 'MMLDXXXIII', 'MMLDXXXIV', 'MMLDXXXV', 'MMLDXXXVI', 'MMLDXXXVII', 'MMLDXXXVIII', 'MMLDXXXIX', 'MMXD', 'MMXDI', 'MMXDII', 'MMXDIII', 'MMXDIV', 'MMXDV', 'MMXDVI', 'MMXDVII', 'MMXDVIII', 'MMXDIX', 'MMD', 'MMDI', 'MMDII', 'MMDIII', 'MMDIV', 'MMDV', 'MMDVI', 'MMDVII', 'MMDVIII', 'MMDIX', 'MMDX', 'MMDXI', 'MMDXII', 'MMDXIII', 'MMDXIV', 'MMDXV', 'MMDXVI', 'MMDXVII', 'MMDXVIII', 'MMDXIX', 'MMDXX', 'MMDXXI', 'MMDXXII', 'MMDXXIII', 'MMDXXIV', 'MMDXXV', 'MMDXXVI', 'MMDXXVII', 'MMDXXVIII', 'MMDXXIX', 'MMDXXX', 'MMDXXXI', 'MMDXXXII', 'MMDXXXIII', 'MMDXXXIV', 'MMDXXXV', 'MMDXXXVI', 'MMDXXXVII', 'MMDXXXVIII', 'MMDXXXIX', 'MMDXL', 'MMDXLI', 'MMDXLII', 'MMDXLIII', 'MMDXLIV', 'MMDVL', 'MMDVLI', 'MMDVLII', 'MMDVLIII', 'MMDIL', 'MMDL', 'MMDLI', 'MMDLII', 'MMDLIII', 'MMDLIV', 'MMDLV', 'MMDLVI', 'MMDLVII', 'MMDLVIII', 'MMDLIX', 'MMDLX', 'MMDLXI', 'MMDLXII', 'MMDLXIII', 'MMDLXIV', 'MMDLXV', 'MMDLXVI', 'MMDLXVII', 'MMDLXVIII', 'MMDLXIX', 'MMDLXX', 'MMDLXXI', 'MMDLXXII', 'MMDLXXIII', 'MMDLXXIV', 'MMDLXXV', 'MMDLXXVI', 'MMDLXXVII', 'MMDLXXVIII', 'MMDLXXIX', 'MMDLXXX', 'MMDLXXXI', 'MMDLXXXII', 'MMDLXXXIII', 'MMDLXXXIV', 'MMDLXXXV', 'MMDLXXXVI', 'MMDLXXXVII', 'MMDLXXXVIII', 'MMDLXXXIX', 'MMDXC', 'MMDXCI', 'MMDXCII', 'MMDXCIII', 'MMDXCIV', 'MMDVC', 'MMDVCI', 'MMDVCII', 'MMDVCIII', 'MMDIC', 'MMDC', 'MMDCI', 'MMDCII', 'MMDCIII', 'MMDCIV', 'MMDCV', 'MMDCVI', 'MMDCVII', 'MMDCVIII', 'MMDCIX', 'MMDCX', 'MMDCXI', 'MMDCXII', 'MMDCXIII', 'MMDCXIV', 'MMDCXV', 'MMDCXVI', 'MMDCXVII', 'MMDCXVIII', 'MMDCXIX', 'MMDCXX', 'MMDCXXI', 'MMDCXXII', 'MMDCXXIII', 'MMDCXXIV', 'MMDCXXV', 'MMDCXXVI', 'MMDCXXVII', 'MMDCXXVIII', 'MMDCXXIX', 'MMDCXXX', 'MMDCXXXI', 'MMDCXXXII', 'MMDCXXXIII', 'MMDCXXXIV', 'MMDCXXXV', 'MMDCXXXVI', 'MMDCXXXVII', 'MMDCXXXVIII', 'MMDCXXXIX', 'MMDCXL', 'MMDCXLI', 'MMDCXLII', 'MMDCXLIII', 'MMDCXLIV', 'MMDCVL', 'MMDCVLI', 'MMDCVLII', 'MMDCVLIII', 'MMDCIL', 'MMDCL', 'MMDCLI', 'MMDCLII', 'MMDCLIII', 'MMDCLIV', 'MMDCLV', 'MMDCLVI', 'MMDCLVII', 'MMDCLVIII', 'MMDCLIX', 'MMDCLX', 'MMDCLXI', 'MMDCLXII', 'MMDCLXIII', 'MMDCLXIV', 'MMDCLXV', 'MMDCLXVI', 'MMDCLXVII', 'MMDCLXVIII', 'MMDCLXIX', 'MMDCLXX', 'MMDCLXXI', 'MMDCLXXII', 'MMDCLXXIII', 'MMDCLXXIV', 'MMDCLXXV', 'MMDCLXXVI', 'MMDCLXXVII', 'MMDCLXXVIII', 'MMDCLXXIX', 'MMDCLXXX', 'MMDCLXXXI', 'MMDCLXXXII', 'MMDCLXXXIII', 'MMDCLXXXIV', 'MMDCLXXXV', 'MMDCLXXXVI', 'MMDCLXXXVII', 'MMDCLXXXVIII', 'MMDCLXXXIX', 'MMDCXC', 'MMDCXCI', 'MMDCXCII', 'MMDCXCIII', 'MMDCXCIV', 'MMDCVC', 'MMDCVCI', 'MMDCVCII', 'MMDCVCIII', 'MMDCIC', 'MMDCC', 'MMDCCI', 'MMDCCII', 'MMDCCIII', 'MMDCCIV', 'MMDCCV', 'MMDCCVI', 'MMDCCVII', 'MMDCCVIII', 'MMDCCIX', 'MMDCCX', 'MMDCCXI', 'MMDCCXII', 'MMDCCXIII', 'MMDCCXIV', 'MMDCCXV', 'MMDCCXVI', 'MMDCCXVII', 'MMDCCXVIII', 'MMDCCXIX', 'MMDCCXX', 'MMDCCXXI', 'MMDCCXXII', 'MMDCCXXIII', 'MMDCCXXIV', 'MMDCCXXV', 'MMDCCXXVI', 'MMDCCXXVII', 'MMDCCXXVIII', 'MMDCCXXIX', 'MMDCCXXX', 'MMDCCXXXI', 'MMDCCXXXII', 'MMDCCXXXIII', 'MMDCCXXXIV', 'MMDCCXXXV', 'MMDCCXXXVI', 'MMDCCXXXVII', 'MMDCCXXXVIII', 'MMDCCXXXIX', 'MMDCCXL', 'MMDCCXLI', 'MMDCCXLII', 'MMDCCXLIII', 'MMDCCXLIV', 'MMDCCVL', 'MMDCCVLI', 'MMDCCVLII', 'MMDCCVLIII', 'MMDCCIL', 'MMDCCL', 'MMDCCLI', 'MMDCCLII', 'MMDCCLIII', 'MMDCCLIV', 'MMDCCLV', 'MMDCCLVI', 'MMDCCLVII', 'MMDCCLVIII', 'MMDCCLIX', 'MMDCCLX', 'MMDCCLXI', 'MMDCCLXII', 'MMDCCLXIII', 'MMDCCLXIV', 'MMDCCLXV', 'MMDCCLXVI', 'MMDCCLXVII', 'MMDCCLXVIII', 'MMDCCLXIX', 'MMDCCLXX', 'MMDCCLXXI', 'MMDCCLXXII', 'MMDCCLXXIII', 'MMDCCLXXIV', 'MMDCCLXXV', 'MMDCCLXXVI', 'MMDCCLXXVII', 'MMDCCLXXVIII', 'MMDCCLXXIX', 'MMDCCLXXX', 'MMDCCLXXXI', 'MMDCCLXXXII', 'MMDCCLXXXIII', 'MMDCCLXXXIV', 'MMDCCLXXXV', 'MMDCCLXXXVI', 'MMDCCLXXXVII', 'MMDCCLXXXVIII', 'MMDCCLXXXIX', 'MMDCCXC', 'MMDCCXCI', 'MMDCCXCII', 'MMDCCXCIII', 'MMDCCXCIV', 'MMDCCVC', 'MMDCCVCI', 'MMDCCVCII', 'MMDCCVCIII', 'MMDCCIC', 'MMDCCC', 'MMDCCCI', 'MMDCCCII', 'MMDCCCIII', 'MMDCCCIV', 'MMDCCCV', 'MMDCCCVI', 'MMDCCCVII', 'MMDCCCVIII', 'MMDCCCIX', 'MMDCCCX', 'MMDCCCXI', 'MMDCCCXII', 'MMDCCCXIII', 'MMDCCCXIV', 'MMDCCCXV', 'MMDCCCXVI', 'MMDCCCXVII', 'MMDCCCXVIII', 'MMDCCCXIX', 'MMDCCCXX', 'MMDCCCXXI', 'MMDCCCXXII', 'MMDCCCXXIII', 'MMDCCCXXIV', 'MMDCCCXXV', 'MMDCCCXXVI', 'MMDCCCXXVII', 'MMDCCCXXVIII', 'MMDCCCXXIX', 'MMDCCCXXX', 'MMDCCCXXXI', 'MMDCCCXXXII', 'MMDCCCXXXIII', 'MMDCCCXXXIV', 'MMDCCCXXXV', 'MMDCCCXXXVI', 'MMDCCCXXXVII', 'MMDCCCXXXVIII', 'MMDCCCXXXIX', 'MMDCCCXL', 'MMDCCCXLI', 'MMDCCCXLII', 'MMDCCCXLIII', 'MMDCCCXLIV', 'MMDCCCVL', 'MMDCCCVLI', 'MMDCCCVLII', 'MMDCCCVLIII', 'MMDCCCIL', 'MMDCCCL', 'MMDCCCLI', 'MMDCCCLII', 'MMDCCCLIII', 'MMDCCCLIV', 'MMDCCCLV', 'MMDCCCLVI', 'MMDCCCLVII', 'MMDCCCLVIII', 'MMDCCCLIX', 'MMDCCCLX', 'MMDCCCLXI', 'MMDCCCLXII', 'MMDCCCLXIII', 'MMDCCCLXIV', 'MMDCCCLXV', 'MMDCCCLXVI', 'MMDCCCLXVII', 'MMDCCCLXVIII', 'MMDCCCLXIX', 'MMDCCCLXX', 'MMDCCCLXXI', 'MMDCCCLXXII', 'MMDCCCLXXIII', 'MMDCCCLXXIV', 'MMDCCCLXXV', 'MMDCCCLXXVI', 'MMDCCCLXXVII', 'MMDCCCLXXVIII', 'MMDCCCLXXIX', 'MMDCCCLXXX', 'MMDCCCLXXXI', 'MMDCCCLXXXII', 'MMDCCCLXXXIII', 'MMDCCCLXXXIV', 'MMDCCCLXXXV', 'MMDCCCLXXXVI', 'MMDCCCLXXXVII', 'MMDCCCLXXXVIII', 'MMDCCCLXXXIX', 'MMDCCCXC', 'MMDCCCXCI', 'MMDCCCXCII', 'MMDCCCXCIII', 'MMDCCCXCIV', 'MMDCCCVC', 'MMDCCCVCI', 'MMDCCCVCII', 'MMDCCCVCIII', 'MMDCCCIC', 'MMCM', 'MMCMI', 'MMCMII', 'MMCMIII', 'MMCMIV', 'MMCMV', 'MMCMVI', 'MMCMVII', 'MMCMVIII', 'MMCMIX', 'MMCMX', 'MMCMXI', 'MMCMXII', 'MMCMXIII', 'MMCMXIV', 'MMCMXV', 'MMCMXVI', 'MMCMXVII', 'MMCMXVIII', 'MMCMXIX', 'MMCMXX', 'MMCMXXI', 'MMCMXXII', 'MMCMXXIII', 'MMCMXXIV', 'MMCMXXV', 'MMCMXXVI', 'MMCMXXVII', 'MMCMXXVIII', 'MMCMXXIX', 'MMCMXXX', 'MMCMXXXI', 'MMCMXXXII', 'MMCMXXXIII', 'MMCMXXXIV', 'MMCMXXXV', 'MMCMXXXVI', 'MMCMXXXVII', 'MMCMXXXVIII', 'MMCMXXXIX', 'MMCMXL', 'MMCMXLI', 'MMCMXLII', 'MMCMXLIII', 'MMCMXLIV', 'MMCMVL', 'MMCMVLI', 'MMCMVLII', 'MMCMVLIII', 'MMCMIL', 'MMLM', 'MMLMI', 'MMLMII', 'MMLMIII', 'MMLMIV', 'MMLMV', 'MMLMVI', 'MMLMVII', 'MMLMVIII', 'MMLMIX', 'MMLMX', 'MMLMXI', 'MMLMXII', 'MMLMXIII', 'MMLMXIV', 'MMLMXV', 'MMLMXVI', 'MMLMXVII', 'MMLMXVIII', 'MMLMXIX', 'MMLMXX', 'MMLMXXI', 'MMLMXXII', 'MMLMXXIII', 'MMLMXXIV', 'MMLMXXV', 'MMLMXXVI', 'MMLMXXVII', 'MMLMXXVIII', 'MMLMXXIX', 'MMLMXXX', 'MMLMXXXI', 'MMLMXXXII', 'MMLMXXXIII', 'MMLMXXXIV', 'MMLMXXXV', 'MMLMXXXVI', 'MMLMXXXVII', 'MMLMXXXVIII', 'MMLMXXXIX', 'MMXM', 'MMXMI', 'MMXMII', 'MMXMIII', 'MMXMIV', 'MMXMV', 'MMXMVI', 'MMXMVII', 'MMXMVIII', 'MMXMIX', 'MMM', 'MMMI', 'MMMII', 'MMMIII', 'MMMIV', 'MMMV', 'MMMVI', 'MMMVII', 'MMMVIII', 'MMMIX', 'MMMX', 'MMMXI', 'MMMXII', 'MMMXIII', 'MMMXIV', 'MMMXV', 'MMMXVI', 'MMMXVII', 'MMMXVIII', 'MMMXIX', 'MMMXX', 'MMMXXI', 'MMMXXII', 'MMMXXIII', 'MMMXXIV', 'MMMXXV', 'MMMXXVI', 'MMMXXVII', 'MMMXXVIII', 'MMMXXIX', 'MMMXXX', 'MMMXXXI', 'MMMXXXII', 'MMMXXXIII', 'MMMXXXIV', 'MMMXXXV', 'MMMXXXVI', 'MMMXXXVII', 'MMMXXXVIII', 'MMMXXXIX', 'MMMXL', 'MMMXLI', 'MMMXLII', 'MMMXLIII', 'MMMXLIV', 'MMMVL', 'MMMVLI', 'MMMVLII', 'MMMVLIII', 'MMMIL', 'MMML', 'MMMLI', 'MMMLII', 'MMMLIII', 'MMMLIV', 'MMMLV', 'MMMLVI', 'MMMLVII', 'MMMLVIII', 'MMMLIX', 'MMMLX', 'MMMLXI', 'MMMLXII', 'MMMLXIII', 'MMMLXIV', 'MMMLXV', 'MMMLXVI', 'MMMLXVII', 'MMMLXVIII', 'MMMLXIX', 'MMMLXX', 'MMMLXXI', 'MMMLXXII', 'MMMLXXIII', 'MMMLXXIV', 'MMMLXXV', 'MMMLXXVI', 'MMMLXXVII', 'MMMLXXVIII', 'MMMLXXIX', 'MMMLXXX', 'MMMLXXXI', 'MMMLXXXII', 'MMMLXXXIII', 'MMMLXXXIV', 'MMMLXXXV', 'MMMLXXXVI', 'MMMLXXXVII', 'MMMLXXXVIII', 'MMMLXXXIX', 'MMMXC', 'MMMXCI', 'MMMXCII', 'MMMXCIII', 'MMMXCIV', 'MMMVC', 'MMMVCI', 'MMMVCII', 'MMMVCIII', 'MMMIC', 'MMMC', 'MMMCI', 'MMMCII', 'MMMCIII', 'MMMCIV', 'MMMCV', 'MMMCVI', 'MMMCVII', 'MMMCVIII', 'MMMCIX', 'MMMCX', 'MMMCXI', 'MMMCXII', 'MMMCXIII', 'MMMCXIV', 'MMMCXV', 'MMMCXVI', 'MMMCXVII', 'MMMCXVIII', 'MMMCXIX', 'MMMCXX', 'MMMCXXI', 'MMMCXXII', 'MMMCXXIII', 'MMMCXXIV', 'MMMCXXV', 'MMMCXXVI', 'MMMCXXVII', 'MMMCXXVIII', 'MMMCXXIX', 'MMMCXXX', 'MMMCXXXI', 'MMMCXXXII', 'MMMCXXXIII', 'MMMCXXXIV', 'MMMCXXXV', 'MMMCXXXVI', 'MMMCXXXVII', 'MMMCXXXVIII', 'MMMCXXXIX', 'MMMCXL', 'MMMCXLI', 'MMMCXLII', 'MMMCXLIII', 'MMMCXLIV', 'MMMCVL', 'MMMCVLI', 'MMMCVLII', 'MMMCVLIII', 'MMMCIL', 'MMMCL', 'MMMCLI', 'MMMCLII', 'MMMCLIII', 'MMMCLIV', 'MMMCLV', 'MMMCLVI', 'MMMCLVII', 'MMMCLVIII', 'MMMCLIX', 'MMMCLX', 'MMMCLXI', 'MMMCLXII', 'MMMCLXIII', 'MMMCLXIV', 'MMMCLXV', 'MMMCLXVI', 'MMMCLXVII', 'MMMCLXVIII', 'MMMCLXIX', 'MMMCLXX', 'MMMCLXXI', 'MMMCLXXII', 'MMMCLXXIII', 'MMMCLXXIV', 'MMMCLXXV', 'MMMCLXXVI', 'MMMCLXXVII', 'MMMCLXXVIII', 'MMMCLXXIX', 'MMMCLXXX', 'MMMCLXXXI', 'MMMCLXXXII', 'MMMCLXXXIII', 'MMMCLXXXIV', 'MMMCLXXXV', 'MMMCLXXXVI', 'MMMCLXXXVII', 'MMMCLXXXVIII', 'MMMCLXXXIX', 'MMMCXC', 'MMMCXCI', 'MMMCXCII', 'MMMCXCIII', 'MMMCXCIV', 'MMMCVC', 'MMMCVCI', 'MMMCVCII', 'MMMCVCIII', 'MMMCIC', 'MMMCC', 'MMMCCI', 'MMMCCII', 'MMMCCIII', 'MMMCCIV', 'MMMCCV', 'MMMCCVI', 'MMMCCVII', 'MMMCCVIII', 'MMMCCIX', 'MMMCCX', 'MMMCCXI', 'MMMCCXII', 'MMMCCXIII', 'MMMCCXIV', 'MMMCCXV', 'MMMCCXVI', 'MMMCCXVII', 'MMMCCXVIII', 'MMMCCXIX', 'MMMCCXX', 'MMMCCXXI', 'MMMCCXXII', 'MMMCCXXIII', 'MMMCCXXIV', 'MMMCCXXV', 'MMMCCXXVI', 'MMMCCXXVII', 'MMMCCXXVIII', 'MMMCCXXIX', 'MMMCCXXX', 'MMMCCXXXI', 'MMMCCXXXII', 'MMMCCXXXIII', 'MMMCCXXXIV', 'MMMCCXXXV', 'MMMCCXXXVI', 'MMMCCXXXVII', 'MMMCCXXXVIII', 'MMMCCXXXIX', 'MMMCCXL', 'MMMCCXLI', 'MMMCCXLII', 'MMMCCXLIII', 'MMMCCXLIV', 'MMMCCVL', 'MMMCCVLI', 'MMMCCVLII', 'MMMCCVLIII', 'MMMCCIL', 'MMMCCL', 'MMMCCLI', 'MMMCCLII', 'MMMCCLIII', 'MMMCCLIV', 'MMMCCLV', 'MMMCCLVI', 'MMMCCLVII', 'MMMCCLVIII', 'MMMCCLIX', 'MMMCCLX', 'MMMCCLXI', 'MMMCCLXII', 'MMMCCLXIII', 'MMMCCLXIV', 'MMMCCLXV', 'MMMCCLXVI', 'MMMCCLXVII', 'MMMCCLXVIII', 'MMMCCLXIX', 'MMMCCLXX', 'MMMCCLXXI', 'MMMCCLXXII', 'MMMCCLXXIII', 'MMMCCLXXIV', 'MMMCCLXXV', 'MMMCCLXXVI', 'MMMCCLXXVII', 'MMMCCLXXVIII', 'MMMCCLXXIX', 'MMMCCLXXX', 'MMMCCLXXXI', 'MMMCCLXXXII', 'MMMCCLXXXIII', 'MMMCCLXXXIV', 'MMMCCLXXXV', 'MMMCCLXXXVI', 'MMMCCLXXXVII', 'MMMCCLXXXVIII', 'MMMCCLXXXIX', 'MMMCCXC', 'MMMCCXCI', 'MMMCCXCII', 'MMMCCXCIII', 'MMMCCXCIV', 'MMMCCVC', 'MMMCCVCI', 'MMMCCVCII', 'MMMCCVCIII', 'MMMCCIC', 'MMMCCC', 'MMMCCCI', 'MMMCCCII', 'MMMCCCIII', 'MMMCCCIV', 'MMMCCCV', 'MMMCCCVI', 'MMMCCCVII', 'MMMCCCVIII', 'MMMCCCIX', 'MMMCCCX', 'MMMCCCXI', 'MMMCCCXII', 'MMMCCCXIII', 'MMMCCCXIV', 'MMMCCCXV', 'MMMCCCXVI', 'MMMCCCXVII', 'MMMCCCXVIII', 'MMMCCCXIX', 'MMMCCCXX', 'MMMCCCXXI', 'MMMCCCXXII', 'MMMCCCXXIII', 'MMMCCCXXIV', 'MMMCCCXXV', 'MMMCCCXXVI', 'MMMCCCXXVII', 'MMMCCCXXVIII', 'MMMCCCXXIX', 'MMMCCCXXX', 'MMMCCCXXXI', 'MMMCCCXXXII', 'MMMCCCXXXIII', 'MMMCCCXXXIV', 'MMMCCCXXXV', 'MMMCCCXXXVI', 'MMMCCCXXXVII', 'MMMCCCXXXVIII', 'MMMCCCXXXIX', 'MMMCCCXL', 'MMMCCCXLI', 'MMMCCCXLII', 'MMMCCCXLIII', 'MMMCCCXLIV', 'MMMCCCVL', 'MMMCCCVLI', 'MMMCCCVLII', 'MMMCCCVLIII', 'MMMCCCIL', 'MMMCCCL', 'MMMCCCLI', 'MMMCCCLII', 'MMMCCCLIII', 'MMMCCCLIV', 'MMMCCCLV', 'MMMCCCLVI', 'MMMCCCLVII', 'MMMCCCLVIII', 'MMMCCCLIX', 'MMMCCCLX', 'MMMCCCLXI', 'MMMCCCLXII', 'MMMCCCLXIII', 'MMMCCCLXIV', 'MMMCCCLXV', 'MMMCCCLXVI', 'MMMCCCLXVII', 'MMMCCCLXVIII', 'MMMCCCLXIX', 'MMMCCCLXX', 'MMMCCCLXXI', 'MMMCCCLXXII', 'MMMCCCLXXIII', 'MMMCCCLXXIV', 'MMMCCCLXXV', 'MMMCCCLXXVI', 'MMMCCCLXXVII', 'MMMCCCLXXVIII', 'MMMCCCLXXIX', 'MMMCCCLXXX', 'MMMCCCLXXXI', 'MMMCCCLXXXII', 'MMMCCCLXXXIII', 'MMMCCCLXXXIV', 'MMMCCCLXXXV', 'MMMCCCLXXXVI', 'MMMCCCLXXXVII', 'MMMCCCLXXXVIII', 'MMMCCCLXXXIX', 'MMMCCCXC', 'MMMCCCXCI', 'MMMCCCXCII', 'MMMCCCXCIII', 'MMMCCCXCIV', 'MMMCCCVC', 'MMMCCCVCI', 'MMMCCCVCII', 'MMMCCCVCIII', 'MMMCCCIC', 'MMMCD', 'MMMCDI', 'MMMCDII', 'MMMCDIII', 'MMMCDIV', 'MMMCDV', 'MMMCDVI', 'MMMCDVII', 'MMMCDVIII', 'MMMCDIX', 'MMMCDX', 'MMMCDXI', 'MMMCDXII', 'MMMCDXIII', 'MMMCDXIV', 'MMMCDXV', 'MMMCDXVI', 'MMMCDXVII', 'MMMCDXVIII', 'MMMCDXIX', 'MMMCDXX', 'MMMCDXXI', 'MMMCDXXII', 'MMMCDXXIII', 'MMMCDXXIV', 'MMMCDXXV', 'MMMCDXXVI', 'MMMCDXXVII', 'MMMCDXXVIII', 'MMMCDXXIX', 'MMMCDXXX', 'MMMCDXXXI', 'MMMCDXXXII', 'MMMCDXXXIII', 'MMMCDXXXIV', 'MMMCDXXXV', 'MMMCDXXXVI', 'MMMCDXXXVII', 'MMMCDXXXVIII', 'MMMCDXXXIX', 'MMMCDXL', 'MMMCDXLI', 'MMMCDXLII', 'MMMCDXLIII', 'MMMCDXLIV', 'MMMCDVL', 'MMMCDVLI', 'MMMCDVLII', 'MMMCDVLIII', 'MMMCDIL', 'MMMLD', 'MMMLDI', 'MMMLDII', 'MMMLDIII', 'MMMLDIV', 'MMMLDV', 'MMMLDVI', 'MMMLDVII', 'MMMLDVIII', 'MMMLDIX', 'MMMLDX', 'MMMLDXI', 'MMMLDXII', 'MMMLDXIII', 'MMMLDXIV', 'MMMLDXV', 'MMMLDXVI', 'MMMLDXVII', 'MMMLDXVIII', 'MMMLDXIX', 'MMMLDXX', 'MMMLDXXI', 'MMMLDXXII', 'MMMLDXXIII', 'MMMLDXXIV', 'MMMLDXXV', 'MMMLDXXVI', 'MMMLDXXVII', 'MMMLDXXVIII', 'MMMLDXXIX', 'MMMLDXXX', 'MMMLDXXXI', 'MMMLDXXXII', 'MMMLDXXXIII', 'MMMLDXXXIV', 'MMMLDXXXV', 'MMMLDXXXVI', 'MMMLDXXXVII', 'MMMLDXXXVIII', 'MMMLDXXXIX', 'MMMXD', 'MMMXDI', 'MMMXDII', 'MMMXDIII', 'MMMXDIV', 'MMMXDV', 'MMMXDVI', 'MMMXDVII', 'MMMXDVIII', 'MMMXDIX', 'MMMD', 'MMMDI', 'MMMDII', 'MMMDIII', 'MMMDIV', 'MMMDV', 'MMMDVI', 'MMMDVII', 'MMMDVIII', 'MMMDIX', 'MMMDX', 'MMMDXI', 'MMMDXII', 'MMMDXIII', 'MMMDXIV', 'MMMDXV', 'MMMDXVI', 'MMMDXVII', 'MMMDXVIII', 'MMMDXIX', 'MMMDXX', 'MMMDXXI', 'MMMDXXII', 'MMMDXXIII', 'MMMDXXIV', 'MMMDXXV', 'MMMDXXVI', 'MMMDXXVII', 'MMMDXXVIII', 'MMMDXXIX', 'MMMDXXX', 'MMMDXXXI', 'MMMDXXXII', 'MMMDXXXIII', 'MMMDXXXIV', 'MMMDXXXV', 'MMMDXXXVI', 'MMMDXXXVII', 'MMMDXXXVIII', 'MMMDXXXIX', 'MMMDXL', 'MMMDXLI', 'MMMDXLII', 'MMMDXLIII', 'MMMDXLIV', 'MMMDVL', 'MMMDVLI', 'MMMDVLII', 'MMMDVLIII', 'MMMDIL', 'MMMDL', 'MMMDLI', 'MMMDLII', 'MMMDLIII', 'MMMDLIV', 'MMMDLV', 'MMMDLVI', 'MMMDLVII', 'MMMDLVIII', 'MMMDLIX', 'MMMDLX', 'MMMDLXI', 'MMMDLXII', 'MMMDLXIII', 'MMMDLXIV', 'MMMDLXV', 'MMMDLXVI', 'MMMDLXVII', 'MMMDLXVIII', 'MMMDLXIX', 'MMMDLXX', 'MMMDLXXI', 'MMMDLXXII', 'MMMDLXXIII', 'MMMDLXXIV', 'MMMDLXXV', 'MMMDLXXVI', 'MMMDLXXVII', 'MMMDLXXVIII', 'MMMDLXXIX', 'MMMDLXXX', 'MMMDLXXXI', 'MMMDLXXXII', 'MMMDLXXXIII', 'MMMDLXXXIV', 'MMMDLXXXV', 'MMMDLXXXVI', 'MMMDLXXXVII', 'MMMDLXXXVIII', 'MMMDLXXXIX', 'MMMDXC', 'MMMDXCI', 'MMMDXCII', 'MMMDXCIII', 'MMMDXCIV', 'MMMDVC', 'MMMDVCI', 'MMMDVCII', 'MMMDVCIII', 'MMMDIC', 'MMMDC', 'MMMDCI', 'MMMDCII', 'MMMDCIII', 'MMMDCIV', 'MMMDCV', 'MMMDCVI', 'MMMDCVII', 'MMMDCVIII', 'MMMDCIX', 'MMMDCX', 'MMMDCXI', 'MMMDCXII', 'MMMDCXIII', 'MMMDCXIV', 'MMMDCXV', 'MMMDCXVI', 'MMMDCXVII', 'MMMDCXVIII', 'MMMDCXIX', 'MMMDCXX', 'MMMDCXXI', 'MMMDCXXII', 'MMMDCXXIII', 'MMMDCXXIV', 'MMMDCXXV', 'MMMDCXXVI', 'MMMDCXXVII', 'MMMDCXXVIII', 'MMMDCXXIX', 'MMMDCXXX', 'MMMDCXXXI', 'MMMDCXXXII', 'MMMDCXXXIII', 'MMMDCXXXIV', 'MMMDCXXXV', 'MMMDCXXXVI', 'MMMDCXXXVII', 'MMMDCXXXVIII', 'MMMDCXXXIX', 'MMMDCXL', 'MMMDCXLI', 'MMMDCXLII', 'MMMDCXLIII', 'MMMDCXLIV', 'MMMDCVL', 'MMMDCVLI', 'MMMDCVLII', 'MMMDCVLIII', 'MMMDCIL', 'MMMDCL', 'MMMDCLI', 'MMMDCLII', 'MMMDCLIII', 'MMMDCLIV', 'MMMDCLV', 'MMMDCLVI', 'MMMDCLVII', 'MMMDCLVIII', 'MMMDCLIX', 'MMMDCLX', 'MMMDCLXI', 'MMMDCLXII', 'MMMDCLXIII', 'MMMDCLXIV', 'MMMDCLXV', 'MMMDCLXVI', 'MMMDCLXVII', 'MMMDCLXVIII', 'MMMDCLXIX', 'MMMDCLXX', 'MMMDCLXXI', 'MMMDCLXXII', 'MMMDCLXXIII', 'MMMDCLXXIV', 'MMMDCLXXV', 'MMMDCLXXVI', 'MMMDCLXXVII', 'MMMDCLXXVIII', 'MMMDCLXXIX', 'MMMDCLXXX', 'MMMDCLXXXI', 'MMMDCLXXXII', 'MMMDCLXXXIII', 'MMMDCLXXXIV', 'MMMDCLXXXV', 'MMMDCLXXXVI', 'MMMDCLXXXVII', 'MMMDCLXXXVIII', 'MMMDCLXXXIX', 'MMMDCXC', 'MMMDCXCI', 'MMMDCXCII', 'MMMDCXCIII', 'MMMDCXCIV', 'MMMDCVC', 'MMMDCVCI', 'MMMDCVCII', 'MMMDCVCIII', 'MMMDCIC', 'MMMDCC', 'MMMDCCI', 'MMMDCCII', 'MMMDCCIII', 'MMMDCCIV', 'MMMDCCV', 'MMMDCCVI', 'MMMDCCVII', 'MMMDCCVIII', 'MMMDCCIX', 'MMMDCCX', 'MMMDCCXI', 'MMMDCCXII', 'MMMDCCXIII', 'MMMDCCXIV', 'MMMDCCXV', 'MMMDCCXVI', 'MMMDCCXVII', 'MMMDCCXVIII', 'MMMDCCXIX', 'MMMDCCXX', 'MMMDCCXXI', 'MMMDCCXXII', 'MMMDCCXXIII', 'MMMDCCXXIV', 'MMMDCCXXV', 'MMMDCCXXVI', 'MMMDCCXXVII', 'MMMDCCXXVIII', 'MMMDCCXXIX', 'MMMDCCXXX', 'MMMDCCXXXI', 'MMMDCCXXXII', 'MMMDCCXXXIII', 'MMMDCCXXXIV', 'MMMDCCXXXV', 'MMMDCCXXXVI', 'MMMDCCXXXVII', 'MMMDCCXXXVIII', 'MMMDCCXXXIX', 'MMMDCCXL', 'MMMDCCXLI', 'MMMDCCXLII', 'MMMDCCXLIII', 'MMMDCCXLIV', 'MMMDCCVL', 'MMMDCCVLI', 'MMMDCCVLII', 'MMMDCCVLIII', 'MMMDCCIL', 'MMMDCCL', 'MMMDCCLI', 'MMMDCCLII', 'MMMDCCLIII', 'MMMDCCLIV', 'MMMDCCLV', 'MMMDCCLVI', 'MMMDCCLVII', 'MMMDCCLVIII', 'MMMDCCLIX', 'MMMDCCLX', 'MMMDCCLXI', 'MMMDCCLXII', 'MMMDCCLXIII', 'MMMDCCLXIV', 'MMMDCCLXV', 'MMMDCCLXVI', 'MMMDCCLXVII', 'MMMDCCLXVIII', 'MMMDCCLXIX', 'MMMDCCLXX', 'MMMDCCLXXI', 'MMMDCCLXXII', 'MMMDCCLXXIII', 'MMMDCCLXXIV', 'MMMDCCLXXV', 'MMMDCCLXXVI', 'MMMDCCLXXVII', 'MMMDCCLXXVIII', 'MMMDCCLXXIX', 'MMMDCCLXXX', 'MMMDCCLXXXI', 'MMMDCCLXXXII', 'MMMDCCLXXXIII', 'MMMDCCLXXXIV', 'MMMDCCLXXXV', 'MMMDCCLXXXVI', 'MMMDCCLXXXVII', 'MMMDCCLXXXVIII', 'MMMDCCLXXXIX', 'MMMDCCXC', 'MMMDCCXCI', 'MMMDCCXCII', 'MMMDCCXCIII', 'MMMDCCXCIV', 'MMMDCCVC', 'MMMDCCVCI', 'MMMDCCVCII', 'MMMDCCVCIII', 'MMMDCCIC', 'MMMDCCC', 'MMMDCCCI', 'MMMDCCCII', 'MMMDCCCIII', 'MMMDCCCIV', 'MMMDCCCV', 'MMMDCCCVI', 'MMMDCCCVII', 'MMMDCCCVIII', 'MMMDCCCIX', 'MMMDCCCX', 'MMMDCCCXI', 'MMMDCCCXII', 'MMMDCCCXIII', 'MMMDCCCXIV', 'MMMDCCCXV', 'MMMDCCCXVI', 'MMMDCCCXVII', 'MMMDCCCXVIII', 'MMMDCCCXIX', 'MMMDCCCXX', 'MMMDCCCXXI', 'MMMDCCCXXII', 'MMMDCCCXXIII', 'MMMDCCCXXIV', 'MMMDCCCXXV', 'MMMDCCCXXVI', 'MMMDCCCXXVII', 'MMMDCCCXXVIII', 'MMMDCCCXXIX', 'MMMDCCCXXX', 'MMMDCCCXXXI', 'MMMDCCCXXXII', 'MMMDCCCXXXIII', 'MMMDCCCXXXIV', 'MMMDCCCXXXV', 'MMMDCCCXXXVI', 'MMMDCCCXXXVII', 'MMMDCCCXXXVIII', 'MMMDCCCXXXIX', 'MMMDCCCXL', 'MMMDCCCXLI', 'MMMDCCCXLII', 'MMMDCCCXLIII', 'MMMDCCCXLIV', 'MMMDCCCVL', 'MMMDCCCVLI', 'MMMDCCCVLII', 'MMMDCCCVLIII', 'MMMDCCCIL', 'MMMDCCCL', 'MMMDCCCLI', 'MMMDCCCLII', 'MMMDCCCLIII', 'MMMDCCCLIV', 'MMMDCCCLV', 'MMMDCCCLVI', 'MMMDCCCLVII', 'MMMDCCCLVIII', 'MMMDCCCLIX', 'MMMDCCCLX', 'MMMDCCCLXI', 'MMMDCCCLXII', 'MMMDCCCLXIII', 'MMMDCCCLXIV', 'MMMDCCCLXV', 'MMMDCCCLXVI', 'MMMDCCCLXVII', 'MMMDCCCLXVIII', 'MMMDCCCLXIX', 'MMMDCCCLXX', 'MMMDCCCLXXI', 'MMMDCCCLXXII', 'MMMDCCCLXXIII', 'MMMDCCCLXXIV', 'MMMDCCCLXXV', 'MMMDCCCLXXVI', 'MMMDCCCLXXVII', 'MMMDCCCLXXVIII', 'MMMDCCCLXXIX', 'MMMDCCCLXXX', 'MMMDCCCLXXXI', 'MMMDCCCLXXXII', 'MMMDCCCLXXXIII', 'MMMDCCCLXXXIV', 'MMMDCCCLXXXV', 'MMMDCCCLXXXVI', 'MMMDCCCLXXXVII', 'MMMDCCCLXXXVIII', 'MMMDCCCLXXXIX', 'MMMDCCCXC', 'MMMDCCCXCI', 'MMMDCCCXCII', 'MMMDCCCXCIII', 'MMMDCCCXCIV', 'MMMDCCCVC', 'MMMDCCCVCI', 'MMMDCCCVCII', 'MMMDCCCVCIII', 'MMMDCCCIC', 'MMMCM', 'MMMCMI', 'MMMCMII', 'MMMCMIII', 'MMMCMIV', 'MMMCMV', 'MMMCMVI', 'MMMCMVII', 'MMMCMVIII', 'MMMCMIX', 'MMMCMX', 'MMMCMXI', 'MMMCMXII', 'MMMCMXIII', 'MMMCMXIV', 'MMMCMXV', 'MMMCMXVI', 'MMMCMXVII', 'MMMCMXVIII', 'MMMCMXIX', 'MMMCMXX', 'MMMCMXXI', 'MMMCMXXII', 'MMMCMXXIII', 'MMMCMXXIV', 'MMMCMXXV', 'MMMCMXXVI', 'MMMCMXXVII', 'MMMCMXXVIII', 'MMMCMXXIX', 'MMMCMXXX', 'MMMCMXXXI', 'MMMCMXXXII', 'MMMCMXXXIII', 'MMMCMXXXIV', 'MMMCMXXXV', 'MMMCMXXXVI', 'MMMCMXXXVII', 'MMMCMXXXVIII', 'MMMCMXXXIX', 'MMMCMXL', 'MMMCMXLI', 'MMMCMXLII', 'MMMCMXLIII', 'MMMCMXLIV', 'MMMCMVL', 'MMMCMVLI', 'MMMCMVLII', 'MMMCMVLIII', 'MMMCMIL', 'MMMLM', 'MMMLMI', 'MMMLMII', 'MMMLMIII', 'MMMLMIV', 'MMMLMV', 'MMMLMVI', 'MMMLMVII', 'MMMLMVIII', 'MMMLMIX', 'MMMLMX', 'MMMLMXI', 'MMMLMXII', 'MMMLMXIII', 'MMMLMXIV', 'MMMLMXV', 'MMMLMXVI', 'MMMLMXVII', 'MMMLMXVIII', 'MMMLMXIX', 'MMMLMXX', 'MMMLMXXI', 'MMMLMXXII', 'MMMLMXXIII', 'MMMLMXXIV', 'MMMLMXXV', 'MMMLMXXVI', 'MMMLMXXVII', 'MMMLMXXVIII', 'MMMLMXXIX', 'MMMLMXXX', 'MMMLMXXXI', 'MMMLMXXXII', 'MMMLMXXXIII', 'MMMLMXXXIV', 'MMMLMXXXV', 'MMMLMXXXVI', 'MMMLMXXXVII', 'MMMLMXXXVIII', 'MMMLMXXXIX', 'MMMXM', 'MMMXMI', 'MMMXMII', 'MMMXMIII', 'MMMXMIV', 'MMMXMV', 'MMMXMVI', 'MMMXMVII', 'MMMXMVIII', 'MMMXMIX', ] -export const mode3 = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI', 'XII', 'XIII', 'XIV', 'XV', 'XVI', 'XVII', 'XVIII', 'XIX', 'XX', 'XXI', 'XXII', 'XXIII', 'XXIV', 'XXV', 'XXVI', 'XXVII', 'XXVIII', 'XXIX', 'XXX', 'XXXI', 'XXXII', 'XXXIII', 'XXXIV', 'XXXV', 'XXXVI', 'XXXVII', 'XXXVIII', 'XXXIX', 'XL', 'XLI', 'XLII', 'XLIII', 'XLIV', 'VL', 'VLI', 'VLII', 'VLIII', 'IL', 'L', 'LI', 'LII', 'LIII', 'LIV', 'LV', 'LVI', 'LVII', 'LVIII', 'LIX', 'LX', 'LXI', 'LXII', 'LXIII', 'LXIV', 'LXV', 'LXVI', 'LXVII', 'LXVIII', 'LXIX', 'LXX', 'LXXI', 'LXXII', 'LXXIII', 'LXXIV', 'LXXV', 'LXXVI', 'LXXVII', 'LXXVIII', 'LXXIX', 'LXXX', 'LXXXI', 'LXXXII', 'LXXXIII', 'LXXXIV', 'LXXXV', 'LXXXVI', 'LXXXVII', 'LXXXVIII', 'LXXXIX', 'XC', 'XCI', 'XCII', 'XCIII', 'XCIV', 'VC', 'VCI', 'VCII', 'VCIII', 'IC', 'C', 'CI', 'CII', 'CIII', 'CIV', 'CV', 'CVI', 'CVII', 'CVIII', 'CIX', 'CX', 'CXI', 'CXII', 'CXIII', 'CXIV', 'CXV', 'CXVI', 'CXVII', 'CXVIII', 'CXIX', 'CXX', 'CXXI', 'CXXII', 'CXXIII', 'CXXIV', 'CXXV', 'CXXVI', 'CXXVII', 'CXXVIII', 'CXXIX', 'CXXX', 'CXXXI', 'CXXXII', 'CXXXIII', 'CXXXIV', 'CXXXV', 'CXXXVI', 'CXXXVII', 'CXXXVIII', 'CXXXIX', 'CXL', 'CXLI', 'CXLII', 'CXLIII', 'CXLIV', 'CVL', 'CVLI', 'CVLII', 'CVLIII', 'CIL', 'CL', 'CLI', 'CLII', 'CLIII', 'CLIV', 'CLV', 'CLVI', 'CLVII', 'CLVIII', 'CLIX', 'CLX', 'CLXI', 'CLXII', 'CLXIII', 'CLXIV', 'CLXV', 'CLXVI', 'CLXVII', 'CLXVIII', 'CLXIX', 'CLXX', 'CLXXI', 'CLXXII', 'CLXXIII', 'CLXXIV', 'CLXXV', 'CLXXVI', 'CLXXVII', 'CLXXVIII', 'CLXXIX', 'CLXXX', 'CLXXXI', 'CLXXXII', 'CLXXXIII', 'CLXXXIV', 'CLXXXV', 'CLXXXVI', 'CLXXXVII', 'CLXXXVIII', 'CLXXXIX', 'CXC', 'CXCI', 'CXCII', 'CXCIII', 'CXCIV', 'CVC', 'CVCI', 'CVCII', 'CVCIII', 'CIC', 'CC', 'CCI', 'CCII', 'CCIII', 'CCIV', 'CCV', 'CCVI', 'CCVII', 'CCVIII', 'CCIX', 'CCX', 'CCXI', 'CCXII', 'CCXIII', 'CCXIV', 'CCXV', 'CCXVI', 'CCXVII', 'CCXVIII', 'CCXIX', 'CCXX', 'CCXXI', 'CCXXII', 'CCXXIII', 'CCXXIV', 'CCXXV', 'CCXXVI', 'CCXXVII', 'CCXXVIII', 'CCXXIX', 'CCXXX', 'CCXXXI', 'CCXXXII', 'CCXXXIII', 'CCXXXIV', 'CCXXXV', 'CCXXXVI', 'CCXXXVII', 'CCXXXVIII', 'CCXXXIX', 'CCXL', 'CCXLI', 'CCXLII', 'CCXLIII', 'CCXLIV', 'CCVL', 'CCVLI', 'CCVLII', 'CCVLIII', 'CCIL', 'CCL', 'CCLI', 'CCLII', 'CCLIII', 'CCLIV', 'CCLV', 'CCLVI', 'CCLVII', 'CCLVIII', 'CCLIX', 'CCLX', 'CCLXI', 'CCLXII', 'CCLXIII', 'CCLXIV', 'CCLXV', 'CCLXVI', 'CCLXVII', 'CCLXVIII', 'CCLXIX', 'CCLXX', 'CCLXXI', 'CCLXXII', 'CCLXXIII', 'CCLXXIV', 'CCLXXV', 'CCLXXVI', 'CCLXXVII', 'CCLXXVIII', 'CCLXXIX', 'CCLXXX', 'CCLXXXI', 'CCLXXXII', 'CCLXXXIII', 'CCLXXXIV', 'CCLXXXV', 'CCLXXXVI', 'CCLXXXVII', 'CCLXXXVIII', 'CCLXXXIX', 'CCXC', 'CCXCI', 'CCXCII', 'CCXCIII', 'CCXCIV', 'CCVC', 'CCVCI', 'CCVCII', 'CCVCIII', 'CCIC', 'CCC', 'CCCI', 'CCCII', 'CCCIII', 'CCCIV', 'CCCV', 'CCCVI', 'CCCVII', 'CCCVIII', 'CCCIX', 'CCCX', 'CCCXI', 'CCCXII', 'CCCXIII', 'CCCXIV', 'CCCXV', 'CCCXVI', 'CCCXVII', 'CCCXVIII', 'CCCXIX', 'CCCXX', 'CCCXXI', 'CCCXXII', 'CCCXXIII', 'CCCXXIV', 'CCCXXV', 'CCCXXVI', 'CCCXXVII', 'CCCXXVIII', 'CCCXXIX', 'CCCXXX', 'CCCXXXI', 'CCCXXXII', 'CCCXXXIII', 'CCCXXXIV', 'CCCXXXV', 'CCCXXXVI', 'CCCXXXVII', 'CCCXXXVIII', 'CCCXXXIX', 'CCCXL', 'CCCXLI', 'CCCXLII', 'CCCXLIII', 'CCCXLIV', 'CCCVL', 'CCCVLI', 'CCCVLII', 'CCCVLIII', 'CCCIL', 'CCCL', 'CCCLI', 'CCCLII', 'CCCLIII', 'CCCLIV', 'CCCLV', 'CCCLVI', 'CCCLVII', 'CCCLVIII', 'CCCLIX', 'CCCLX', 'CCCLXI', 'CCCLXII', 'CCCLXIII', 'CCCLXIV', 'CCCLXV', 'CCCLXVI', 'CCCLXVII', 'CCCLXVIII', 'CCCLXIX', 'CCCLXX', 'CCCLXXI', 'CCCLXXII', 'CCCLXXIII', 'CCCLXXIV', 'CCCLXXV', 'CCCLXXVI', 'CCCLXXVII', 'CCCLXXVIII', 'CCCLXXIX', 'CCCLXXX', 'CCCLXXXI', 'CCCLXXXII', 'CCCLXXXIII', 'CCCLXXXIV', 'CCCLXXXV', 'CCCLXXXVI', 'CCCLXXXVII', 'CCCLXXXVIII', 'CCCLXXXIX', 'CCCXC', 'CCCXCI', 'CCCXCII', 'CCCXCIII', 'CCCXCIV', 'CCCVC', 'CCCVCI', 'CCCVCII', 'CCCVCIII', 'CCCIC', 'CD', 'CDI', 'CDII', 'CDIII', 'CDIV', 'CDV', 'CDVI', 'CDVII', 'CDVIII', 'CDIX', 'CDX', 'CDXI', 'CDXII', 'CDXIII', 'CDXIV', 'CDXV', 'CDXVI', 'CDXVII', 'CDXVIII', 'CDXIX', 'CDXX', 'CDXXI', 'CDXXII', 'CDXXIII', 'CDXXIV', 'CDXXV', 'CDXXVI', 'CDXXVII', 'CDXXVIII', 'CDXXIX', 'CDXXX', 'CDXXXI', 'CDXXXII', 'CDXXXIII', 'CDXXXIV', 'CDXXXV', 'CDXXXVI', 'CDXXXVII', 'CDXXXVIII', 'CDXXXIX', 'CDXL', 'CDXLI', 'CDXLII', 'CDXLIII', 'CDXLIV', 'CDVL', 'CDVLI', 'CDVLII', 'CDVLIII', 'CDIL', 'LD', 'LDI', 'LDII', 'LDIII', 'LDIV', 'LDV', 'LDVI', 'LDVII', 'LDVIII', 'LDIX', 'LDX', 'LDXI', 'LDXII', 'LDXIII', 'LDXIV', 'LDXV', 'LDXVI', 'LDXVII', 'LDXVIII', 'LDXIX', 'LDXX', 'LDXXI', 'LDXXII', 'LDXXIII', 'LDXXIV', 'LDXXV', 'LDXXVI', 'LDXXVII', 'LDXXVIII', 'LDXXIX', 'LDXXX', 'LDXXXI', 'LDXXXII', 'LDXXXIII', 'LDXXXIV', 'LDXXXV', 'LDXXXVI', 'LDXXXVII', 'LDXXXVIII', 'LDXXXIX', 'XD', 'XDI', 'XDII', 'XDIII', 'XDIV', 'VD', 'VDI', 'VDII', 'VDIII', 'VDIV', 'D', 'DI', 'DII', 'DIII', 'DIV', 'DV', 'DVI', 'DVII', 'DVIII', 'DIX', 'DX', 'DXI', 'DXII', 'DXIII', 'DXIV', 'DXV', 'DXVI', 'DXVII', 'DXVIII', 'DXIX', 'DXX', 'DXXI', 'DXXII', 'DXXIII', 'DXXIV', 'DXXV', 'DXXVI', 'DXXVII', 'DXXVIII', 'DXXIX', 'DXXX', 'DXXXI', 'DXXXII', 'DXXXIII', 'DXXXIV', 'DXXXV', 'DXXXVI', 'DXXXVII', 'DXXXVIII', 'DXXXIX', 'DXL', 'DXLI', 'DXLII', 'DXLIII', 'DXLIV', 'DVL', 'DVLI', 'DVLII', 'DVLIII', 'DIL', 'DL', 'DLI', 'DLII', 'DLIII', 'DLIV', 'DLV', 'DLVI', 'DLVII', 'DLVIII', 'DLIX', 'DLX', 'DLXI', 'DLXII', 'DLXIII', 'DLXIV', 'DLXV', 'DLXVI', 'DLXVII', 'DLXVIII', 'DLXIX', 'DLXX', 'DLXXI', 'DLXXII', 'DLXXIII', 'DLXXIV', 'DLXXV', 'DLXXVI', 'DLXXVII', 'DLXXVIII', 'DLXXIX', 'DLXXX', 'DLXXXI', 'DLXXXII', 'DLXXXIII', 'DLXXXIV', 'DLXXXV', 'DLXXXVI', 'DLXXXVII', 'DLXXXVIII', 'DLXXXIX', 'DXC', 'DXCI', 'DXCII', 'DXCIII', 'DXCIV', 'DVC', 'DVCI', 'DVCII', 'DVCIII', 'DIC', 'DC', 'DCI', 'DCII', 'DCIII', 'DCIV', 'DCV', 'DCVI', 'DCVII', 'DCVIII', 'DCIX', 'DCX', 'DCXI', 'DCXII', 'DCXIII', 'DCXIV', 'DCXV', 'DCXVI', 'DCXVII', 'DCXVIII', 'DCXIX', 'DCXX', 'DCXXI', 'DCXXII', 'DCXXIII', 'DCXXIV', 'DCXXV', 'DCXXVI', 'DCXXVII', 'DCXXVIII', 'DCXXIX', 'DCXXX', 'DCXXXI', 'DCXXXII', 'DCXXXIII', 'DCXXXIV', 'DCXXXV', 'DCXXXVI', 'DCXXXVII', 'DCXXXVIII', 'DCXXXIX', 'DCXL', 'DCXLI', 'DCXLII', 'DCXLIII', 'DCXLIV', 'DCVL', 'DCVLI', 'DCVLII', 'DCVLIII', 'DCIL', 'DCL', 'DCLI', 'DCLII', 'DCLIII', 'DCLIV', 'DCLV', 'DCLVI', 'DCLVII', 'DCLVIII', 'DCLIX', 'DCLX', 'DCLXI', 'DCLXII', 'DCLXIII', 'DCLXIV', 'DCLXV', 'DCLXVI', 'DCLXVII', 'DCLXVIII', 'DCLXIX', 'DCLXX', 'DCLXXI', 'DCLXXII', 'DCLXXIII', 'DCLXXIV', 'DCLXXV', 'DCLXXVI', 'DCLXXVII', 'DCLXXVIII', 'DCLXXIX', 'DCLXXX', 'DCLXXXI', 'DCLXXXII', 'DCLXXXIII', 'DCLXXXIV', 'DCLXXXV', 'DCLXXXVI', 'DCLXXXVII', 'DCLXXXVIII', 'DCLXXXIX', 'DCXC', 'DCXCI', 'DCXCII', 'DCXCIII', 'DCXCIV', 'DCVC', 'DCVCI', 'DCVCII', 'DCVCIII', 'DCIC', 'DCC', 'DCCI', 'DCCII', 'DCCIII', 'DCCIV', 'DCCV', 'DCCVI', 'DCCVII', 'DCCVIII', 'DCCIX', 'DCCX', 'DCCXI', 'DCCXII', 'DCCXIII', 'DCCXIV', 'DCCXV', 'DCCXVI', 'DCCXVII', 'DCCXVIII', 'DCCXIX', 'DCCXX', 'DCCXXI', 'DCCXXII', 'DCCXXIII', 'DCCXXIV', 'DCCXXV', 'DCCXXVI', 'DCCXXVII', 'DCCXXVIII', 'DCCXXIX', 'DCCXXX', 'DCCXXXI', 'DCCXXXII', 'DCCXXXIII', 'DCCXXXIV', 'DCCXXXV', 'DCCXXXVI', 'DCCXXXVII', 'DCCXXXVIII', 'DCCXXXIX', 'DCCXL', 'DCCXLI', 'DCCXLII', 'DCCXLIII', 'DCCXLIV', 'DCCVL', 'DCCVLI', 'DCCVLII', 'DCCVLIII', 'DCCIL', 'DCCL', 'DCCLI', 'DCCLII', 'DCCLIII', 'DCCLIV', 'DCCLV', 'DCCLVI', 'DCCLVII', 'DCCLVIII', 'DCCLIX', 'DCCLX', 'DCCLXI', 'DCCLXII', 'DCCLXIII', 'DCCLXIV', 'DCCLXV', 'DCCLXVI', 'DCCLXVII', 'DCCLXVIII', 'DCCLXIX', 'DCCLXX', 'DCCLXXI', 'DCCLXXII', 'DCCLXXIII', 'DCCLXXIV', 'DCCLXXV', 'DCCLXXVI', 'DCCLXXVII', 'DCCLXXVIII', 'DCCLXXIX', 'DCCLXXX', 'DCCLXXXI', 'DCCLXXXII', 'DCCLXXXIII', 'DCCLXXXIV', 'DCCLXXXV', 'DCCLXXXVI', 'DCCLXXXVII', 'DCCLXXXVIII', 'DCCLXXXIX', 'DCCXC', 'DCCXCI', 'DCCXCII', 'DCCXCIII', 'DCCXCIV', 'DCCVC', 'DCCVCI', 'DCCVCII', 'DCCVCIII', 'DCCIC', 'DCCC', 'DCCCI', 'DCCCII', 'DCCCIII', 'DCCCIV', 'DCCCV', 'DCCCVI', 'DCCCVII', 'DCCCVIII', 'DCCCIX', 'DCCCX', 'DCCCXI', 'DCCCXII', 'DCCCXIII', 'DCCCXIV', 'DCCCXV', 'DCCCXVI', 'DCCCXVII', 'DCCCXVIII', 'DCCCXIX', 'DCCCXX', 'DCCCXXI', 'DCCCXXII', 'DCCCXXIII', 'DCCCXXIV', 'DCCCXXV', 'DCCCXXVI', 'DCCCXXVII', 'DCCCXXVIII', 'DCCCXXIX', 'DCCCXXX', 'DCCCXXXI', 'DCCCXXXII', 'DCCCXXXIII', 'DCCCXXXIV', 'DCCCXXXV', 'DCCCXXXVI', 'DCCCXXXVII', 'DCCCXXXVIII', 'DCCCXXXIX', 'DCCCXL', 'DCCCXLI', 'DCCCXLII', 'DCCCXLIII', 'DCCCXLIV', 'DCCCVL', 'DCCCVLI', 'DCCCVLII', 'DCCCVLIII', 'DCCCIL', 'DCCCL', 'DCCCLI', 'DCCCLII', 'DCCCLIII', 'DCCCLIV', 'DCCCLV', 'DCCCLVI', 'DCCCLVII', 'DCCCLVIII', 'DCCCLIX', 'DCCCLX', 'DCCCLXI', 'DCCCLXII', 'DCCCLXIII', 'DCCCLXIV', 'DCCCLXV', 'DCCCLXVI', 'DCCCLXVII', 'DCCCLXVIII', 'DCCCLXIX', 'DCCCLXX', 'DCCCLXXI', 'DCCCLXXII', 'DCCCLXXIII', 'DCCCLXXIV', 'DCCCLXXV', 'DCCCLXXVI', 'DCCCLXXVII', 'DCCCLXXVIII', 'DCCCLXXIX', 'DCCCLXXX', 'DCCCLXXXI', 'DCCCLXXXII', 'DCCCLXXXIII', 'DCCCLXXXIV', 'DCCCLXXXV', 'DCCCLXXXVI', 'DCCCLXXXVII', 'DCCCLXXXVIII', 'DCCCLXXXIX', 'DCCCXC', 'DCCCXCI', 'DCCCXCII', 'DCCCXCIII', 'DCCCXCIV', 'DCCCVC', 'DCCCVCI', 'DCCCVCII', 'DCCCVCIII', 'DCCCIC', 'CM', 'CMI', 'CMII', 'CMIII', 'CMIV', 'CMV', 'CMVI', 'CMVII', 'CMVIII', 'CMIX', 'CMX', 'CMXI', 'CMXII', 'CMXIII', 'CMXIV', 'CMXV', 'CMXVI', 'CMXVII', 'CMXVIII', 'CMXIX', 'CMXX', 'CMXXI', 'CMXXII', 'CMXXIII', 'CMXXIV', 'CMXXV', 'CMXXVI', 'CMXXVII', 'CMXXVIII', 'CMXXIX', 'CMXXX', 'CMXXXI', 'CMXXXII', 'CMXXXIII', 'CMXXXIV', 'CMXXXV', 'CMXXXVI', 'CMXXXVII', 'CMXXXVIII', 'CMXXXIX', 'CMXL', 'CMXLI', 'CMXLII', 'CMXLIII', 'CMXLIV', 'CMVL', 'CMVLI', 'CMVLII', 'CMVLIII', 'CMIL', 'LM', 'LMI', 'LMII', 'LMIII', 'LMIV', 'LMV', 'LMVI', 'LMVII', 'LMVIII', 'LMIX', 'LMX', 'LMXI', 'LMXII', 'LMXIII', 'LMXIV', 'LMXV', 'LMXVI', 'LMXVII', 'LMXVIII', 'LMXIX', 'LMXX', 'LMXXI', 'LMXXII', 'LMXXIII', 'LMXXIV', 'LMXXV', 'LMXXVI', 'LMXXVII', 'LMXXVIII', 'LMXXIX', 'LMXXX', 'LMXXXI', 'LMXXXII', 'LMXXXIII', 'LMXXXIV', 'LMXXXV', 'LMXXXVI', 'LMXXXVII', 'LMXXXVIII', 'LMXXXIX', 'XM', 'XMI', 'XMII', 'XMIII', 'XMIV', 'VM', 'VMI', 'VMII', 'VMIII', 'VMIV', 'M', 'MI', 'MII', 'MIII', 'MIV', 'MV', 'MVI', 'MVII', 'MVIII', 'MIX', 'MX', 'MXI', 'MXII', 'MXIII', 'MXIV', 'MXV', 'MXVI', 'MXVII', 'MXVIII', 'MXIX', 'MXX', 'MXXI', 'MXXII', 'MXXIII', 'MXXIV', 'MXXV', 'MXXVI', 'MXXVII', 'MXXVIII', 'MXXIX', 'MXXX', 'MXXXI', 'MXXXII', 'MXXXIII', 'MXXXIV', 'MXXXV', 'MXXXVI', 'MXXXVII', 'MXXXVIII', 'MXXXIX', 'MXL', 'MXLI', 'MXLII', 'MXLIII', 'MXLIV', 'MVL', 'MVLI', 'MVLII', 'MVLIII', 'MIL', 'ML', 'MLI', 'MLII', 'MLIII', 'MLIV', 'MLV', 'MLVI', 'MLVII', 'MLVIII', 'MLIX', 'MLX', 'MLXI', 'MLXII', 'MLXIII', 'MLXIV', 'MLXV', 'MLXVI', 'MLXVII', 'MLXVIII', 'MLXIX', 'MLXX', 'MLXXI', 'MLXXII', 'MLXXIII', 'MLXXIV', 'MLXXV', 'MLXXVI', 'MLXXVII', 'MLXXVIII', 'MLXXIX', 'MLXXX', 'MLXXXI', 'MLXXXII', 'MLXXXIII', 'MLXXXIV', 'MLXXXV', 'MLXXXVI', 'MLXXXVII', 'MLXXXVIII', 'MLXXXIX', 'MXC', 'MXCI', 'MXCII', 'MXCIII', 'MXCIV', 'MVC', 'MVCI', 'MVCII', 'MVCIII', 'MIC', 'MC', 'MCI', 'MCII', 'MCIII', 'MCIV', 'MCV', 'MCVI', 'MCVII', 'MCVIII', 'MCIX', 'MCX', 'MCXI', 'MCXII', 'MCXIII', 'MCXIV', 'MCXV', 'MCXVI', 'MCXVII', 'MCXVIII', 'MCXIX', 'MCXX', 'MCXXI', 'MCXXII', 'MCXXIII', 'MCXXIV', 'MCXXV', 'MCXXVI', 'MCXXVII', 'MCXXVIII', 'MCXXIX', 'MCXXX', 'MCXXXI', 'MCXXXII', 'MCXXXIII', 'MCXXXIV', 'MCXXXV', 'MCXXXVI', 'MCXXXVII', 'MCXXXVIII', 'MCXXXIX', 'MCXL', 'MCXLI', 'MCXLII', 'MCXLIII', 'MCXLIV', 'MCVL', 'MCVLI', 'MCVLII', 'MCVLIII', 'MCIL', 'MCL', 'MCLI', 'MCLII', 'MCLIII', 'MCLIV', 'MCLV', 'MCLVI', 'MCLVII', 'MCLVIII', 'MCLIX', 'MCLX', 'MCLXI', 'MCLXII', 'MCLXIII', 'MCLXIV', 'MCLXV', 'MCLXVI', 'MCLXVII', 'MCLXVIII', 'MCLXIX', 'MCLXX', 'MCLXXI', 'MCLXXII', 'MCLXXIII', 'MCLXXIV', 'MCLXXV', 'MCLXXVI', 'MCLXXVII', 'MCLXXVIII', 'MCLXXIX', 'MCLXXX', 'MCLXXXI', 'MCLXXXII', 'MCLXXXIII', 'MCLXXXIV', 'MCLXXXV', 'MCLXXXVI', 'MCLXXXVII', 'MCLXXXVIII', 'MCLXXXIX', 'MCXC', 'MCXCI', 'MCXCII', 'MCXCIII', 'MCXCIV', 'MCVC', 'MCVCI', 'MCVCII', 'MCVCIII', 'MCIC', 'MCC', 'MCCI', 'MCCII', 'MCCIII', 'MCCIV', 'MCCV', 'MCCVI', 'MCCVII', 'MCCVIII', 'MCCIX', 'MCCX', 'MCCXI', 'MCCXII', 'MCCXIII', 'MCCXIV', 'MCCXV', 'MCCXVI', 'MCCXVII', 'MCCXVIII', 'MCCXIX', 'MCCXX', 'MCCXXI', 'MCCXXII', 'MCCXXIII', 'MCCXXIV', 'MCCXXV', 'MCCXXVI', 'MCCXXVII', 'MCCXXVIII', 'MCCXXIX', 'MCCXXX', 'MCCXXXI', 'MCCXXXII', 'MCCXXXIII', 'MCCXXXIV', 'MCCXXXV', 'MCCXXXVI', 'MCCXXXVII', 'MCCXXXVIII', 'MCCXXXIX', 'MCCXL', 'MCCXLI', 'MCCXLII', 'MCCXLIII', 'MCCXLIV', 'MCCVL', 'MCCVLI', 'MCCVLII', 'MCCVLIII', 'MCCIL', 'MCCL', 'MCCLI', 'MCCLII', 'MCCLIII', 'MCCLIV', 'MCCLV', 'MCCLVI', 'MCCLVII', 'MCCLVIII', 'MCCLIX', 'MCCLX', 'MCCLXI', 'MCCLXII', 'MCCLXIII', 'MCCLXIV', 'MCCLXV', 'MCCLXVI', 'MCCLXVII', 'MCCLXVIII', 'MCCLXIX', 'MCCLXX', 'MCCLXXI', 'MCCLXXII', 'MCCLXXIII', 'MCCLXXIV', 'MCCLXXV', 'MCCLXXVI', 'MCCLXXVII', 'MCCLXXVIII', 'MCCLXXIX', 'MCCLXXX', 'MCCLXXXI', 'MCCLXXXII', 'MCCLXXXIII', 'MCCLXXXIV', 'MCCLXXXV', 'MCCLXXXVI', 'MCCLXXXVII', 'MCCLXXXVIII', 'MCCLXXXIX', 'MCCXC', 'MCCXCI', 'MCCXCII', 'MCCXCIII', 'MCCXCIV', 'MCCVC', 'MCCVCI', 'MCCVCII', 'MCCVCIII', 'MCCIC', 'MCCC', 'MCCCI', 'MCCCII', 'MCCCIII', 'MCCCIV', 'MCCCV', 'MCCCVI', 'MCCCVII', 'MCCCVIII', 'MCCCIX', 'MCCCX', 'MCCCXI', 'MCCCXII', 'MCCCXIII', 'MCCCXIV', 'MCCCXV', 'MCCCXVI', 'MCCCXVII', 'MCCCXVIII', 'MCCCXIX', 'MCCCXX', 'MCCCXXI', 'MCCCXXII', 'MCCCXXIII', 'MCCCXXIV', 'MCCCXXV', 'MCCCXXVI', 'MCCCXXVII', 'MCCCXXVIII', 'MCCCXXIX', 'MCCCXXX', 'MCCCXXXI', 'MCCCXXXII', 'MCCCXXXIII', 'MCCCXXXIV', 'MCCCXXXV', 'MCCCXXXVI', 'MCCCXXXVII', 'MCCCXXXVIII', 'MCCCXXXIX', 'MCCCXL', 'MCCCXLI', 'MCCCXLII', 'MCCCXLIII', 'MCCCXLIV', 'MCCCVL', 'MCCCVLI', 'MCCCVLII', 'MCCCVLIII', 'MCCCIL', 'MCCCL', 'MCCCLI', 'MCCCLII', 'MCCCLIII', 'MCCCLIV', 'MCCCLV', 'MCCCLVI', 'MCCCLVII', 'MCCCLVIII', 'MCCCLIX', 'MCCCLX', 'MCCCLXI', 'MCCCLXII', 'MCCCLXIII', 'MCCCLXIV', 'MCCCLXV', 'MCCCLXVI', 'MCCCLXVII', 'MCCCLXVIII', 'MCCCLXIX', 'MCCCLXX', 'MCCCLXXI', 'MCCCLXXII', 'MCCCLXXIII', 'MCCCLXXIV', 'MCCCLXXV', 'MCCCLXXVI', 'MCCCLXXVII', 'MCCCLXXVIII', 'MCCCLXXIX', 'MCCCLXXX', 'MCCCLXXXI', 'MCCCLXXXII', 'MCCCLXXXIII', 'MCCCLXXXIV', 'MCCCLXXXV', 'MCCCLXXXVI', 'MCCCLXXXVII', 'MCCCLXXXVIII', 'MCCCLXXXIX', 'MCCCXC', 'MCCCXCI', 'MCCCXCII', 'MCCCXCIII', 'MCCCXCIV', 'MCCCVC', 'MCCCVCI', 'MCCCVCII', 'MCCCVCIII', 'MCCCIC', 'MCD', 'MCDI', 'MCDII', 'MCDIII', 'MCDIV', 'MCDV', 'MCDVI', 'MCDVII', 'MCDVIII', 'MCDIX', 'MCDX', 'MCDXI', 'MCDXII', 'MCDXIII', 'MCDXIV', 'MCDXV', 'MCDXVI', 'MCDXVII', 'MCDXVIII', 'MCDXIX', 'MCDXX', 'MCDXXI', 'MCDXXII', 'MCDXXIII', 'MCDXXIV', 'MCDXXV', 'MCDXXVI', 'MCDXXVII', 'MCDXXVIII', 'MCDXXIX', 'MCDXXX', 'MCDXXXI', 'MCDXXXII', 'MCDXXXIII', 'MCDXXXIV', 'MCDXXXV', 'MCDXXXVI', 'MCDXXXVII', 'MCDXXXVIII', 'MCDXXXIX', 'MCDXL', 'MCDXLI', 'MCDXLII', 'MCDXLIII', 'MCDXLIV', 'MCDVL', 'MCDVLI', 'MCDVLII', 'MCDVLIII', 'MCDIL', 'MLD', 'MLDI', 'MLDII', 'MLDIII', 'MLDIV', 'MLDV', 'MLDVI', 'MLDVII', 'MLDVIII', 'MLDIX', 'MLDX', 'MLDXI', 'MLDXII', 'MLDXIII', 'MLDXIV', 'MLDXV', 'MLDXVI', 'MLDXVII', 'MLDXVIII', 'MLDXIX', 'MLDXX', 'MLDXXI', 'MLDXXII', 'MLDXXIII', 'MLDXXIV', 'MLDXXV', 'MLDXXVI', 'MLDXXVII', 'MLDXXVIII', 'MLDXXIX', 'MLDXXX', 'MLDXXXI', 'MLDXXXII', 'MLDXXXIII', 'MLDXXXIV', 'MLDXXXV', 'MLDXXXVI', 'MLDXXXVII', 'MLDXXXVIII', 'MLDXXXIX', 'MXD', 'MXDI', 'MXDII', 'MXDIII', 'MXDIV', 'MVD', 'MVDI', 'MVDII', 'MVDIII', 'MVDIV', 'MD', 'MDI', 'MDII', 'MDIII', 'MDIV', 'MDV', 'MDVI', 'MDVII', 'MDVIII', 'MDIX', 'MDX', 'MDXI', 'MDXII', 'MDXIII', 'MDXIV', 'MDXV', 'MDXVI', 'MDXVII', 'MDXVIII', 'MDXIX', 'MDXX', 'MDXXI', 'MDXXII', 'MDXXIII', 'MDXXIV', 'MDXXV', 'MDXXVI', 'MDXXVII', 'MDXXVIII', 'MDXXIX', 'MDXXX', 'MDXXXI', 'MDXXXII', 'MDXXXIII', 'MDXXXIV', 'MDXXXV', 'MDXXXVI', 'MDXXXVII', 'MDXXXVIII', 'MDXXXIX', 'MDXL', 'MDXLI', 'MDXLII', 'MDXLIII', 'MDXLIV', 'MDVL', 'MDVLI', 'MDVLII', 'MDVLIII', 'MDIL', 'MDL', 'MDLI', 'MDLII', 'MDLIII', 'MDLIV', 'MDLV', 'MDLVI', 'MDLVII', 'MDLVIII', 'MDLIX', 'MDLX', 'MDLXI', 'MDLXII', 'MDLXIII', 'MDLXIV', 'MDLXV', 'MDLXVI', 'MDLXVII', 'MDLXVIII', 'MDLXIX', 'MDLXX', 'MDLXXI', 'MDLXXII', 'MDLXXIII', 'MDLXXIV', 'MDLXXV', 'MDLXXVI', 'MDLXXVII', 'MDLXXVIII', 'MDLXXIX', 'MDLXXX', 'MDLXXXI', 'MDLXXXII', 'MDLXXXIII', 'MDLXXXIV', 'MDLXXXV', 'MDLXXXVI', 'MDLXXXVII', 'MDLXXXVIII', 'MDLXXXIX', 'MDXC', 'MDXCI', 'MDXCII', 'MDXCIII', 'MDXCIV', 'MDVC', 'MDVCI', 'MDVCII', 'MDVCIII', 'MDIC', 'MDC', 'MDCI', 'MDCII', 'MDCIII', 'MDCIV', 'MDCV', 'MDCVI', 'MDCVII', 'MDCVIII', 'MDCIX', 'MDCX', 'MDCXI', 'MDCXII', 'MDCXIII', 'MDCXIV', 'MDCXV', 'MDCXVI', 'MDCXVII', 'MDCXVIII', 'MDCXIX', 'MDCXX', 'MDCXXI', 'MDCXXII', 'MDCXXIII', 'MDCXXIV', 'MDCXXV', 'MDCXXVI', 'MDCXXVII', 'MDCXXVIII', 'MDCXXIX', 'MDCXXX', 'MDCXXXI', 'MDCXXXII', 'MDCXXXIII', 'MDCXXXIV', 'MDCXXXV', 'MDCXXXVI', 'MDCXXXVII', 'MDCXXXVIII', 'MDCXXXIX', 'MDCXL', 'MDCXLI', 'MDCXLII', 'MDCXLIII', 'MDCXLIV', 'MDCVL', 'MDCVLI', 'MDCVLII', 'MDCVLIII', 'MDCIL', 'MDCL', 'MDCLI', 'MDCLII', 'MDCLIII', 'MDCLIV', 'MDCLV', 'MDCLVI', 'MDCLVII', 'MDCLVIII', 'MDCLIX', 'MDCLX', 'MDCLXI', 'MDCLXII', 'MDCLXIII', 'MDCLXIV', 'MDCLXV', 'MDCLXVI', 'MDCLXVII', 'MDCLXVIII', 'MDCLXIX', 'MDCLXX', 'MDCLXXI', 'MDCLXXII', 'MDCLXXIII', 'MDCLXXIV', 'MDCLXXV', 'MDCLXXVI', 'MDCLXXVII', 'MDCLXXVIII', 'MDCLXXIX', 'MDCLXXX', 'MDCLXXXI', 'MDCLXXXII', 'MDCLXXXIII', 'MDCLXXXIV', 'MDCLXXXV', 'MDCLXXXVI', 'MDCLXXXVII', 'MDCLXXXVIII', 'MDCLXXXIX', 'MDCXC', 'MDCXCI', 'MDCXCII', 'MDCXCIII', 'MDCXCIV', 'MDCVC', 'MDCVCI', 'MDCVCII', 'MDCVCIII', 'MDCIC', 'MDCC', 'MDCCI', 'MDCCII', 'MDCCIII', 'MDCCIV', 'MDCCV', 'MDCCVI', 'MDCCVII', 'MDCCVIII', 'MDCCIX', 'MDCCX', 'MDCCXI', 'MDCCXII', 'MDCCXIII', 'MDCCXIV', 'MDCCXV', 'MDCCXVI', 'MDCCXVII', 'MDCCXVIII', 'MDCCXIX', 'MDCCXX', 'MDCCXXI', 'MDCCXXII', 'MDCCXXIII', 'MDCCXXIV', 'MDCCXXV', 'MDCCXXVI', 'MDCCXXVII', 'MDCCXXVIII', 'MDCCXXIX', 'MDCCXXX', 'MDCCXXXI', 'MDCCXXXII', 'MDCCXXXIII', 'MDCCXXXIV', 'MDCCXXXV', 'MDCCXXXVI', 'MDCCXXXVII', 'MDCCXXXVIII', 'MDCCXXXIX', 'MDCCXL', 'MDCCXLI', 'MDCCXLII', 'MDCCXLIII', 'MDCCXLIV', 'MDCCVL', 'MDCCVLI', 'MDCCVLII', 'MDCCVLIII', 'MDCCIL', 'MDCCL', 'MDCCLI', 'MDCCLII', 'MDCCLIII', 'MDCCLIV', 'MDCCLV', 'MDCCLVI', 'MDCCLVII', 'MDCCLVIII', 'MDCCLIX', 'MDCCLX', 'MDCCLXI', 'MDCCLXII', 'MDCCLXIII', 'MDCCLXIV', 'MDCCLXV', 'MDCCLXVI', 'MDCCLXVII', 'MDCCLXVIII', 'MDCCLXIX', 'MDCCLXX', 'MDCCLXXI', 'MDCCLXXII', 'MDCCLXXIII', 'MDCCLXXIV', 'MDCCLXXV', 'MDCCLXXVI', 'MDCCLXXVII', 'MDCCLXXVIII', 'MDCCLXXIX', 'MDCCLXXX', 'MDCCLXXXI', 'MDCCLXXXII', 'MDCCLXXXIII', 'MDCCLXXXIV', 'MDCCLXXXV', 'MDCCLXXXVI', 'MDCCLXXXVII', 'MDCCLXXXVIII', 'MDCCLXXXIX', 'MDCCXC', 'MDCCXCI', 'MDCCXCII', 'MDCCXCIII', 'MDCCXCIV', 'MDCCVC', 'MDCCVCI', 'MDCCVCII', 'MDCCVCIII', 'MDCCIC', 'MDCCC', 'MDCCCI', 'MDCCCII', 'MDCCCIII', 'MDCCCIV', 'MDCCCV', 'MDCCCVI', 'MDCCCVII', 'MDCCCVIII', 'MDCCCIX', 'MDCCCX', 'MDCCCXI', 'MDCCCXII', 'MDCCCXIII', 'MDCCCXIV', 'MDCCCXV', 'MDCCCXVI', 'MDCCCXVII', 'MDCCCXVIII', 'MDCCCXIX', 'MDCCCXX', 'MDCCCXXI', 'MDCCCXXII', 'MDCCCXXIII', 'MDCCCXXIV', 'MDCCCXXV', 'MDCCCXXVI', 'MDCCCXXVII', 'MDCCCXXVIII', 'MDCCCXXIX', 'MDCCCXXX', 'MDCCCXXXI', 'MDCCCXXXII', 'MDCCCXXXIII', 'MDCCCXXXIV', 'MDCCCXXXV', 'MDCCCXXXVI', 'MDCCCXXXVII', 'MDCCCXXXVIII', 'MDCCCXXXIX', 'MDCCCXL', 'MDCCCXLI', 'MDCCCXLII', 'MDCCCXLIII', 'MDCCCXLIV', 'MDCCCVL', 'MDCCCVLI', 'MDCCCVLII', 'MDCCCVLIII', 'MDCCCIL', 'MDCCCL', 'MDCCCLI', 'MDCCCLII', 'MDCCCLIII', 'MDCCCLIV', 'MDCCCLV', 'MDCCCLVI', 'MDCCCLVII', 'MDCCCLVIII', 'MDCCCLIX', 'MDCCCLX', 'MDCCCLXI', 'MDCCCLXII', 'MDCCCLXIII', 'MDCCCLXIV', 'MDCCCLXV', 'MDCCCLXVI', 'MDCCCLXVII', 'MDCCCLXVIII', 'MDCCCLXIX', 'MDCCCLXX', 'MDCCCLXXI', 'MDCCCLXXII', 'MDCCCLXXIII', 'MDCCCLXXIV', 'MDCCCLXXV', 'MDCCCLXXVI', 'MDCCCLXXVII', 'MDCCCLXXVIII', 'MDCCCLXXIX', 'MDCCCLXXX', 'MDCCCLXXXI', 'MDCCCLXXXII', 'MDCCCLXXXIII', 'MDCCCLXXXIV', 'MDCCCLXXXV', 'MDCCCLXXXVI', 'MDCCCLXXXVII', 'MDCCCLXXXVIII', 'MDCCCLXXXIX', 'MDCCCXC', 'MDCCCXCI', 'MDCCCXCII', 'MDCCCXCIII', 'MDCCCXCIV', 'MDCCCVC', 'MDCCCVCI', 'MDCCCVCII', 'MDCCCVCIII', 'MDCCCIC', 'MCM', 'MCMI', 'MCMII', 'MCMIII', 'MCMIV', 'MCMV', 'MCMVI', 'MCMVII', 'MCMVIII', 'MCMIX', 'MCMX', 'MCMXI', 'MCMXII', 'MCMXIII', 'MCMXIV', 'MCMXV', 'MCMXVI', 'MCMXVII', 'MCMXVIII', 'MCMXIX', 'MCMXX', 'MCMXXI', 'MCMXXII', 'MCMXXIII', 'MCMXXIV', 'MCMXXV', 'MCMXXVI', 'MCMXXVII', 'MCMXXVIII', 'MCMXXIX', 'MCMXXX', 'MCMXXXI', 'MCMXXXII', 'MCMXXXIII', 'MCMXXXIV', 'MCMXXXV', 'MCMXXXVI', 'MCMXXXVII', 'MCMXXXVIII', 'MCMXXXIX', 'MCMXL', 'MCMXLI', 'MCMXLII', 'MCMXLIII', 'MCMXLIV', 'MCMVL', 'MCMVLI', 'MCMVLII', 'MCMVLIII', 'MCMIL', 'MLM', 'MLMI', 'MLMII', 'MLMIII', 'MLMIV', 'MLMV', 'MLMVI', 'MLMVII', 'MLMVIII', 'MLMIX', 'MLMX', 'MLMXI', 'MLMXII', 'MLMXIII', 'MLMXIV', 'MLMXV', 'MLMXVI', 'MLMXVII', 'MLMXVIII', 'MLMXIX', 'MLMXX', 'MLMXXI', 'MLMXXII', 'MLMXXIII', 'MLMXXIV', 'MLMXXV', 'MLMXXVI', 'MLMXXVII', 'MLMXXVIII', 'MLMXXIX', 'MLMXXX', 'MLMXXXI', 'MLMXXXII', 'MLMXXXIII', 'MLMXXXIV', 'MLMXXXV', 'MLMXXXVI', 'MLMXXXVII', 'MLMXXXVIII', 'MLMXXXIX', 'MXM', 'MXMI', 'MXMII', 'MXMIII', 'MXMIV', 'MVM', 'MVMI', 'MVMII', 'MVMIII', 'MVMIV', 'MM', 'MMI', 'MMII', 'MMIII', 'MMIV', 'MMV', 'MMVI', 'MMVII', 'MMVIII', 'MMIX', 'MMX', 'MMXI', 'MMXII', 'MMXIII', 'MMXIV', 'MMXV', 'MMXVI', 'MMXVII', 'MMXVIII', 'MMXIX', 'MMXX', 'MMXXI', 'MMXXII', 'MMXXIII', 'MMXXIV', 'MMXXV', 'MMXXVI', 'MMXXVII', 'MMXXVIII', 'MMXXIX', 'MMXXX', 'MMXXXI', 'MMXXXII', 'MMXXXIII', 'MMXXXIV', 'MMXXXV', 'MMXXXVI', 'MMXXXVII', 'MMXXXVIII', 'MMXXXIX', 'MMXL', 'MMXLI', 'MMXLII', 'MMXLIII', 'MMXLIV', 'MMVL', 'MMVLI', 'MMVLII', 'MMVLIII', 'MMIL', 'MML', 'MMLI', 'MMLII', 'MMLIII', 'MMLIV', 'MMLV', 'MMLVI', 'MMLVII', 'MMLVIII', 'MMLIX', 'MMLX', 'MMLXI', 'MMLXII', 'MMLXIII', 'MMLXIV', 'MMLXV', 'MMLXVI', 'MMLXVII', 'MMLXVIII', 'MMLXIX', 'MMLXX', 'MMLXXI', 'MMLXXII', 'MMLXXIII', 'MMLXXIV', 'MMLXXV', 'MMLXXVI', 'MMLXXVII', 'MMLXXVIII', 'MMLXXIX', 'MMLXXX', 'MMLXXXI', 'MMLXXXII', 'MMLXXXIII', 'MMLXXXIV', 'MMLXXXV', 'MMLXXXVI', 'MMLXXXVII', 'MMLXXXVIII', 'MMLXXXIX', 'MMXC', 'MMXCI', 'MMXCII', 'MMXCIII', 'MMXCIV', 'MMVC', 'MMVCI', 'MMVCII', 'MMVCIII', 'MMIC', 'MMC', 'MMCI', 'MMCII', 'MMCIII', 'MMCIV', 'MMCV', 'MMCVI', 'MMCVII', 'MMCVIII', 'MMCIX', 'MMCX', 'MMCXI', 'MMCXII', 'MMCXIII', 'MMCXIV', 'MMCXV', 'MMCXVI', 'MMCXVII', 'MMCXVIII', 'MMCXIX', 'MMCXX', 'MMCXXI', 'MMCXXII', 'MMCXXIII', 'MMCXXIV', 'MMCXXV', 'MMCXXVI', 'MMCXXVII', 'MMCXXVIII', 'MMCXXIX', 'MMCXXX', 'MMCXXXI', 'MMCXXXII', 'MMCXXXIII', 'MMCXXXIV', 'MMCXXXV', 'MMCXXXVI', 'MMCXXXVII', 'MMCXXXVIII', 'MMCXXXIX', 'MMCXL', 'MMCXLI', 'MMCXLII', 'MMCXLIII', 'MMCXLIV', 'MMCVL', 'MMCVLI', 'MMCVLII', 'MMCVLIII', 'MMCIL', 'MMCL', 'MMCLI', 'MMCLII', 'MMCLIII', 'MMCLIV', 'MMCLV', 'MMCLVI', 'MMCLVII', 'MMCLVIII', 'MMCLIX', 'MMCLX', 'MMCLXI', 'MMCLXII', 'MMCLXIII', 'MMCLXIV', 'MMCLXV', 'MMCLXVI', 'MMCLXVII', 'MMCLXVIII', 'MMCLXIX', 'MMCLXX', 'MMCLXXI', 'MMCLXXII', 'MMCLXXIII', 'MMCLXXIV', 'MMCLXXV', 'MMCLXXVI', 'MMCLXXVII', 'MMCLXXVIII', 'MMCLXXIX', 'MMCLXXX', 'MMCLXXXI', 'MMCLXXXII', 'MMCLXXXIII', 'MMCLXXXIV', 'MMCLXXXV', 'MMCLXXXVI', 'MMCLXXXVII', 'MMCLXXXVIII', 'MMCLXXXIX', 'MMCXC', 'MMCXCI', 'MMCXCII', 'MMCXCIII', 'MMCXCIV', 'MMCVC', 'MMCVCI', 'MMCVCII', 'MMCVCIII', 'MMCIC', 'MMCC', 'MMCCI', 'MMCCII', 'MMCCIII', 'MMCCIV', 'MMCCV', 'MMCCVI', 'MMCCVII', 'MMCCVIII', 'MMCCIX', 'MMCCX', 'MMCCXI', 'MMCCXII', 'MMCCXIII', 'MMCCXIV', 'MMCCXV', 'MMCCXVI', 'MMCCXVII', 'MMCCXVIII', 'MMCCXIX', 'MMCCXX', 'MMCCXXI', 'MMCCXXII', 'MMCCXXIII', 'MMCCXXIV', 'MMCCXXV', 'MMCCXXVI', 'MMCCXXVII', 'MMCCXXVIII', 'MMCCXXIX', 'MMCCXXX', 'MMCCXXXI', 'MMCCXXXII', 'MMCCXXXIII', 'MMCCXXXIV', 'MMCCXXXV', 'MMCCXXXVI', 'MMCCXXXVII', 'MMCCXXXVIII', 'MMCCXXXIX', 'MMCCXL', 'MMCCXLI', 'MMCCXLII', 'MMCCXLIII', 'MMCCXLIV', 'MMCCVL', 'MMCCVLI', 'MMCCVLII', 'MMCCVLIII', 'MMCCIL', 'MMCCL', 'MMCCLI', 'MMCCLII', 'MMCCLIII', 'MMCCLIV', 'MMCCLV', 'MMCCLVI', 'MMCCLVII', 'MMCCLVIII', 'MMCCLIX', 'MMCCLX', 'MMCCLXI', 'MMCCLXII', 'MMCCLXIII', 'MMCCLXIV', 'MMCCLXV', 'MMCCLXVI', 'MMCCLXVII', 'MMCCLXVIII', 'MMCCLXIX', 'MMCCLXX', 'MMCCLXXI', 'MMCCLXXII', 'MMCCLXXIII', 'MMCCLXXIV', 'MMCCLXXV', 'MMCCLXXVI', 'MMCCLXXVII', 'MMCCLXXVIII', 'MMCCLXXIX', 'MMCCLXXX', 'MMCCLXXXI', 'MMCCLXXXII', 'MMCCLXXXIII', 'MMCCLXXXIV', 'MMCCLXXXV', 'MMCCLXXXVI', 'MMCCLXXXVII', 'MMCCLXXXVIII', 'MMCCLXXXIX', 'MMCCXC', 'MMCCXCI', 'MMCCXCII', 'MMCCXCIII', 'MMCCXCIV', 'MMCCVC', 'MMCCVCI', 'MMCCVCII', 'MMCCVCIII', 'MMCCIC', 'MMCCC', 'MMCCCI', 'MMCCCII', 'MMCCCIII', 'MMCCCIV', 'MMCCCV', 'MMCCCVI', 'MMCCCVII', 'MMCCCVIII', 'MMCCCIX', 'MMCCCX', 'MMCCCXI', 'MMCCCXII', 'MMCCCXIII', 'MMCCCXIV', 'MMCCCXV', 'MMCCCXVI', 'MMCCCXVII', 'MMCCCXVIII', 'MMCCCXIX', 'MMCCCXX', 'MMCCCXXI', 'MMCCCXXII', 'MMCCCXXIII', 'MMCCCXXIV', 'MMCCCXXV', 'MMCCCXXVI', 'MMCCCXXVII', 'MMCCCXXVIII', 'MMCCCXXIX', 'MMCCCXXX', 'MMCCCXXXI', 'MMCCCXXXII', 'MMCCCXXXIII', 'MMCCCXXXIV', 'MMCCCXXXV', 'MMCCCXXXVI', 'MMCCCXXXVII', 'MMCCCXXXVIII', 'MMCCCXXXIX', 'MMCCCXL', 'MMCCCXLI', 'MMCCCXLII', 'MMCCCXLIII', 'MMCCCXLIV', 'MMCCCVL', 'MMCCCVLI', 'MMCCCVLII', 'MMCCCVLIII', 'MMCCCIL', 'MMCCCL', 'MMCCCLI', 'MMCCCLII', 'MMCCCLIII', 'MMCCCLIV', 'MMCCCLV', 'MMCCCLVI', 'MMCCCLVII', 'MMCCCLVIII', 'MMCCCLIX', 'MMCCCLX', 'MMCCCLXI', 'MMCCCLXII', 'MMCCCLXIII', 'MMCCCLXIV', 'MMCCCLXV', 'MMCCCLXVI', 'MMCCCLXVII', 'MMCCCLXVIII', 'MMCCCLXIX', 'MMCCCLXX', 'MMCCCLXXI', 'MMCCCLXXII', 'MMCCCLXXIII', 'MMCCCLXXIV', 'MMCCCLXXV', 'MMCCCLXXVI', 'MMCCCLXXVII', 'MMCCCLXXVIII', 'MMCCCLXXIX', 'MMCCCLXXX', 'MMCCCLXXXI', 'MMCCCLXXXII', 'MMCCCLXXXIII', 'MMCCCLXXXIV', 'MMCCCLXXXV', 'MMCCCLXXXVI', 'MMCCCLXXXVII', 'MMCCCLXXXVIII', 'MMCCCLXXXIX', 'MMCCCXC', 'MMCCCXCI', 'MMCCCXCII', 'MMCCCXCIII', 'MMCCCXCIV', 'MMCCCVC', 'MMCCCVCI', 'MMCCCVCII', 'MMCCCVCIII', 'MMCCCIC', 'MMCD', 'MMCDI', 'MMCDII', 'MMCDIII', 'MMCDIV', 'MMCDV', 'MMCDVI', 'MMCDVII', 'MMCDVIII', 'MMCDIX', 'MMCDX', 'MMCDXI', 'MMCDXII', 'MMCDXIII', 'MMCDXIV', 'MMCDXV', 'MMCDXVI', 'MMCDXVII', 'MMCDXVIII', 'MMCDXIX', 'MMCDXX', 'MMCDXXI', 'MMCDXXII', 'MMCDXXIII', 'MMCDXXIV', 'MMCDXXV', 'MMCDXXVI', 'MMCDXXVII', 'MMCDXXVIII', 'MMCDXXIX', 'MMCDXXX', 'MMCDXXXI', 'MMCDXXXII', 'MMCDXXXIII', 'MMCDXXXIV', 'MMCDXXXV', 'MMCDXXXVI', 'MMCDXXXVII', 'MMCDXXXVIII', 'MMCDXXXIX', 'MMCDXL', 'MMCDXLI', 'MMCDXLII', 'MMCDXLIII', 'MMCDXLIV', 'MMCDVL', 'MMCDVLI', 'MMCDVLII', 'MMCDVLIII', 'MMCDIL', 'MMLD', 'MMLDI', 'MMLDII', 'MMLDIII', 'MMLDIV', 'MMLDV', 'MMLDVI', 'MMLDVII', 'MMLDVIII', 'MMLDIX', 'MMLDX', 'MMLDXI', 'MMLDXII', 'MMLDXIII', 'MMLDXIV', 'MMLDXV', 'MMLDXVI', 'MMLDXVII', 'MMLDXVIII', 'MMLDXIX', 'MMLDXX', 'MMLDXXI', 'MMLDXXII', 'MMLDXXIII', 'MMLDXXIV', 'MMLDXXV', 'MMLDXXVI', 'MMLDXXVII', 'MMLDXXVIII', 'MMLDXXIX', 'MMLDXXX', 'MMLDXXXI', 'MMLDXXXII', 'MMLDXXXIII', 'MMLDXXXIV', 'MMLDXXXV', 'MMLDXXXVI', 'MMLDXXXVII', 'MMLDXXXVIII', 'MMLDXXXIX', 'MMXD', 'MMXDI', 'MMXDII', 'MMXDIII', 'MMXDIV', 'MMVD', 'MMVDI', 'MMVDII', 'MMVDIII', 'MMVDIV', 'MMD', 'MMDI', 'MMDII', 'MMDIII', 'MMDIV', 'MMDV', 'MMDVI', 'MMDVII', 'MMDVIII', 'MMDIX', 'MMDX', 'MMDXI', 'MMDXII', 'MMDXIII', 'MMDXIV', 'MMDXV', 'MMDXVI', 'MMDXVII', 'MMDXVIII', 'MMDXIX', 'MMDXX', 'MMDXXI', 'MMDXXII', 'MMDXXIII', 'MMDXXIV', 'MMDXXV', 'MMDXXVI', 'MMDXXVII', 'MMDXXVIII', 'MMDXXIX', 'MMDXXX', 'MMDXXXI', 'MMDXXXII', 'MMDXXXIII', 'MMDXXXIV', 'MMDXXXV', 'MMDXXXVI', 'MMDXXXVII', 'MMDXXXVIII', 'MMDXXXIX', 'MMDXL', 'MMDXLI', 'MMDXLII', 'MMDXLIII', 'MMDXLIV', 'MMDVL', 'MMDVLI', 'MMDVLII', 'MMDVLIII', 'MMDIL', 'MMDL', 'MMDLI', 'MMDLII', 'MMDLIII', 'MMDLIV', 'MMDLV', 'MMDLVI', 'MMDLVII', 'MMDLVIII', 'MMDLIX', 'MMDLX', 'MMDLXI', 'MMDLXII', 'MMDLXIII', 'MMDLXIV', 'MMDLXV', 'MMDLXVI', 'MMDLXVII', 'MMDLXVIII', 'MMDLXIX', 'MMDLXX', 'MMDLXXI', 'MMDLXXII', 'MMDLXXIII', 'MMDLXXIV', 'MMDLXXV', 'MMDLXXVI', 'MMDLXXVII', 'MMDLXXVIII', 'MMDLXXIX', 'MMDLXXX', 'MMDLXXXI', 'MMDLXXXII', 'MMDLXXXIII', 'MMDLXXXIV', 'MMDLXXXV', 'MMDLXXXVI', 'MMDLXXXVII', 'MMDLXXXVIII', 'MMDLXXXIX', 'MMDXC', 'MMDXCI', 'MMDXCII', 'MMDXCIII', 'MMDXCIV', 'MMDVC', 'MMDVCI', 'MMDVCII', 'MMDVCIII', 'MMDIC', 'MMDC', 'MMDCI', 'MMDCII', 'MMDCIII', 'MMDCIV', 'MMDCV', 'MMDCVI', 'MMDCVII', 'MMDCVIII', 'MMDCIX', 'MMDCX', 'MMDCXI', 'MMDCXII', 'MMDCXIII', 'MMDCXIV', 'MMDCXV', 'MMDCXVI', 'MMDCXVII', 'MMDCXVIII', 'MMDCXIX', 'MMDCXX', 'MMDCXXI', 'MMDCXXII', 'MMDCXXIII', 'MMDCXXIV', 'MMDCXXV', 'MMDCXXVI', 'MMDCXXVII', 'MMDCXXVIII', 'MMDCXXIX', 'MMDCXXX', 'MMDCXXXI', 'MMDCXXXII', 'MMDCXXXIII', 'MMDCXXXIV', 'MMDCXXXV', 'MMDCXXXVI', 'MMDCXXXVII', 'MMDCXXXVIII', 'MMDCXXXIX', 'MMDCXL', 'MMDCXLI', 'MMDCXLII', 'MMDCXLIII', 'MMDCXLIV', 'MMDCVL', 'MMDCVLI', 'MMDCVLII', 'MMDCVLIII', 'MMDCIL', 'MMDCL', 'MMDCLI', 'MMDCLII', 'MMDCLIII', 'MMDCLIV', 'MMDCLV', 'MMDCLVI', 'MMDCLVII', 'MMDCLVIII', 'MMDCLIX', 'MMDCLX', 'MMDCLXI', 'MMDCLXII', 'MMDCLXIII', 'MMDCLXIV', 'MMDCLXV', 'MMDCLXVI', 'MMDCLXVII', 'MMDCLXVIII', 'MMDCLXIX', 'MMDCLXX', 'MMDCLXXI', 'MMDCLXXII', 'MMDCLXXIII', 'MMDCLXXIV', 'MMDCLXXV', 'MMDCLXXVI', 'MMDCLXXVII', 'MMDCLXXVIII', 'MMDCLXXIX', 'MMDCLXXX', 'MMDCLXXXI', 'MMDCLXXXII', 'MMDCLXXXIII', 'MMDCLXXXIV', 'MMDCLXXXV', 'MMDCLXXXVI', 'MMDCLXXXVII', 'MMDCLXXXVIII', 'MMDCLXXXIX', 'MMDCXC', 'MMDCXCI', 'MMDCXCII', 'MMDCXCIII', 'MMDCXCIV', 'MMDCVC', 'MMDCVCI', 'MMDCVCII', 'MMDCVCIII', 'MMDCIC', 'MMDCC', 'MMDCCI', 'MMDCCII', 'MMDCCIII', 'MMDCCIV', 'MMDCCV', 'MMDCCVI', 'MMDCCVII', 'MMDCCVIII', 'MMDCCIX', 'MMDCCX', 'MMDCCXI', 'MMDCCXII', 'MMDCCXIII', 'MMDCCXIV', 'MMDCCXV', 'MMDCCXVI', 'MMDCCXVII', 'MMDCCXVIII', 'MMDCCXIX', 'MMDCCXX', 'MMDCCXXI', 'MMDCCXXII', 'MMDCCXXIII', 'MMDCCXXIV', 'MMDCCXXV', 'MMDCCXXVI', 'MMDCCXXVII', 'MMDCCXXVIII', 'MMDCCXXIX', 'MMDCCXXX', 'MMDCCXXXI', 'MMDCCXXXII', 'MMDCCXXXIII', 'MMDCCXXXIV', 'MMDCCXXXV', 'MMDCCXXXVI', 'MMDCCXXXVII', 'MMDCCXXXVIII', 'MMDCCXXXIX', 'MMDCCXL', 'MMDCCXLI', 'MMDCCXLII', 'MMDCCXLIII', 'MMDCCXLIV', 'MMDCCVL', 'MMDCCVLI', 'MMDCCVLII', 'MMDCCVLIII', 'MMDCCIL', 'MMDCCL', 'MMDCCLI', 'MMDCCLII', 'MMDCCLIII', 'MMDCCLIV', 'MMDCCLV', 'MMDCCLVI', 'MMDCCLVII', 'MMDCCLVIII', 'MMDCCLIX', 'MMDCCLX', 'MMDCCLXI', 'MMDCCLXII', 'MMDCCLXIII', 'MMDCCLXIV', 'MMDCCLXV', 'MMDCCLXVI', 'MMDCCLXVII', 'MMDCCLXVIII', 'MMDCCLXIX', 'MMDCCLXX', 'MMDCCLXXI', 'MMDCCLXXII', 'MMDCCLXXIII', 'MMDCCLXXIV', 'MMDCCLXXV', 'MMDCCLXXVI', 'MMDCCLXXVII', 'MMDCCLXXVIII', 'MMDCCLXXIX', 'MMDCCLXXX', 'MMDCCLXXXI', 'MMDCCLXXXII', 'MMDCCLXXXIII', 'MMDCCLXXXIV', 'MMDCCLXXXV', 'MMDCCLXXXVI', 'MMDCCLXXXVII', 'MMDCCLXXXVIII', 'MMDCCLXXXIX', 'MMDCCXC', 'MMDCCXCI', 'MMDCCXCII', 'MMDCCXCIII', 'MMDCCXCIV', 'MMDCCVC', 'MMDCCVCI', 'MMDCCVCII', 'MMDCCVCIII', 'MMDCCIC', 'MMDCCC', 'MMDCCCI', 'MMDCCCII', 'MMDCCCIII', 'MMDCCCIV', 'MMDCCCV', 'MMDCCCVI', 'MMDCCCVII', 'MMDCCCVIII', 'MMDCCCIX', 'MMDCCCX', 'MMDCCCXI', 'MMDCCCXII', 'MMDCCCXIII', 'MMDCCCXIV', 'MMDCCCXV', 'MMDCCCXVI', 'MMDCCCXVII', 'MMDCCCXVIII', 'MMDCCCXIX', 'MMDCCCXX', 'MMDCCCXXI', 'MMDCCCXXII', 'MMDCCCXXIII', 'MMDCCCXXIV', 'MMDCCCXXV', 'MMDCCCXXVI', 'MMDCCCXXVII', 'MMDCCCXXVIII', 'MMDCCCXXIX', 'MMDCCCXXX', 'MMDCCCXXXI', 'MMDCCCXXXII', 'MMDCCCXXXIII', 'MMDCCCXXXIV', 'MMDCCCXXXV', 'MMDCCCXXXVI', 'MMDCCCXXXVII', 'MMDCCCXXXVIII', 'MMDCCCXXXIX', 'MMDCCCXL', 'MMDCCCXLI', 'MMDCCCXLII', 'MMDCCCXLIII', 'MMDCCCXLIV', 'MMDCCCVL', 'MMDCCCVLI', 'MMDCCCVLII', 'MMDCCCVLIII', 'MMDCCCIL', 'MMDCCCL', 'MMDCCCLI', 'MMDCCCLII', 'MMDCCCLIII', 'MMDCCCLIV', 'MMDCCCLV', 'MMDCCCLVI', 'MMDCCCLVII', 'MMDCCCLVIII', 'MMDCCCLIX', 'MMDCCCLX', 'MMDCCCLXI', 'MMDCCCLXII', 'MMDCCCLXIII', 'MMDCCCLXIV', 'MMDCCCLXV', 'MMDCCCLXVI', 'MMDCCCLXVII', 'MMDCCCLXVIII', 'MMDCCCLXIX', 'MMDCCCLXX', 'MMDCCCLXXI', 'MMDCCCLXXII', 'MMDCCCLXXIII', 'MMDCCCLXXIV', 'MMDCCCLXXV', 'MMDCCCLXXVI', 'MMDCCCLXXVII', 'MMDCCCLXXVIII', 'MMDCCCLXXIX', 'MMDCCCLXXX', 'MMDCCCLXXXI', 'MMDCCCLXXXII', 'MMDCCCLXXXIII', 'MMDCCCLXXXIV', 'MMDCCCLXXXV', 'MMDCCCLXXXVI', 'MMDCCCLXXXVII', 'MMDCCCLXXXVIII', 'MMDCCCLXXXIX', 'MMDCCCXC', 'MMDCCCXCI', 'MMDCCCXCII', 'MMDCCCXCIII', 'MMDCCCXCIV', 'MMDCCCVC', 'MMDCCCVCI', 'MMDCCCVCII', 'MMDCCCVCIII', 'MMDCCCIC', 'MMCM', 'MMCMI', 'MMCMII', 'MMCMIII', 'MMCMIV', 'MMCMV', 'MMCMVI', 'MMCMVII', 'MMCMVIII', 'MMCMIX', 'MMCMX', 'MMCMXI', 'MMCMXII', 'MMCMXIII', 'MMCMXIV', 'MMCMXV', 'MMCMXVI', 'MMCMXVII', 'MMCMXVIII', 'MMCMXIX', 'MMCMXX', 'MMCMXXI', 'MMCMXXII', 'MMCMXXIII', 'MMCMXXIV', 'MMCMXXV', 'MMCMXXVI', 'MMCMXXVII', 'MMCMXXVIII', 'MMCMXXIX', 'MMCMXXX', 'MMCMXXXI', 'MMCMXXXII', 'MMCMXXXIII', 'MMCMXXXIV', 'MMCMXXXV', 'MMCMXXXVI', 'MMCMXXXVII', 'MMCMXXXVIII', 'MMCMXXXIX', 'MMCMXL', 'MMCMXLI', 'MMCMXLII', 'MMCMXLIII', 'MMCMXLIV', 'MMCMVL', 'MMCMVLI', 'MMCMVLII', 'MMCMVLIII', 'MMCMIL', 'MMLM', 'MMLMI', 'MMLMII', 'MMLMIII', 'MMLMIV', 'MMLMV', 'MMLMVI', 'MMLMVII', 'MMLMVIII', 'MMLMIX', 'MMLMX', 'MMLMXI', 'MMLMXII', 'MMLMXIII', 'MMLMXIV', 'MMLMXV', 'MMLMXVI', 'MMLMXVII', 'MMLMXVIII', 'MMLMXIX', 'MMLMXX', 'MMLMXXI', 'MMLMXXII', 'MMLMXXIII', 'MMLMXXIV', 'MMLMXXV', 'MMLMXXVI', 'MMLMXXVII', 'MMLMXXVIII', 'MMLMXXIX', 'MMLMXXX', 'MMLMXXXI', 'MMLMXXXII', 'MMLMXXXIII', 'MMLMXXXIV', 'MMLMXXXV', 'MMLMXXXVI', 'MMLMXXXVII', 'MMLMXXXVIII', 'MMLMXXXIX', 'MMXM', 'MMXMI', 'MMXMII', 'MMXMIII', 'MMXMIV', 'MMVM', 'MMVMI', 'MMVMII', 'MMVMIII', 'MMVMIV', 'MMM', 'MMMI', 'MMMII', 'MMMIII', 'MMMIV', 'MMMV', 'MMMVI', 'MMMVII', 'MMMVIII', 'MMMIX', 'MMMX', 'MMMXI', 'MMMXII', 'MMMXIII', 'MMMXIV', 'MMMXV', 'MMMXVI', 'MMMXVII', 'MMMXVIII', 'MMMXIX', 'MMMXX', 'MMMXXI', 'MMMXXII', 'MMMXXIII', 'MMMXXIV', 'MMMXXV', 'MMMXXVI', 'MMMXXVII', 'MMMXXVIII', 'MMMXXIX', 'MMMXXX', 'MMMXXXI', 'MMMXXXII', 'MMMXXXIII', 'MMMXXXIV', 'MMMXXXV', 'MMMXXXVI', 'MMMXXXVII', 'MMMXXXVIII', 'MMMXXXIX', 'MMMXL', 'MMMXLI', 'MMMXLII', 'MMMXLIII', 'MMMXLIV', 'MMMVL', 'MMMVLI', 'MMMVLII', 'MMMVLIII', 'MMMIL', 'MMML', 'MMMLI', 'MMMLII', 'MMMLIII', 'MMMLIV', 'MMMLV', 'MMMLVI', 'MMMLVII', 'MMMLVIII', 'MMMLIX', 'MMMLX', 'MMMLXI', 'MMMLXII', 'MMMLXIII', 'MMMLXIV', 'MMMLXV', 'MMMLXVI', 'MMMLXVII', 'MMMLXVIII', 'MMMLXIX', 'MMMLXX', 'MMMLXXI', 'MMMLXXII', 'MMMLXXIII', 'MMMLXXIV', 'MMMLXXV', 'MMMLXXVI', 'MMMLXXVII', 'MMMLXXVIII', 'MMMLXXIX', 'MMMLXXX', 'MMMLXXXI', 'MMMLXXXII', 'MMMLXXXIII', 'MMMLXXXIV', 'MMMLXXXV', 'MMMLXXXVI', 'MMMLXXXVII', 'MMMLXXXVIII', 'MMMLXXXIX', 'MMMXC', 'MMMXCI', 'MMMXCII', 'MMMXCIII', 'MMMXCIV', 'MMMVC', 'MMMVCI', 'MMMVCII', 'MMMVCIII', 'MMMIC', 'MMMC', 'MMMCI', 'MMMCII', 'MMMCIII', 'MMMCIV', 'MMMCV', 'MMMCVI', 'MMMCVII', 'MMMCVIII', 'MMMCIX', 'MMMCX', 'MMMCXI', 'MMMCXII', 'MMMCXIII', 'MMMCXIV', 'MMMCXV', 'MMMCXVI', 'MMMCXVII', 'MMMCXVIII', 'MMMCXIX', 'MMMCXX', 'MMMCXXI', 'MMMCXXII', 'MMMCXXIII', 'MMMCXXIV', 'MMMCXXV', 'MMMCXXVI', 'MMMCXXVII', 'MMMCXXVIII', 'MMMCXXIX', 'MMMCXXX', 'MMMCXXXI', 'MMMCXXXII', 'MMMCXXXIII', 'MMMCXXXIV', 'MMMCXXXV', 'MMMCXXXVI', 'MMMCXXXVII', 'MMMCXXXVIII', 'MMMCXXXIX', 'MMMCXL', 'MMMCXLI', 'MMMCXLII', 'MMMCXLIII', 'MMMCXLIV', 'MMMCVL', 'MMMCVLI', 'MMMCVLII', 'MMMCVLIII', 'MMMCIL', 'MMMCL', 'MMMCLI', 'MMMCLII', 'MMMCLIII', 'MMMCLIV', 'MMMCLV', 'MMMCLVI', 'MMMCLVII', 'MMMCLVIII', 'MMMCLIX', 'MMMCLX', 'MMMCLXI', 'MMMCLXII', 'MMMCLXIII', 'MMMCLXIV', 'MMMCLXV', 'MMMCLXVI', 'MMMCLXVII', 'MMMCLXVIII', 'MMMCLXIX', 'MMMCLXX', 'MMMCLXXI', 'MMMCLXXII', 'MMMCLXXIII', 'MMMCLXXIV', 'MMMCLXXV', 'MMMCLXXVI', 'MMMCLXXVII', 'MMMCLXXVIII', 'MMMCLXXIX', 'MMMCLXXX', 'MMMCLXXXI', 'MMMCLXXXII', 'MMMCLXXXIII', 'MMMCLXXXIV', 'MMMCLXXXV', 'MMMCLXXXVI', 'MMMCLXXXVII', 'MMMCLXXXVIII', 'MMMCLXXXIX', 'MMMCXC', 'MMMCXCI', 'MMMCXCII', 'MMMCXCIII', 'MMMCXCIV', 'MMMCVC', 'MMMCVCI', 'MMMCVCII', 'MMMCVCIII', 'MMMCIC', 'MMMCC', 'MMMCCI', 'MMMCCII', 'MMMCCIII', 'MMMCCIV', 'MMMCCV', 'MMMCCVI', 'MMMCCVII', 'MMMCCVIII', 'MMMCCIX', 'MMMCCX', 'MMMCCXI', 'MMMCCXII', 'MMMCCXIII', 'MMMCCXIV', 'MMMCCXV', 'MMMCCXVI', 'MMMCCXVII', 'MMMCCXVIII', 'MMMCCXIX', 'MMMCCXX', 'MMMCCXXI', 'MMMCCXXII', 'MMMCCXXIII', 'MMMCCXXIV', 'MMMCCXXV', 'MMMCCXXVI', 'MMMCCXXVII', 'MMMCCXXVIII', 'MMMCCXXIX', 'MMMCCXXX', 'MMMCCXXXI', 'MMMCCXXXII', 'MMMCCXXXIII', 'MMMCCXXXIV', 'MMMCCXXXV', 'MMMCCXXXVI', 'MMMCCXXXVII', 'MMMCCXXXVIII', 'MMMCCXXXIX', 'MMMCCXL', 'MMMCCXLI', 'MMMCCXLII', 'MMMCCXLIII', 'MMMCCXLIV', 'MMMCCVL', 'MMMCCVLI', 'MMMCCVLII', 'MMMCCVLIII', 'MMMCCIL', 'MMMCCL', 'MMMCCLI', 'MMMCCLII', 'MMMCCLIII', 'MMMCCLIV', 'MMMCCLV', 'MMMCCLVI', 'MMMCCLVII', 'MMMCCLVIII', 'MMMCCLIX', 'MMMCCLX', 'MMMCCLXI', 'MMMCCLXII', 'MMMCCLXIII', 'MMMCCLXIV', 'MMMCCLXV', 'MMMCCLXVI', 'MMMCCLXVII', 'MMMCCLXVIII', 'MMMCCLXIX', 'MMMCCLXX', 'MMMCCLXXI', 'MMMCCLXXII', 'MMMCCLXXIII', 'MMMCCLXXIV', 'MMMCCLXXV', 'MMMCCLXXVI', 'MMMCCLXXVII', 'MMMCCLXXVIII', 'MMMCCLXXIX', 'MMMCCLXXX', 'MMMCCLXXXI', 'MMMCCLXXXII', 'MMMCCLXXXIII', 'MMMCCLXXXIV', 'MMMCCLXXXV', 'MMMCCLXXXVI', 'MMMCCLXXXVII', 'MMMCCLXXXVIII', 'MMMCCLXXXIX', 'MMMCCXC', 'MMMCCXCI', 'MMMCCXCII', 'MMMCCXCIII', 'MMMCCXCIV', 'MMMCCVC', 'MMMCCVCI', 'MMMCCVCII', 'MMMCCVCIII', 'MMMCCIC', 'MMMCCC', 'MMMCCCI', 'MMMCCCII', 'MMMCCCIII', 'MMMCCCIV', 'MMMCCCV', 'MMMCCCVI', 'MMMCCCVII', 'MMMCCCVIII', 'MMMCCCIX', 'MMMCCCX', 'MMMCCCXI', 'MMMCCCXII', 'MMMCCCXIII', 'MMMCCCXIV', 'MMMCCCXV', 'MMMCCCXVI', 'MMMCCCXVII', 'MMMCCCXVIII', 'MMMCCCXIX', 'MMMCCCXX', 'MMMCCCXXI', 'MMMCCCXXII', 'MMMCCCXXIII', 'MMMCCCXXIV', 'MMMCCCXXV', 'MMMCCCXXVI', 'MMMCCCXXVII', 'MMMCCCXXVIII', 'MMMCCCXXIX', 'MMMCCCXXX', 'MMMCCCXXXI', 'MMMCCCXXXII', 'MMMCCCXXXIII', 'MMMCCCXXXIV', 'MMMCCCXXXV', 'MMMCCCXXXVI', 'MMMCCCXXXVII', 'MMMCCCXXXVIII', 'MMMCCCXXXIX', 'MMMCCCXL', 'MMMCCCXLI', 'MMMCCCXLII', 'MMMCCCXLIII', 'MMMCCCXLIV', 'MMMCCCVL', 'MMMCCCVLI', 'MMMCCCVLII', 'MMMCCCVLIII', 'MMMCCCIL', 'MMMCCCL', 'MMMCCCLI', 'MMMCCCLII', 'MMMCCCLIII', 'MMMCCCLIV', 'MMMCCCLV', 'MMMCCCLVI', 'MMMCCCLVII', 'MMMCCCLVIII', 'MMMCCCLIX', 'MMMCCCLX', 'MMMCCCLXI', 'MMMCCCLXII', 'MMMCCCLXIII', 'MMMCCCLXIV', 'MMMCCCLXV', 'MMMCCCLXVI', 'MMMCCCLXVII', 'MMMCCCLXVIII', 'MMMCCCLXIX', 'MMMCCCLXX', 'MMMCCCLXXI', 'MMMCCCLXXII', 'MMMCCCLXXIII', 'MMMCCCLXXIV', 'MMMCCCLXXV', 'MMMCCCLXXVI', 'MMMCCCLXXVII', 'MMMCCCLXXVIII', 'MMMCCCLXXIX', 'MMMCCCLXXX', 'MMMCCCLXXXI', 'MMMCCCLXXXII', 'MMMCCCLXXXIII', 'MMMCCCLXXXIV', 'MMMCCCLXXXV', 'MMMCCCLXXXVI', 'MMMCCCLXXXVII', 'MMMCCCLXXXVIII', 'MMMCCCLXXXIX', 'MMMCCCXC', 'MMMCCCXCI', 'MMMCCCXCII', 'MMMCCCXCIII', 'MMMCCCXCIV', 'MMMCCCVC', 'MMMCCCVCI', 'MMMCCCVCII', 'MMMCCCVCIII', 'MMMCCCIC', 'MMMCD', 'MMMCDI', 'MMMCDII', 'MMMCDIII', 'MMMCDIV', 'MMMCDV', 'MMMCDVI', 'MMMCDVII', 'MMMCDVIII', 'MMMCDIX', 'MMMCDX', 'MMMCDXI', 'MMMCDXII', 'MMMCDXIII', 'MMMCDXIV', 'MMMCDXV', 'MMMCDXVI', 'MMMCDXVII', 'MMMCDXVIII', 'MMMCDXIX', 'MMMCDXX', 'MMMCDXXI', 'MMMCDXXII', 'MMMCDXXIII', 'MMMCDXXIV', 'MMMCDXXV', 'MMMCDXXVI', 'MMMCDXXVII', 'MMMCDXXVIII', 'MMMCDXXIX', 'MMMCDXXX', 'MMMCDXXXI', 'MMMCDXXXII', 'MMMCDXXXIII', 'MMMCDXXXIV', 'MMMCDXXXV', 'MMMCDXXXVI', 'MMMCDXXXVII', 'MMMCDXXXVIII', 'MMMCDXXXIX', 'MMMCDXL', 'MMMCDXLI', 'MMMCDXLII', 'MMMCDXLIII', 'MMMCDXLIV', 'MMMCDVL', 'MMMCDVLI', 'MMMCDVLII', 'MMMCDVLIII', 'MMMCDIL', 'MMMLD', 'MMMLDI', 'MMMLDII', 'MMMLDIII', 'MMMLDIV', 'MMMLDV', 'MMMLDVI', 'MMMLDVII', 'MMMLDVIII', 'MMMLDIX', 'MMMLDX', 'MMMLDXI', 'MMMLDXII', 'MMMLDXIII', 'MMMLDXIV', 'MMMLDXV', 'MMMLDXVI', 'MMMLDXVII', 'MMMLDXVIII', 'MMMLDXIX', 'MMMLDXX', 'MMMLDXXI', 'MMMLDXXII', 'MMMLDXXIII', 'MMMLDXXIV', 'MMMLDXXV', 'MMMLDXXVI', 'MMMLDXXVII', 'MMMLDXXVIII', 'MMMLDXXIX', 'MMMLDXXX', 'MMMLDXXXI', 'MMMLDXXXII', 'MMMLDXXXIII', 'MMMLDXXXIV', 'MMMLDXXXV', 'MMMLDXXXVI', 'MMMLDXXXVII', 'MMMLDXXXVIII', 'MMMLDXXXIX', 'MMMXD', 'MMMXDI', 'MMMXDII', 'MMMXDIII', 'MMMXDIV', 'MMMVD', 'MMMVDI', 'MMMVDII', 'MMMVDIII', 'MMMVDIV', 'MMMD', 'MMMDI', 'MMMDII', 'MMMDIII', 'MMMDIV', 'MMMDV', 'MMMDVI', 'MMMDVII', 'MMMDVIII', 'MMMDIX', 'MMMDX', 'MMMDXI', 'MMMDXII', 'MMMDXIII', 'MMMDXIV', 'MMMDXV', 'MMMDXVI', 'MMMDXVII', 'MMMDXVIII', 'MMMDXIX', 'MMMDXX', 'MMMDXXI', 'MMMDXXII', 'MMMDXXIII', 'MMMDXXIV', 'MMMDXXV', 'MMMDXXVI', 'MMMDXXVII', 'MMMDXXVIII', 'MMMDXXIX', 'MMMDXXX', 'MMMDXXXI', 'MMMDXXXII', 'MMMDXXXIII', 'MMMDXXXIV', 'MMMDXXXV', 'MMMDXXXVI', 'MMMDXXXVII', 'MMMDXXXVIII', 'MMMDXXXIX', 'MMMDXL', 'MMMDXLI', 'MMMDXLII', 'MMMDXLIII', 'MMMDXLIV', 'MMMDVL', 'MMMDVLI', 'MMMDVLII', 'MMMDVLIII', 'MMMDIL', 'MMMDL', 'MMMDLI', 'MMMDLII', 'MMMDLIII', 'MMMDLIV', 'MMMDLV', 'MMMDLVI', 'MMMDLVII', 'MMMDLVIII', 'MMMDLIX', 'MMMDLX', 'MMMDLXI', 'MMMDLXII', 'MMMDLXIII', 'MMMDLXIV', 'MMMDLXV', 'MMMDLXVI', 'MMMDLXVII', 'MMMDLXVIII', 'MMMDLXIX', 'MMMDLXX', 'MMMDLXXI', 'MMMDLXXII', 'MMMDLXXIII', 'MMMDLXXIV', 'MMMDLXXV', 'MMMDLXXVI', 'MMMDLXXVII', 'MMMDLXXVIII', 'MMMDLXXIX', 'MMMDLXXX', 'MMMDLXXXI', 'MMMDLXXXII', 'MMMDLXXXIII', 'MMMDLXXXIV', 'MMMDLXXXV', 'MMMDLXXXVI', 'MMMDLXXXVII', 'MMMDLXXXVIII', 'MMMDLXXXIX', 'MMMDXC', 'MMMDXCI', 'MMMDXCII', 'MMMDXCIII', 'MMMDXCIV', 'MMMDVC', 'MMMDVCI', 'MMMDVCII', 'MMMDVCIII', 'MMMDIC', 'MMMDC', 'MMMDCI', 'MMMDCII', 'MMMDCIII', 'MMMDCIV', 'MMMDCV', 'MMMDCVI', 'MMMDCVII', 'MMMDCVIII', 'MMMDCIX', 'MMMDCX', 'MMMDCXI', 'MMMDCXII', 'MMMDCXIII', 'MMMDCXIV', 'MMMDCXV', 'MMMDCXVI', 'MMMDCXVII', 'MMMDCXVIII', 'MMMDCXIX', 'MMMDCXX', 'MMMDCXXI', 'MMMDCXXII', 'MMMDCXXIII', 'MMMDCXXIV', 'MMMDCXXV', 'MMMDCXXVI', 'MMMDCXXVII', 'MMMDCXXVIII', 'MMMDCXXIX', 'MMMDCXXX', 'MMMDCXXXI', 'MMMDCXXXII', 'MMMDCXXXIII', 'MMMDCXXXIV', 'MMMDCXXXV', 'MMMDCXXXVI', 'MMMDCXXXVII', 'MMMDCXXXVIII', 'MMMDCXXXIX', 'MMMDCXL', 'MMMDCXLI', 'MMMDCXLII', 'MMMDCXLIII', 'MMMDCXLIV', 'MMMDCVL', 'MMMDCVLI', 'MMMDCVLII', 'MMMDCVLIII', 'MMMDCIL', 'MMMDCL', 'MMMDCLI', 'MMMDCLII', 'MMMDCLIII', 'MMMDCLIV', 'MMMDCLV', 'MMMDCLVI', 'MMMDCLVII', 'MMMDCLVIII', 'MMMDCLIX', 'MMMDCLX', 'MMMDCLXI', 'MMMDCLXII', 'MMMDCLXIII', 'MMMDCLXIV', 'MMMDCLXV', 'MMMDCLXVI', 'MMMDCLXVII', 'MMMDCLXVIII', 'MMMDCLXIX', 'MMMDCLXX', 'MMMDCLXXI', 'MMMDCLXXII', 'MMMDCLXXIII', 'MMMDCLXXIV', 'MMMDCLXXV', 'MMMDCLXXVI', 'MMMDCLXXVII', 'MMMDCLXXVIII', 'MMMDCLXXIX', 'MMMDCLXXX', 'MMMDCLXXXI', 'MMMDCLXXXII', 'MMMDCLXXXIII', 'MMMDCLXXXIV', 'MMMDCLXXXV', 'MMMDCLXXXVI', 'MMMDCLXXXVII', 'MMMDCLXXXVIII', 'MMMDCLXXXIX', 'MMMDCXC', 'MMMDCXCI', 'MMMDCXCII', 'MMMDCXCIII', 'MMMDCXCIV', 'MMMDCVC', 'MMMDCVCI', 'MMMDCVCII', 'MMMDCVCIII', 'MMMDCIC', 'MMMDCC', 'MMMDCCI', 'MMMDCCII', 'MMMDCCIII', 'MMMDCCIV', 'MMMDCCV', 'MMMDCCVI', 'MMMDCCVII', 'MMMDCCVIII', 'MMMDCCIX', 'MMMDCCX', 'MMMDCCXI', 'MMMDCCXII', 'MMMDCCXIII', 'MMMDCCXIV', 'MMMDCCXV', 'MMMDCCXVI', 'MMMDCCXVII', 'MMMDCCXVIII', 'MMMDCCXIX', 'MMMDCCXX', 'MMMDCCXXI', 'MMMDCCXXII', 'MMMDCCXXIII', 'MMMDCCXXIV', 'MMMDCCXXV', 'MMMDCCXXVI', 'MMMDCCXXVII', 'MMMDCCXXVIII', 'MMMDCCXXIX', 'MMMDCCXXX', 'MMMDCCXXXI', 'MMMDCCXXXII', 'MMMDCCXXXIII', 'MMMDCCXXXIV', 'MMMDCCXXXV', 'MMMDCCXXXVI', 'MMMDCCXXXVII', 'MMMDCCXXXVIII', 'MMMDCCXXXIX', 'MMMDCCXL', 'MMMDCCXLI', 'MMMDCCXLII', 'MMMDCCXLIII', 'MMMDCCXLIV', 'MMMDCCVL', 'MMMDCCVLI', 'MMMDCCVLII', 'MMMDCCVLIII', 'MMMDCCIL', 'MMMDCCL', 'MMMDCCLI', 'MMMDCCLII', 'MMMDCCLIII', 'MMMDCCLIV', 'MMMDCCLV', 'MMMDCCLVI', 'MMMDCCLVII', 'MMMDCCLVIII', 'MMMDCCLIX', 'MMMDCCLX', 'MMMDCCLXI', 'MMMDCCLXII', 'MMMDCCLXIII', 'MMMDCCLXIV', 'MMMDCCLXV', 'MMMDCCLXVI', 'MMMDCCLXVII', 'MMMDCCLXVIII', 'MMMDCCLXIX', 'MMMDCCLXX', 'MMMDCCLXXI', 'MMMDCCLXXII', 'MMMDCCLXXIII', 'MMMDCCLXXIV', 'MMMDCCLXXV', 'MMMDCCLXXVI', 'MMMDCCLXXVII', 'MMMDCCLXXVIII', 'MMMDCCLXXIX', 'MMMDCCLXXX', 'MMMDCCLXXXI', 'MMMDCCLXXXII', 'MMMDCCLXXXIII', 'MMMDCCLXXXIV', 'MMMDCCLXXXV', 'MMMDCCLXXXVI', 'MMMDCCLXXXVII', 'MMMDCCLXXXVIII', 'MMMDCCLXXXIX', 'MMMDCCXC', 'MMMDCCXCI', 'MMMDCCXCII', 'MMMDCCXCIII', 'MMMDCCXCIV', 'MMMDCCVC', 'MMMDCCVCI', 'MMMDCCVCII', 'MMMDCCVCIII', 'MMMDCCIC', 'MMMDCCC', 'MMMDCCCI', 'MMMDCCCII', 'MMMDCCCIII', 'MMMDCCCIV', 'MMMDCCCV', 'MMMDCCCVI', 'MMMDCCCVII', 'MMMDCCCVIII', 'MMMDCCCIX', 'MMMDCCCX', 'MMMDCCCXI', 'MMMDCCCXII', 'MMMDCCCXIII', 'MMMDCCCXIV', 'MMMDCCCXV', 'MMMDCCCXVI', 'MMMDCCCXVII', 'MMMDCCCXVIII', 'MMMDCCCXIX', 'MMMDCCCXX', 'MMMDCCCXXI', 'MMMDCCCXXII', 'MMMDCCCXXIII', 'MMMDCCCXXIV', 'MMMDCCCXXV', 'MMMDCCCXXVI', 'MMMDCCCXXVII', 'MMMDCCCXXVIII', 'MMMDCCCXXIX', 'MMMDCCCXXX', 'MMMDCCCXXXI', 'MMMDCCCXXXII', 'MMMDCCCXXXIII', 'MMMDCCCXXXIV', 'MMMDCCCXXXV', 'MMMDCCCXXXVI', 'MMMDCCCXXXVII', 'MMMDCCCXXXVIII', 'MMMDCCCXXXIX', 'MMMDCCCXL', 'MMMDCCCXLI', 'MMMDCCCXLII', 'MMMDCCCXLIII', 'MMMDCCCXLIV', 'MMMDCCCVL', 'MMMDCCCVLI', 'MMMDCCCVLII', 'MMMDCCCVLIII', 'MMMDCCCIL', 'MMMDCCCL', 'MMMDCCCLI', 'MMMDCCCLII', 'MMMDCCCLIII', 'MMMDCCCLIV', 'MMMDCCCLV', 'MMMDCCCLVI', 'MMMDCCCLVII', 'MMMDCCCLVIII', 'MMMDCCCLIX', 'MMMDCCCLX', 'MMMDCCCLXI', 'MMMDCCCLXII', 'MMMDCCCLXIII', 'MMMDCCCLXIV', 'MMMDCCCLXV', 'MMMDCCCLXVI', 'MMMDCCCLXVII', 'MMMDCCCLXVIII', 'MMMDCCCLXIX', 'MMMDCCCLXX', 'MMMDCCCLXXI', 'MMMDCCCLXXII', 'MMMDCCCLXXIII', 'MMMDCCCLXXIV', 'MMMDCCCLXXV', 'MMMDCCCLXXVI', 'MMMDCCCLXXVII', 'MMMDCCCLXXVIII', 'MMMDCCCLXXIX', 'MMMDCCCLXXX', 'MMMDCCCLXXXI', 'MMMDCCCLXXXII', 'MMMDCCCLXXXIII', 'MMMDCCCLXXXIV', 'MMMDCCCLXXXV', 'MMMDCCCLXXXVI', 'MMMDCCCLXXXVII', 'MMMDCCCLXXXVIII', 'MMMDCCCLXXXIX', 'MMMDCCCXC', 'MMMDCCCXCI', 'MMMDCCCXCII', 'MMMDCCCXCIII', 'MMMDCCCXCIV', 'MMMDCCCVC', 'MMMDCCCVCI', 'MMMDCCCVCII', 'MMMDCCCVCIII', 'MMMDCCCIC', 'MMMCM', 'MMMCMI', 'MMMCMII', 'MMMCMIII', 'MMMCMIV', 'MMMCMV', 'MMMCMVI', 'MMMCMVII', 'MMMCMVIII', 'MMMCMIX', 'MMMCMX', 'MMMCMXI', 'MMMCMXII', 'MMMCMXIII', 'MMMCMXIV', 'MMMCMXV', 'MMMCMXVI', 'MMMCMXVII', 'MMMCMXVIII', 'MMMCMXIX', 'MMMCMXX', 'MMMCMXXI', 'MMMCMXXII', 'MMMCMXXIII', 'MMMCMXXIV', 'MMMCMXXV', 'MMMCMXXVI', 'MMMCMXXVII', 'MMMCMXXVIII', 'MMMCMXXIX', 'MMMCMXXX', 'MMMCMXXXI', 'MMMCMXXXII', 'MMMCMXXXIII', 'MMMCMXXXIV', 'MMMCMXXXV', 'MMMCMXXXVI', 'MMMCMXXXVII', 'MMMCMXXXVIII', 'MMMCMXXXIX', 'MMMCMXL', 'MMMCMXLI', 'MMMCMXLII', 'MMMCMXLIII', 'MMMCMXLIV', 'MMMCMVL', 'MMMCMVLI', 'MMMCMVLII', 'MMMCMVLIII', 'MMMCMIL', 'MMMLM', 'MMMLMI', 'MMMLMII', 'MMMLMIII', 'MMMLMIV', 'MMMLMV', 'MMMLMVI', 'MMMLMVII', 'MMMLMVIII', 'MMMLMIX', 'MMMLMX', 'MMMLMXI', 'MMMLMXII', 'MMMLMXIII', 'MMMLMXIV', 'MMMLMXV', 'MMMLMXVI', 'MMMLMXVII', 'MMMLMXVIII', 'MMMLMXIX', 'MMMLMXX', 'MMMLMXXI', 'MMMLMXXII', 'MMMLMXXIII', 'MMMLMXXIV', 'MMMLMXXV', 'MMMLMXXVI', 'MMMLMXXVII', 'MMMLMXXVIII', 'MMMLMXXIX', 'MMMLMXXX', 'MMMLMXXXI', 'MMMLMXXXII', 'MMMLMXXXIII', 'MMMLMXXXIV', 'MMMLMXXXV', 'MMMLMXXXVI', 'MMMLMXXXVII', 'MMMLMXXXVIII', 'MMMLMXXXIX', 'MMMXM', 'MMMXMI', 'MMMXMII', 'MMMXMIII', 'MMMXMIV', 'MMMVM', 'MMMVMI', 'MMMVMII', 'MMMVMIII', 'MMMVMIV', ] -export const mode4 = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI', 'XII', 'XIII', 'XIV', 'XV', 'XVI', 'XVII', 'XVIII', 'XIX', 'XX', 'XXI', 'XXII', 'XXIII', 'XXIV', 'XXV', 'XXVI', 'XXVII', 'XXVIII', 'XXIX', 'XXX', 'XXXI', 'XXXII', 'XXXIII', 'XXXIV', 'XXXV', 'XXXVI', 'XXXVII', 'XXXVIII', 'XXXIX', 'XL', 'XLI', 'XLII', 'XLIII', 'XLIV', 'VL', 'VLI', 'VLII', 'VLIII', 'IL', 'L', 'LI', 'LII', 'LIII', 'LIV', 'LV', 'LVI', 'LVII', 'LVIII', 'LIX', 'LX', 'LXI', 'LXII', 'LXIII', 'LXIV', 'LXV', 'LXVI', 'LXVII', 'LXVIII', 'LXIX', 'LXX', 'LXXI', 'LXXII', 'LXXIII', 'LXXIV', 'LXXV', 'LXXVI', 'LXXVII', 'LXXVIII', 'LXXIX', 'LXXX', 'LXXXI', 'LXXXII', 'LXXXIII', 'LXXXIV', 'LXXXV', 'LXXXVI', 'LXXXVII', 'LXXXVIII', 'LXXXIX', 'XC', 'XCI', 'XCII', 'XCIII', 'XCIV', 'VC', 'VCI', 'VCII', 'VCIII', 'IC', 'C', 'CI', 'CII', 'CIII', 'CIV', 'CV', 'CVI', 'CVII', 'CVIII', 'CIX', 'CX', 'CXI', 'CXII', 'CXIII', 'CXIV', 'CXV', 'CXVI', 'CXVII', 'CXVIII', 'CXIX', 'CXX', 'CXXI', 'CXXII', 'CXXIII', 'CXXIV', 'CXXV', 'CXXVI', 'CXXVII', 'CXXVIII', 'CXXIX', 'CXXX', 'CXXXI', 'CXXXII', 'CXXXIII', 'CXXXIV', 'CXXXV', 'CXXXVI', 'CXXXVII', 'CXXXVIII', 'CXXXIX', 'CXL', 'CXLI', 'CXLII', 'CXLIII', 'CXLIV', 'CVL', 'CVLI', 'CVLII', 'CVLIII', 'CIL', 'CL', 'CLI', 'CLII', 'CLIII', 'CLIV', 'CLV', 'CLVI', 'CLVII', 'CLVIII', 'CLIX', 'CLX', 'CLXI', 'CLXII', 'CLXIII', 'CLXIV', 'CLXV', 'CLXVI', 'CLXVII', 'CLXVIII', 'CLXIX', 'CLXX', 'CLXXI', 'CLXXII', 'CLXXIII', 'CLXXIV', 'CLXXV', 'CLXXVI', 'CLXXVII', 'CLXXVIII', 'CLXXIX', 'CLXXX', 'CLXXXI', 'CLXXXII', 'CLXXXIII', 'CLXXXIV', 'CLXXXV', 'CLXXXVI', 'CLXXXVII', 'CLXXXVIII', 'CLXXXIX', 'CXC', 'CXCI', 'CXCII', 'CXCIII', 'CXCIV', 'CVC', 'CVCI', 'CVCII', 'CVCIII', 'CIC', 'CC', 'CCI', 'CCII', 'CCIII', 'CCIV', 'CCV', 'CCVI', 'CCVII', 'CCVIII', 'CCIX', 'CCX', 'CCXI', 'CCXII', 'CCXIII', 'CCXIV', 'CCXV', 'CCXVI', 'CCXVII', 'CCXVIII', 'CCXIX', 'CCXX', 'CCXXI', 'CCXXII', 'CCXXIII', 'CCXXIV', 'CCXXV', 'CCXXVI', 'CCXXVII', 'CCXXVIII', 'CCXXIX', 'CCXXX', 'CCXXXI', 'CCXXXII', 'CCXXXIII', 'CCXXXIV', 'CCXXXV', 'CCXXXVI', 'CCXXXVII', 'CCXXXVIII', 'CCXXXIX', 'CCXL', 'CCXLI', 'CCXLII', 'CCXLIII', 'CCXLIV', 'CCVL', 'CCVLI', 'CCVLII', 'CCVLIII', 'CCIL', 'CCL', 'CCLI', 'CCLII', 'CCLIII', 'CCLIV', 'CCLV', 'CCLVI', 'CCLVII', 'CCLVIII', 'CCLIX', 'CCLX', 'CCLXI', 'CCLXII', 'CCLXIII', 'CCLXIV', 'CCLXV', 'CCLXVI', 'CCLXVII', 'CCLXVIII', 'CCLXIX', 'CCLXX', 'CCLXXI', 'CCLXXII', 'CCLXXIII', 'CCLXXIV', 'CCLXXV', 'CCLXXVI', 'CCLXXVII', 'CCLXXVIII', 'CCLXXIX', 'CCLXXX', 'CCLXXXI', 'CCLXXXII', 'CCLXXXIII', 'CCLXXXIV', 'CCLXXXV', 'CCLXXXVI', 'CCLXXXVII', 'CCLXXXVIII', 'CCLXXXIX', 'CCXC', 'CCXCI', 'CCXCII', 'CCXCIII', 'CCXCIV', 'CCVC', 'CCVCI', 'CCVCII', 'CCVCIII', 'CCIC', 'CCC', 'CCCI', 'CCCII', 'CCCIII', 'CCCIV', 'CCCV', 'CCCVI', 'CCCVII', 'CCCVIII', 'CCCIX', 'CCCX', 'CCCXI', 'CCCXII', 'CCCXIII', 'CCCXIV', 'CCCXV', 'CCCXVI', 'CCCXVII', 'CCCXVIII', 'CCCXIX', 'CCCXX', 'CCCXXI', 'CCCXXII', 'CCCXXIII', 'CCCXXIV', 'CCCXXV', 'CCCXXVI', 'CCCXXVII', 'CCCXXVIII', 'CCCXXIX', 'CCCXXX', 'CCCXXXI', 'CCCXXXII', 'CCCXXXIII', 'CCCXXXIV', 'CCCXXXV', 'CCCXXXVI', 'CCCXXXVII', 'CCCXXXVIII', 'CCCXXXIX', 'CCCXL', 'CCCXLI', 'CCCXLII', 'CCCXLIII', 'CCCXLIV', 'CCCVL', 'CCCVLI', 'CCCVLII', 'CCCVLIII', 'CCCIL', 'CCCL', 'CCCLI', 'CCCLII', 'CCCLIII', 'CCCLIV', 'CCCLV', 'CCCLVI', 'CCCLVII', 'CCCLVIII', 'CCCLIX', 'CCCLX', 'CCCLXI', 'CCCLXII', 'CCCLXIII', 'CCCLXIV', 'CCCLXV', 'CCCLXVI', 'CCCLXVII', 'CCCLXVIII', 'CCCLXIX', 'CCCLXX', 'CCCLXXI', 'CCCLXXII', 'CCCLXXIII', 'CCCLXXIV', 'CCCLXXV', 'CCCLXXVI', 'CCCLXXVII', 'CCCLXXVIII', 'CCCLXXIX', 'CCCLXXX', 'CCCLXXXI', 'CCCLXXXII', 'CCCLXXXIII', 'CCCLXXXIV', 'CCCLXXXV', 'CCCLXXXVI', 'CCCLXXXVII', 'CCCLXXXVIII', 'CCCLXXXIX', 'CCCXC', 'CCCXCI', 'CCCXCII', 'CCCXCIII', 'CCCXCIV', 'CCCVC', 'CCCVCI', 'CCCVCII', 'CCCVCIII', 'CCCIC', 'CD', 'CDI', 'CDII', 'CDIII', 'CDIV', 'CDV', 'CDVI', 'CDVII', 'CDVIII', 'CDIX', 'CDX', 'CDXI', 'CDXII', 'CDXIII', 'CDXIV', 'CDXV', 'CDXVI', 'CDXVII', 'CDXVIII', 'CDXIX', 'CDXX', 'CDXXI', 'CDXXII', 'CDXXIII', 'CDXXIV', 'CDXXV', 'CDXXVI', 'CDXXVII', 'CDXXVIII', 'CDXXIX', 'CDXXX', 'CDXXXI', 'CDXXXII', 'CDXXXIII', 'CDXXXIV', 'CDXXXV', 'CDXXXVI', 'CDXXXVII', 'CDXXXVIII', 'CDXXXIX', 'CDXL', 'CDXLI', 'CDXLII', 'CDXLIII', 'CDXLIV', 'CDVL', 'CDVLI', 'CDVLII', 'CDVLIII', 'CDIL', 'LD', 'LDI', 'LDII', 'LDIII', 'LDIV', 'LDV', 'LDVI', 'LDVII', 'LDVIII', 'LDIX', 'LDX', 'LDXI', 'LDXII', 'LDXIII', 'LDXIV', 'LDXV', 'LDXVI', 'LDXVII', 'LDXVIII', 'LDXIX', 'LDXX', 'LDXXI', 'LDXXII', 'LDXXIII', 'LDXXIV', 'LDXXV', 'LDXXVI', 'LDXXVII', 'LDXXVIII', 'LDXXIX', 'LDXXX', 'LDXXXI', 'LDXXXII', 'LDXXXIII', 'LDXXXIV', 'LDXXXV', 'LDXXXVI', 'LDXXXVII', 'LDXXXVIII', 'LDXXXIX', 'XD', 'XDI', 'XDII', 'XDIII', 'XDIV', 'VD', 'VDI', 'VDII', 'VDIII', 'ID', 'D', 'DI', 'DII', 'DIII', 'DIV', 'DV', 'DVI', 'DVII', 'DVIII', 'DIX', 'DX', 'DXI', 'DXII', 'DXIII', 'DXIV', 'DXV', 'DXVI', 'DXVII', 'DXVIII', 'DXIX', 'DXX', 'DXXI', 'DXXII', 'DXXIII', 'DXXIV', 'DXXV', 'DXXVI', 'DXXVII', 'DXXVIII', 'DXXIX', 'DXXX', 'DXXXI', 'DXXXII', 'DXXXIII', 'DXXXIV', 'DXXXV', 'DXXXVI', 'DXXXVII', 'DXXXVIII', 'DXXXIX', 'DXL', 'DXLI', 'DXLII', 'DXLIII', 'DXLIV', 'DVL', 'DVLI', 'DVLII', 'DVLIII', 'DIL', 'DL', 'DLI', 'DLII', 'DLIII', 'DLIV', 'DLV', 'DLVI', 'DLVII', 'DLVIII', 'DLIX', 'DLX', 'DLXI', 'DLXII', 'DLXIII', 'DLXIV', 'DLXV', 'DLXVI', 'DLXVII', 'DLXVIII', 'DLXIX', 'DLXX', 'DLXXI', 'DLXXII', 'DLXXIII', 'DLXXIV', 'DLXXV', 'DLXXVI', 'DLXXVII', 'DLXXVIII', 'DLXXIX', 'DLXXX', 'DLXXXI', 'DLXXXII', 'DLXXXIII', 'DLXXXIV', 'DLXXXV', 'DLXXXVI', 'DLXXXVII', 'DLXXXVIII', 'DLXXXIX', 'DXC', 'DXCI', 'DXCII', 'DXCIII', 'DXCIV', 'DVC', 'DVCI', 'DVCII', 'DVCIII', 'DIC', 'DC', 'DCI', 'DCII', 'DCIII', 'DCIV', 'DCV', 'DCVI', 'DCVII', 'DCVIII', 'DCIX', 'DCX', 'DCXI', 'DCXII', 'DCXIII', 'DCXIV', 'DCXV', 'DCXVI', 'DCXVII', 'DCXVIII', 'DCXIX', 'DCXX', 'DCXXI', 'DCXXII', 'DCXXIII', 'DCXXIV', 'DCXXV', 'DCXXVI', 'DCXXVII', 'DCXXVIII', 'DCXXIX', 'DCXXX', 'DCXXXI', 'DCXXXII', 'DCXXXIII', 'DCXXXIV', 'DCXXXV', 'DCXXXVI', 'DCXXXVII', 'DCXXXVIII', 'DCXXXIX', 'DCXL', 'DCXLI', 'DCXLII', 'DCXLIII', 'DCXLIV', 'DCVL', 'DCVLI', 'DCVLII', 'DCVLIII', 'DCIL', 'DCL', 'DCLI', 'DCLII', 'DCLIII', 'DCLIV', 'DCLV', 'DCLVI', 'DCLVII', 'DCLVIII', 'DCLIX', 'DCLX', 'DCLXI', 'DCLXII', 'DCLXIII', 'DCLXIV', 'DCLXV', 'DCLXVI', 'DCLXVII', 'DCLXVIII', 'DCLXIX', 'DCLXX', 'DCLXXI', 'DCLXXII', 'DCLXXIII', 'DCLXXIV', 'DCLXXV', 'DCLXXVI', 'DCLXXVII', 'DCLXXVIII', 'DCLXXIX', 'DCLXXX', 'DCLXXXI', 'DCLXXXII', 'DCLXXXIII', 'DCLXXXIV', 'DCLXXXV', 'DCLXXXVI', 'DCLXXXVII', 'DCLXXXVIII', 'DCLXXXIX', 'DCXC', 'DCXCI', 'DCXCII', 'DCXCIII', 'DCXCIV', 'DCVC', 'DCVCI', 'DCVCII', 'DCVCIII', 'DCIC', 'DCC', 'DCCI', 'DCCII', 'DCCIII', 'DCCIV', 'DCCV', 'DCCVI', 'DCCVII', 'DCCVIII', 'DCCIX', 'DCCX', 'DCCXI', 'DCCXII', 'DCCXIII', 'DCCXIV', 'DCCXV', 'DCCXVI', 'DCCXVII', 'DCCXVIII', 'DCCXIX', 'DCCXX', 'DCCXXI', 'DCCXXII', 'DCCXXIII', 'DCCXXIV', 'DCCXXV', 'DCCXXVI', 'DCCXXVII', 'DCCXXVIII', 'DCCXXIX', 'DCCXXX', 'DCCXXXI', 'DCCXXXII', 'DCCXXXIII', 'DCCXXXIV', 'DCCXXXV', 'DCCXXXVI', 'DCCXXXVII', 'DCCXXXVIII', 'DCCXXXIX', 'DCCXL', 'DCCXLI', 'DCCXLII', 'DCCXLIII', 'DCCXLIV', 'DCCVL', 'DCCVLI', 'DCCVLII', 'DCCVLIII', 'DCCIL', 'DCCL', 'DCCLI', 'DCCLII', 'DCCLIII', 'DCCLIV', 'DCCLV', 'DCCLVI', 'DCCLVII', 'DCCLVIII', 'DCCLIX', 'DCCLX', 'DCCLXI', 'DCCLXII', 'DCCLXIII', 'DCCLXIV', 'DCCLXV', 'DCCLXVI', 'DCCLXVII', 'DCCLXVIII', 'DCCLXIX', 'DCCLXX', 'DCCLXXI', 'DCCLXXII', 'DCCLXXIII', 'DCCLXXIV', 'DCCLXXV', 'DCCLXXVI', 'DCCLXXVII', 'DCCLXXVIII', 'DCCLXXIX', 'DCCLXXX', 'DCCLXXXI', 'DCCLXXXII', 'DCCLXXXIII', 'DCCLXXXIV', 'DCCLXXXV', 'DCCLXXXVI', 'DCCLXXXVII', 'DCCLXXXVIII', 'DCCLXXXIX', 'DCCXC', 'DCCXCI', 'DCCXCII', 'DCCXCIII', 'DCCXCIV', 'DCCVC', 'DCCVCI', 'DCCVCII', 'DCCVCIII', 'DCCIC', 'DCCC', 'DCCCI', 'DCCCII', 'DCCCIII', 'DCCCIV', 'DCCCV', 'DCCCVI', 'DCCCVII', 'DCCCVIII', 'DCCCIX', 'DCCCX', 'DCCCXI', 'DCCCXII', 'DCCCXIII', 'DCCCXIV', 'DCCCXV', 'DCCCXVI', 'DCCCXVII', 'DCCCXVIII', 'DCCCXIX', 'DCCCXX', 'DCCCXXI', 'DCCCXXII', 'DCCCXXIII', 'DCCCXXIV', 'DCCCXXV', 'DCCCXXVI', 'DCCCXXVII', 'DCCCXXVIII', 'DCCCXXIX', 'DCCCXXX', 'DCCCXXXI', 'DCCCXXXII', 'DCCCXXXIII', 'DCCCXXXIV', 'DCCCXXXV', 'DCCCXXXVI', 'DCCCXXXVII', 'DCCCXXXVIII', 'DCCCXXXIX', 'DCCCXL', 'DCCCXLI', 'DCCCXLII', 'DCCCXLIII', 'DCCCXLIV', 'DCCCVL', 'DCCCVLI', 'DCCCVLII', 'DCCCVLIII', 'DCCCIL', 'DCCCL', 'DCCCLI', 'DCCCLII', 'DCCCLIII', 'DCCCLIV', 'DCCCLV', 'DCCCLVI', 'DCCCLVII', 'DCCCLVIII', 'DCCCLIX', 'DCCCLX', 'DCCCLXI', 'DCCCLXII', 'DCCCLXIII', 'DCCCLXIV', 'DCCCLXV', 'DCCCLXVI', 'DCCCLXVII', 'DCCCLXVIII', 'DCCCLXIX', 'DCCCLXX', 'DCCCLXXI', 'DCCCLXXII', 'DCCCLXXIII', 'DCCCLXXIV', 'DCCCLXXV', 'DCCCLXXVI', 'DCCCLXXVII', 'DCCCLXXVIII', 'DCCCLXXIX', 'DCCCLXXX', 'DCCCLXXXI', 'DCCCLXXXII', 'DCCCLXXXIII', 'DCCCLXXXIV', 'DCCCLXXXV', 'DCCCLXXXVI', 'DCCCLXXXVII', 'DCCCLXXXVIII', 'DCCCLXXXIX', 'DCCCXC', 'DCCCXCI', 'DCCCXCII', 'DCCCXCIII', 'DCCCXCIV', 'DCCCVC', 'DCCCVCI', 'DCCCVCII', 'DCCCVCIII', 'DCCCIC', 'CM', 'CMI', 'CMII', 'CMIII', 'CMIV', 'CMV', 'CMVI', 'CMVII', 'CMVIII', 'CMIX', 'CMX', 'CMXI', 'CMXII', 'CMXIII', 'CMXIV', 'CMXV', 'CMXVI', 'CMXVII', 'CMXVIII', 'CMXIX', 'CMXX', 'CMXXI', 'CMXXII', 'CMXXIII', 'CMXXIV', 'CMXXV', 'CMXXVI', 'CMXXVII', 'CMXXVIII', 'CMXXIX', 'CMXXX', 'CMXXXI', 'CMXXXII', 'CMXXXIII', 'CMXXXIV', 'CMXXXV', 'CMXXXVI', 'CMXXXVII', 'CMXXXVIII', 'CMXXXIX', 'CMXL', 'CMXLI', 'CMXLII', 'CMXLIII', 'CMXLIV', 'CMVL', 'CMVLI', 'CMVLII', 'CMVLIII', 'CMIL', 'LM', 'LMI', 'LMII', 'LMIII', 'LMIV', 'LMV', 'LMVI', 'LMVII', 'LMVIII', 'LMIX', 'LMX', 'LMXI', 'LMXII', 'LMXIII', 'LMXIV', 'LMXV', 'LMXVI', 'LMXVII', 'LMXVIII', 'LMXIX', 'LMXX', 'LMXXI', 'LMXXII', 'LMXXIII', 'LMXXIV', 'LMXXV', 'LMXXVI', 'LMXXVII', 'LMXXVIII', 'LMXXIX', 'LMXXX', 'LMXXXI', 'LMXXXII', 'LMXXXIII', 'LMXXXIV', 'LMXXXV', 'LMXXXVI', 'LMXXXVII', 'LMXXXVIII', 'LMXXXIX', 'XM', 'XMI', 'XMII', 'XMIII', 'XMIV', 'VM', 'VMI', 'VMII', 'VMIII', 'IM', 'M', 'MI', 'MII', 'MIII', 'MIV', 'MV', 'MVI', 'MVII', 'MVIII', 'MIX', 'MX', 'MXI', 'MXII', 'MXIII', 'MXIV', 'MXV', 'MXVI', 'MXVII', 'MXVIII', 'MXIX', 'MXX', 'MXXI', 'MXXII', 'MXXIII', 'MXXIV', 'MXXV', 'MXXVI', 'MXXVII', 'MXXVIII', 'MXXIX', 'MXXX', 'MXXXI', 'MXXXII', 'MXXXIII', 'MXXXIV', 'MXXXV', 'MXXXVI', 'MXXXVII', 'MXXXVIII', 'MXXXIX', 'MXL', 'MXLI', 'MXLII', 'MXLIII', 'MXLIV', 'MVL', 'MVLI', 'MVLII', 'MVLIII', 'MIL', 'ML', 'MLI', 'MLII', 'MLIII', 'MLIV', 'MLV', 'MLVI', 'MLVII', 'MLVIII', 'MLIX', 'MLX', 'MLXI', 'MLXII', 'MLXIII', 'MLXIV', 'MLXV', 'MLXVI', 'MLXVII', 'MLXVIII', 'MLXIX', 'MLXX', 'MLXXI', 'MLXXII', 'MLXXIII', 'MLXXIV', 'MLXXV', 'MLXXVI', 'MLXXVII', 'MLXXVIII', 'MLXXIX', 'MLXXX', 'MLXXXI', 'MLXXXII', 'MLXXXIII', 'MLXXXIV', 'MLXXXV', 'MLXXXVI', 'MLXXXVII', 'MLXXXVIII', 'MLXXXIX', 'MXC', 'MXCI', 'MXCII', 'MXCIII', 'MXCIV', 'MVC', 'MVCI', 'MVCII', 'MVCIII', 'MIC', 'MC', 'MCI', 'MCII', 'MCIII', 'MCIV', 'MCV', 'MCVI', 'MCVII', 'MCVIII', 'MCIX', 'MCX', 'MCXI', 'MCXII', 'MCXIII', 'MCXIV', 'MCXV', 'MCXVI', 'MCXVII', 'MCXVIII', 'MCXIX', 'MCXX', 'MCXXI', 'MCXXII', 'MCXXIII', 'MCXXIV', 'MCXXV', 'MCXXVI', 'MCXXVII', 'MCXXVIII', 'MCXXIX', 'MCXXX', 'MCXXXI', 'MCXXXII', 'MCXXXIII', 'MCXXXIV', 'MCXXXV', 'MCXXXVI', 'MCXXXVII', 'MCXXXVIII', 'MCXXXIX', 'MCXL', 'MCXLI', 'MCXLII', 'MCXLIII', 'MCXLIV', 'MCVL', 'MCVLI', 'MCVLII', 'MCVLIII', 'MCIL', 'MCL', 'MCLI', 'MCLII', 'MCLIII', 'MCLIV', 'MCLV', 'MCLVI', 'MCLVII', 'MCLVIII', 'MCLIX', 'MCLX', 'MCLXI', 'MCLXII', 'MCLXIII', 'MCLXIV', 'MCLXV', 'MCLXVI', 'MCLXVII', 'MCLXVIII', 'MCLXIX', 'MCLXX', 'MCLXXI', 'MCLXXII', 'MCLXXIII', 'MCLXXIV', 'MCLXXV', 'MCLXXVI', 'MCLXXVII', 'MCLXXVIII', 'MCLXXIX', 'MCLXXX', 'MCLXXXI', 'MCLXXXII', 'MCLXXXIII', 'MCLXXXIV', 'MCLXXXV', 'MCLXXXVI', 'MCLXXXVII', 'MCLXXXVIII', 'MCLXXXIX', 'MCXC', 'MCXCI', 'MCXCII', 'MCXCIII', 'MCXCIV', 'MCVC', 'MCVCI', 'MCVCII', 'MCVCIII', 'MCIC', 'MCC', 'MCCI', 'MCCII', 'MCCIII', 'MCCIV', 'MCCV', 'MCCVI', 'MCCVII', 'MCCVIII', 'MCCIX', 'MCCX', 'MCCXI', 'MCCXII', 'MCCXIII', 'MCCXIV', 'MCCXV', 'MCCXVI', 'MCCXVII', 'MCCXVIII', 'MCCXIX', 'MCCXX', 'MCCXXI', 'MCCXXII', 'MCCXXIII', 'MCCXXIV', 'MCCXXV', 'MCCXXVI', 'MCCXXVII', 'MCCXXVIII', 'MCCXXIX', 'MCCXXX', 'MCCXXXI', 'MCCXXXII', 'MCCXXXIII', 'MCCXXXIV', 'MCCXXXV', 'MCCXXXVI', 'MCCXXXVII', 'MCCXXXVIII', 'MCCXXXIX', 'MCCXL', 'MCCXLI', 'MCCXLII', 'MCCXLIII', 'MCCXLIV', 'MCCVL', 'MCCVLI', 'MCCVLII', 'MCCVLIII', 'MCCIL', 'MCCL', 'MCCLI', 'MCCLII', 'MCCLIII', 'MCCLIV', 'MCCLV', 'MCCLVI', 'MCCLVII', 'MCCLVIII', 'MCCLIX', 'MCCLX', 'MCCLXI', 'MCCLXII', 'MCCLXIII', 'MCCLXIV', 'MCCLXV', 'MCCLXVI', 'MCCLXVII', 'MCCLXVIII', 'MCCLXIX', 'MCCLXX', 'MCCLXXI', 'MCCLXXII', 'MCCLXXIII', 'MCCLXXIV', 'MCCLXXV', 'MCCLXXVI', 'MCCLXXVII', 'MCCLXXVIII', 'MCCLXXIX', 'MCCLXXX', 'MCCLXXXI', 'MCCLXXXII', 'MCCLXXXIII', 'MCCLXXXIV', 'MCCLXXXV', 'MCCLXXXVI', 'MCCLXXXVII', 'MCCLXXXVIII', 'MCCLXXXIX', 'MCCXC', 'MCCXCI', 'MCCXCII', 'MCCXCIII', 'MCCXCIV', 'MCCVC', 'MCCVCI', 'MCCVCII', 'MCCVCIII', 'MCCIC', 'MCCC', 'MCCCI', 'MCCCII', 'MCCCIII', 'MCCCIV', 'MCCCV', 'MCCCVI', 'MCCCVII', 'MCCCVIII', 'MCCCIX', 'MCCCX', 'MCCCXI', 'MCCCXII', 'MCCCXIII', 'MCCCXIV', 'MCCCXV', 'MCCCXVI', 'MCCCXVII', 'MCCCXVIII', 'MCCCXIX', 'MCCCXX', 'MCCCXXI', 'MCCCXXII', 'MCCCXXIII', 'MCCCXXIV', 'MCCCXXV', 'MCCCXXVI', 'MCCCXXVII', 'MCCCXXVIII', 'MCCCXXIX', 'MCCCXXX', 'MCCCXXXI', 'MCCCXXXII', 'MCCCXXXIII', 'MCCCXXXIV', 'MCCCXXXV', 'MCCCXXXVI', 'MCCCXXXVII', 'MCCCXXXVIII', 'MCCCXXXIX', 'MCCCXL', 'MCCCXLI', 'MCCCXLII', 'MCCCXLIII', 'MCCCXLIV', 'MCCCVL', 'MCCCVLI', 'MCCCVLII', 'MCCCVLIII', 'MCCCIL', 'MCCCL', 'MCCCLI', 'MCCCLII', 'MCCCLIII', 'MCCCLIV', 'MCCCLV', 'MCCCLVI', 'MCCCLVII', 'MCCCLVIII', 'MCCCLIX', 'MCCCLX', 'MCCCLXI', 'MCCCLXII', 'MCCCLXIII', 'MCCCLXIV', 'MCCCLXV', 'MCCCLXVI', 'MCCCLXVII', 'MCCCLXVIII', 'MCCCLXIX', 'MCCCLXX', 'MCCCLXXI', 'MCCCLXXII', 'MCCCLXXIII', 'MCCCLXXIV', 'MCCCLXXV', 'MCCCLXXVI', 'MCCCLXXVII', 'MCCCLXXVIII', 'MCCCLXXIX', 'MCCCLXXX', 'MCCCLXXXI', 'MCCCLXXXII', 'MCCCLXXXIII', 'MCCCLXXXIV', 'MCCCLXXXV', 'MCCCLXXXVI', 'MCCCLXXXVII', 'MCCCLXXXVIII', 'MCCCLXXXIX', 'MCCCXC', 'MCCCXCI', 'MCCCXCII', 'MCCCXCIII', 'MCCCXCIV', 'MCCCVC', 'MCCCVCI', 'MCCCVCII', 'MCCCVCIII', 'MCCCIC', 'MCD', 'MCDI', 'MCDII', 'MCDIII', 'MCDIV', 'MCDV', 'MCDVI', 'MCDVII', 'MCDVIII', 'MCDIX', 'MCDX', 'MCDXI', 'MCDXII', 'MCDXIII', 'MCDXIV', 'MCDXV', 'MCDXVI', 'MCDXVII', 'MCDXVIII', 'MCDXIX', 'MCDXX', 'MCDXXI', 'MCDXXII', 'MCDXXIII', 'MCDXXIV', 'MCDXXV', 'MCDXXVI', 'MCDXXVII', 'MCDXXVIII', 'MCDXXIX', 'MCDXXX', 'MCDXXXI', 'MCDXXXII', 'MCDXXXIII', 'MCDXXXIV', 'MCDXXXV', 'MCDXXXVI', 'MCDXXXVII', 'MCDXXXVIII', 'MCDXXXIX', 'MCDXL', 'MCDXLI', 'MCDXLII', 'MCDXLIII', 'MCDXLIV', 'MCDVL', 'MCDVLI', 'MCDVLII', 'MCDVLIII', 'MCDIL', 'MLD', 'MLDI', 'MLDII', 'MLDIII', 'MLDIV', 'MLDV', 'MLDVI', 'MLDVII', 'MLDVIII', 'MLDIX', 'MLDX', 'MLDXI', 'MLDXII', 'MLDXIII', 'MLDXIV', 'MLDXV', 'MLDXVI', 'MLDXVII', 'MLDXVIII', 'MLDXIX', 'MLDXX', 'MLDXXI', 'MLDXXII', 'MLDXXIII', 'MLDXXIV', 'MLDXXV', 'MLDXXVI', 'MLDXXVII', 'MLDXXVIII', 'MLDXXIX', 'MLDXXX', 'MLDXXXI', 'MLDXXXII', 'MLDXXXIII', 'MLDXXXIV', 'MLDXXXV', 'MLDXXXVI', 'MLDXXXVII', 'MLDXXXVIII', 'MLDXXXIX', 'MXD', 'MXDI', 'MXDII', 'MXDIII', 'MXDIV', 'MVD', 'MVDI', 'MVDII', 'MVDIII', 'MID', 'MD', 'MDI', 'MDII', 'MDIII', 'MDIV', 'MDV', 'MDVI', 'MDVII', 'MDVIII', 'MDIX', 'MDX', 'MDXI', 'MDXII', 'MDXIII', 'MDXIV', 'MDXV', 'MDXVI', 'MDXVII', 'MDXVIII', 'MDXIX', 'MDXX', 'MDXXI', 'MDXXII', 'MDXXIII', 'MDXXIV', 'MDXXV', 'MDXXVI', 'MDXXVII', 'MDXXVIII', 'MDXXIX', 'MDXXX', 'MDXXXI', 'MDXXXII', 'MDXXXIII', 'MDXXXIV', 'MDXXXV', 'MDXXXVI', 'MDXXXVII', 'MDXXXVIII', 'MDXXXIX', 'MDXL', 'MDXLI', 'MDXLII', 'MDXLIII', 'MDXLIV', 'MDVL', 'MDVLI', 'MDVLII', 'MDVLIII', 'MDIL', 'MDL', 'MDLI', 'MDLII', 'MDLIII', 'MDLIV', 'MDLV', 'MDLVI', 'MDLVII', 'MDLVIII', 'MDLIX', 'MDLX', 'MDLXI', 'MDLXII', 'MDLXIII', 'MDLXIV', 'MDLXV', 'MDLXVI', 'MDLXVII', 'MDLXVIII', 'MDLXIX', 'MDLXX', 'MDLXXI', 'MDLXXII', 'MDLXXIII', 'MDLXXIV', 'MDLXXV', 'MDLXXVI', 'MDLXXVII', 'MDLXXVIII', 'MDLXXIX', 'MDLXXX', 'MDLXXXI', 'MDLXXXII', 'MDLXXXIII', 'MDLXXXIV', 'MDLXXXV', 'MDLXXXVI', 'MDLXXXVII', 'MDLXXXVIII', 'MDLXXXIX', 'MDXC', 'MDXCI', 'MDXCII', 'MDXCIII', 'MDXCIV', 'MDVC', 'MDVCI', 'MDVCII', 'MDVCIII', 'MDIC', 'MDC', 'MDCI', 'MDCII', 'MDCIII', 'MDCIV', 'MDCV', 'MDCVI', 'MDCVII', 'MDCVIII', 'MDCIX', 'MDCX', 'MDCXI', 'MDCXII', 'MDCXIII', 'MDCXIV', 'MDCXV', 'MDCXVI', 'MDCXVII', 'MDCXVIII', 'MDCXIX', 'MDCXX', 'MDCXXI', 'MDCXXII', 'MDCXXIII', 'MDCXXIV', 'MDCXXV', 'MDCXXVI', 'MDCXXVII', 'MDCXXVIII', 'MDCXXIX', 'MDCXXX', 'MDCXXXI', 'MDCXXXII', 'MDCXXXIII', 'MDCXXXIV', 'MDCXXXV', 'MDCXXXVI', 'MDCXXXVII', 'MDCXXXVIII', 'MDCXXXIX', 'MDCXL', 'MDCXLI', 'MDCXLII', 'MDCXLIII', 'MDCXLIV', 'MDCVL', 'MDCVLI', 'MDCVLII', 'MDCVLIII', 'MDCIL', 'MDCL', 'MDCLI', 'MDCLII', 'MDCLIII', 'MDCLIV', 'MDCLV', 'MDCLVI', 'MDCLVII', 'MDCLVIII', 'MDCLIX', 'MDCLX', 'MDCLXI', 'MDCLXII', 'MDCLXIII', 'MDCLXIV', 'MDCLXV', 'MDCLXVI', 'MDCLXVII', 'MDCLXVIII', 'MDCLXIX', 'MDCLXX', 'MDCLXXI', 'MDCLXXII', 'MDCLXXIII', 'MDCLXXIV', 'MDCLXXV', 'MDCLXXVI', 'MDCLXXVII', 'MDCLXXVIII', 'MDCLXXIX', 'MDCLXXX', 'MDCLXXXI', 'MDCLXXXII', 'MDCLXXXIII', 'MDCLXXXIV', 'MDCLXXXV', 'MDCLXXXVI', 'MDCLXXXVII', 'MDCLXXXVIII', 'MDCLXXXIX', 'MDCXC', 'MDCXCI', 'MDCXCII', 'MDCXCIII', 'MDCXCIV', 'MDCVC', 'MDCVCI', 'MDCVCII', 'MDCVCIII', 'MDCIC', 'MDCC', 'MDCCI', 'MDCCII', 'MDCCIII', 'MDCCIV', 'MDCCV', 'MDCCVI', 'MDCCVII', 'MDCCVIII', 'MDCCIX', 'MDCCX', 'MDCCXI', 'MDCCXII', 'MDCCXIII', 'MDCCXIV', 'MDCCXV', 'MDCCXVI', 'MDCCXVII', 'MDCCXVIII', 'MDCCXIX', 'MDCCXX', 'MDCCXXI', 'MDCCXXII', 'MDCCXXIII', 'MDCCXXIV', 'MDCCXXV', 'MDCCXXVI', 'MDCCXXVII', 'MDCCXXVIII', 'MDCCXXIX', 'MDCCXXX', 'MDCCXXXI', 'MDCCXXXII', 'MDCCXXXIII', 'MDCCXXXIV', 'MDCCXXXV', 'MDCCXXXVI', 'MDCCXXXVII', 'MDCCXXXVIII', 'MDCCXXXIX', 'MDCCXL', 'MDCCXLI', 'MDCCXLII', 'MDCCXLIII', 'MDCCXLIV', 'MDCCVL', 'MDCCVLI', 'MDCCVLII', 'MDCCVLIII', 'MDCCIL', 'MDCCL', 'MDCCLI', 'MDCCLII', 'MDCCLIII', 'MDCCLIV', 'MDCCLV', 'MDCCLVI', 'MDCCLVII', 'MDCCLVIII', 'MDCCLIX', 'MDCCLX', 'MDCCLXI', 'MDCCLXII', 'MDCCLXIII', 'MDCCLXIV', 'MDCCLXV', 'MDCCLXVI', 'MDCCLXVII', 'MDCCLXVIII', 'MDCCLXIX', 'MDCCLXX', 'MDCCLXXI', 'MDCCLXXII', 'MDCCLXXIII', 'MDCCLXXIV', 'MDCCLXXV', 'MDCCLXXVI', 'MDCCLXXVII', 'MDCCLXXVIII', 'MDCCLXXIX', 'MDCCLXXX', 'MDCCLXXXI', 'MDCCLXXXII', 'MDCCLXXXIII', 'MDCCLXXXIV', 'MDCCLXXXV', 'MDCCLXXXVI', 'MDCCLXXXVII', 'MDCCLXXXVIII', 'MDCCLXXXIX', 'MDCCXC', 'MDCCXCI', 'MDCCXCII', 'MDCCXCIII', 'MDCCXCIV', 'MDCCVC', 'MDCCVCI', 'MDCCVCII', 'MDCCVCIII', 'MDCCIC', 'MDCCC', 'MDCCCI', 'MDCCCII', 'MDCCCIII', 'MDCCCIV', 'MDCCCV', 'MDCCCVI', 'MDCCCVII', 'MDCCCVIII', 'MDCCCIX', 'MDCCCX', 'MDCCCXI', 'MDCCCXII', 'MDCCCXIII', 'MDCCCXIV', 'MDCCCXV', 'MDCCCXVI', 'MDCCCXVII', 'MDCCCXVIII', 'MDCCCXIX', 'MDCCCXX', 'MDCCCXXI', 'MDCCCXXII', 'MDCCCXXIII', 'MDCCCXXIV', 'MDCCCXXV', 'MDCCCXXVI', 'MDCCCXXVII', 'MDCCCXXVIII', 'MDCCCXXIX', 'MDCCCXXX', 'MDCCCXXXI', 'MDCCCXXXII', 'MDCCCXXXIII', 'MDCCCXXXIV', 'MDCCCXXXV', 'MDCCCXXXVI', 'MDCCCXXXVII', 'MDCCCXXXVIII', 'MDCCCXXXIX', 'MDCCCXL', 'MDCCCXLI', 'MDCCCXLII', 'MDCCCXLIII', 'MDCCCXLIV', 'MDCCCVL', 'MDCCCVLI', 'MDCCCVLII', 'MDCCCVLIII', 'MDCCCIL', 'MDCCCL', 'MDCCCLI', 'MDCCCLII', 'MDCCCLIII', 'MDCCCLIV', 'MDCCCLV', 'MDCCCLVI', 'MDCCCLVII', 'MDCCCLVIII', 'MDCCCLIX', 'MDCCCLX', 'MDCCCLXI', 'MDCCCLXII', 'MDCCCLXIII', 'MDCCCLXIV', 'MDCCCLXV', 'MDCCCLXVI', 'MDCCCLXVII', 'MDCCCLXVIII', 'MDCCCLXIX', 'MDCCCLXX', 'MDCCCLXXI', 'MDCCCLXXII', 'MDCCCLXXIII', 'MDCCCLXXIV', 'MDCCCLXXV', 'MDCCCLXXVI', 'MDCCCLXXVII', 'MDCCCLXXVIII', 'MDCCCLXXIX', 'MDCCCLXXX', 'MDCCCLXXXI', 'MDCCCLXXXII', 'MDCCCLXXXIII', 'MDCCCLXXXIV', 'MDCCCLXXXV', 'MDCCCLXXXVI', 'MDCCCLXXXVII', 'MDCCCLXXXVIII', 'MDCCCLXXXIX', 'MDCCCXC', 'MDCCCXCI', 'MDCCCXCII', 'MDCCCXCIII', 'MDCCCXCIV', 'MDCCCVC', 'MDCCCVCI', 'MDCCCVCII', 'MDCCCVCIII', 'MDCCCIC', 'MCM', 'MCMI', 'MCMII', 'MCMIII', 'MCMIV', 'MCMV', 'MCMVI', 'MCMVII', 'MCMVIII', 'MCMIX', 'MCMX', 'MCMXI', 'MCMXII', 'MCMXIII', 'MCMXIV', 'MCMXV', 'MCMXVI', 'MCMXVII', 'MCMXVIII', 'MCMXIX', 'MCMXX', 'MCMXXI', 'MCMXXII', 'MCMXXIII', 'MCMXXIV', 'MCMXXV', 'MCMXXVI', 'MCMXXVII', 'MCMXXVIII', 'MCMXXIX', 'MCMXXX', 'MCMXXXI', 'MCMXXXII', 'MCMXXXIII', 'MCMXXXIV', 'MCMXXXV', 'MCMXXXVI', 'MCMXXXVII', 'MCMXXXVIII', 'MCMXXXIX', 'MCMXL', 'MCMXLI', 'MCMXLII', 'MCMXLIII', 'MCMXLIV', 'MCMVL', 'MCMVLI', 'MCMVLII', 'MCMVLIII', 'MCMIL', 'MLM', 'MLMI', 'MLMII', 'MLMIII', 'MLMIV', 'MLMV', 'MLMVI', 'MLMVII', 'MLMVIII', 'MLMIX', 'MLMX', 'MLMXI', 'MLMXII', 'MLMXIII', 'MLMXIV', 'MLMXV', 'MLMXVI', 'MLMXVII', 'MLMXVIII', 'MLMXIX', 'MLMXX', 'MLMXXI', 'MLMXXII', 'MLMXXIII', 'MLMXXIV', 'MLMXXV', 'MLMXXVI', 'MLMXXVII', 'MLMXXVIII', 'MLMXXIX', 'MLMXXX', 'MLMXXXI', 'MLMXXXII', 'MLMXXXIII', 'MLMXXXIV', 'MLMXXXV', 'MLMXXXVI', 'MLMXXXVII', 'MLMXXXVIII', 'MLMXXXIX', 'MXM', 'MXMI', 'MXMII', 'MXMIII', 'MXMIV', 'MVM', 'MVMI', 'MVMII', 'MVMIII', 'MIM', 'MM', 'MMI', 'MMII', 'MMIII', 'MMIV', 'MMV', 'MMVI', 'MMVII', 'MMVIII', 'MMIX', 'MMX', 'MMXI', 'MMXII', 'MMXIII', 'MMXIV', 'MMXV', 'MMXVI', 'MMXVII', 'MMXVIII', 'MMXIX', 'MMXX', 'MMXXI', 'MMXXII', 'MMXXIII', 'MMXXIV', 'MMXXV', 'MMXXVI', 'MMXXVII', 'MMXXVIII', 'MMXXIX', 'MMXXX', 'MMXXXI', 'MMXXXII', 'MMXXXIII', 'MMXXXIV', 'MMXXXV', 'MMXXXVI', 'MMXXXVII', 'MMXXXVIII', 'MMXXXIX', 'MMXL', 'MMXLI', 'MMXLII', 'MMXLIII', 'MMXLIV', 'MMVL', 'MMVLI', 'MMVLII', 'MMVLIII', 'MMIL', 'MML', 'MMLI', 'MMLII', 'MMLIII', 'MMLIV', 'MMLV', 'MMLVI', 'MMLVII', 'MMLVIII', 'MMLIX', 'MMLX', 'MMLXI', 'MMLXII', 'MMLXIII', 'MMLXIV', 'MMLXV', 'MMLXVI', 'MMLXVII', 'MMLXVIII', 'MMLXIX', 'MMLXX', 'MMLXXI', 'MMLXXII', 'MMLXXIII', 'MMLXXIV', 'MMLXXV', 'MMLXXVI', 'MMLXXVII', 'MMLXXVIII', 'MMLXXIX', 'MMLXXX', 'MMLXXXI', 'MMLXXXII', 'MMLXXXIII', 'MMLXXXIV', 'MMLXXXV', 'MMLXXXVI', 'MMLXXXVII', 'MMLXXXVIII', 'MMLXXXIX', 'MMXC', 'MMXCI', 'MMXCII', 'MMXCIII', 'MMXCIV', 'MMVC', 'MMVCI', 'MMVCII', 'MMVCIII', 'MMIC', 'MMC', 'MMCI', 'MMCII', 'MMCIII', 'MMCIV', 'MMCV', 'MMCVI', 'MMCVII', 'MMCVIII', 'MMCIX', 'MMCX', 'MMCXI', 'MMCXII', 'MMCXIII', 'MMCXIV', 'MMCXV', 'MMCXVI', 'MMCXVII', 'MMCXVIII', 'MMCXIX', 'MMCXX', 'MMCXXI', 'MMCXXII', 'MMCXXIII', 'MMCXXIV', 'MMCXXV', 'MMCXXVI', 'MMCXXVII', 'MMCXXVIII', 'MMCXXIX', 'MMCXXX', 'MMCXXXI', 'MMCXXXII', 'MMCXXXIII', 'MMCXXXIV', 'MMCXXXV', 'MMCXXXVI', 'MMCXXXVII', 'MMCXXXVIII', 'MMCXXXIX', 'MMCXL', 'MMCXLI', 'MMCXLII', 'MMCXLIII', 'MMCXLIV', 'MMCVL', 'MMCVLI', 'MMCVLII', 'MMCVLIII', 'MMCIL', 'MMCL', 'MMCLI', 'MMCLII', 'MMCLIII', 'MMCLIV', 'MMCLV', 'MMCLVI', 'MMCLVII', 'MMCLVIII', 'MMCLIX', 'MMCLX', 'MMCLXI', 'MMCLXII', 'MMCLXIII', 'MMCLXIV', 'MMCLXV', 'MMCLXVI', 'MMCLXVII', 'MMCLXVIII', 'MMCLXIX', 'MMCLXX', 'MMCLXXI', 'MMCLXXII', 'MMCLXXIII', 'MMCLXXIV', 'MMCLXXV', 'MMCLXXVI', 'MMCLXXVII', 'MMCLXXVIII', 'MMCLXXIX', 'MMCLXXX', 'MMCLXXXI', 'MMCLXXXII', 'MMCLXXXIII', 'MMCLXXXIV', 'MMCLXXXV', 'MMCLXXXVI', 'MMCLXXXVII', 'MMCLXXXVIII', 'MMCLXXXIX', 'MMCXC', 'MMCXCI', 'MMCXCII', 'MMCXCIII', 'MMCXCIV', 'MMCVC', 'MMCVCI', 'MMCVCII', 'MMCVCIII', 'MMCIC', 'MMCC', 'MMCCI', 'MMCCII', 'MMCCIII', 'MMCCIV', 'MMCCV', 'MMCCVI', 'MMCCVII', 'MMCCVIII', 'MMCCIX', 'MMCCX', 'MMCCXI', 'MMCCXII', 'MMCCXIII', 'MMCCXIV', 'MMCCXV', 'MMCCXVI', 'MMCCXVII', 'MMCCXVIII', 'MMCCXIX', 'MMCCXX', 'MMCCXXI', 'MMCCXXII', 'MMCCXXIII', 'MMCCXXIV', 'MMCCXXV', 'MMCCXXVI', 'MMCCXXVII', 'MMCCXXVIII', 'MMCCXXIX', 'MMCCXXX', 'MMCCXXXI', 'MMCCXXXII', 'MMCCXXXIII', 'MMCCXXXIV', 'MMCCXXXV', 'MMCCXXXVI', 'MMCCXXXVII', 'MMCCXXXVIII', 'MMCCXXXIX', 'MMCCXL', 'MMCCXLI', 'MMCCXLII', 'MMCCXLIII', 'MMCCXLIV', 'MMCCVL', 'MMCCVLI', 'MMCCVLII', 'MMCCVLIII', 'MMCCIL', 'MMCCL', 'MMCCLI', 'MMCCLII', 'MMCCLIII', 'MMCCLIV', 'MMCCLV', 'MMCCLVI', 'MMCCLVII', 'MMCCLVIII', 'MMCCLIX', 'MMCCLX', 'MMCCLXI', 'MMCCLXII', 'MMCCLXIII', 'MMCCLXIV', 'MMCCLXV', 'MMCCLXVI', 'MMCCLXVII', 'MMCCLXVIII', 'MMCCLXIX', 'MMCCLXX', 'MMCCLXXI', 'MMCCLXXII', 'MMCCLXXIII', 'MMCCLXXIV', 'MMCCLXXV', 'MMCCLXXVI', 'MMCCLXXVII', 'MMCCLXXVIII', 'MMCCLXXIX', 'MMCCLXXX', 'MMCCLXXXI', 'MMCCLXXXII', 'MMCCLXXXIII', 'MMCCLXXXIV', 'MMCCLXXXV', 'MMCCLXXXVI', 'MMCCLXXXVII', 'MMCCLXXXVIII', 'MMCCLXXXIX', 'MMCCXC', 'MMCCXCI', 'MMCCXCII', 'MMCCXCIII', 'MMCCXCIV', 'MMCCVC', 'MMCCVCI', 'MMCCVCII', 'MMCCVCIII', 'MMCCIC', 'MMCCC', 'MMCCCI', 'MMCCCII', 'MMCCCIII', 'MMCCCIV', 'MMCCCV', 'MMCCCVI', 'MMCCCVII', 'MMCCCVIII', 'MMCCCIX', 'MMCCCX', 'MMCCCXI', 'MMCCCXII', 'MMCCCXIII', 'MMCCCXIV', 'MMCCCXV', 'MMCCCXVI', 'MMCCCXVII', 'MMCCCXVIII', 'MMCCCXIX', 'MMCCCXX', 'MMCCCXXI', 'MMCCCXXII', 'MMCCCXXIII', 'MMCCCXXIV', 'MMCCCXXV', 'MMCCCXXVI', 'MMCCCXXVII', 'MMCCCXXVIII', 'MMCCCXXIX', 'MMCCCXXX', 'MMCCCXXXI', 'MMCCCXXXII', 'MMCCCXXXIII', 'MMCCCXXXIV', 'MMCCCXXXV', 'MMCCCXXXVI', 'MMCCCXXXVII', 'MMCCCXXXVIII', 'MMCCCXXXIX', 'MMCCCXL', 'MMCCCXLI', 'MMCCCXLII', 'MMCCCXLIII', 'MMCCCXLIV', 'MMCCCVL', 'MMCCCVLI', 'MMCCCVLII', 'MMCCCVLIII', 'MMCCCIL', 'MMCCCL', 'MMCCCLI', 'MMCCCLII', 'MMCCCLIII', 'MMCCCLIV', 'MMCCCLV', 'MMCCCLVI', 'MMCCCLVII', 'MMCCCLVIII', 'MMCCCLIX', 'MMCCCLX', 'MMCCCLXI', 'MMCCCLXII', 'MMCCCLXIII', 'MMCCCLXIV', 'MMCCCLXV', 'MMCCCLXVI', 'MMCCCLXVII', 'MMCCCLXVIII', 'MMCCCLXIX', 'MMCCCLXX', 'MMCCCLXXI', 'MMCCCLXXII', 'MMCCCLXXIII', 'MMCCCLXXIV', 'MMCCCLXXV', 'MMCCCLXXVI', 'MMCCCLXXVII', 'MMCCCLXXVIII', 'MMCCCLXXIX', 'MMCCCLXXX', 'MMCCCLXXXI', 'MMCCCLXXXII', 'MMCCCLXXXIII', 'MMCCCLXXXIV', 'MMCCCLXXXV', 'MMCCCLXXXVI', 'MMCCCLXXXVII', 'MMCCCLXXXVIII', 'MMCCCLXXXIX', 'MMCCCXC', 'MMCCCXCI', 'MMCCCXCII', 'MMCCCXCIII', 'MMCCCXCIV', 'MMCCCVC', 'MMCCCVCI', 'MMCCCVCII', 'MMCCCVCIII', 'MMCCCIC', 'MMCD', 'MMCDI', 'MMCDII', 'MMCDIII', 'MMCDIV', 'MMCDV', 'MMCDVI', 'MMCDVII', 'MMCDVIII', 'MMCDIX', 'MMCDX', 'MMCDXI', 'MMCDXII', 'MMCDXIII', 'MMCDXIV', 'MMCDXV', 'MMCDXVI', 'MMCDXVII', 'MMCDXVIII', 'MMCDXIX', 'MMCDXX', 'MMCDXXI', 'MMCDXXII', 'MMCDXXIII', 'MMCDXXIV', 'MMCDXXV', 'MMCDXXVI', 'MMCDXXVII', 'MMCDXXVIII', 'MMCDXXIX', 'MMCDXXX', 'MMCDXXXI', 'MMCDXXXII', 'MMCDXXXIII', 'MMCDXXXIV', 'MMCDXXXV', 'MMCDXXXVI', 'MMCDXXXVII', 'MMCDXXXVIII', 'MMCDXXXIX', 'MMCDXL', 'MMCDXLI', 'MMCDXLII', 'MMCDXLIII', 'MMCDXLIV', 'MMCDVL', 'MMCDVLI', 'MMCDVLII', 'MMCDVLIII', 'MMCDIL', 'MMLD', 'MMLDI', 'MMLDII', 'MMLDIII', 'MMLDIV', 'MMLDV', 'MMLDVI', 'MMLDVII', 'MMLDVIII', 'MMLDIX', 'MMLDX', 'MMLDXI', 'MMLDXII', 'MMLDXIII', 'MMLDXIV', 'MMLDXV', 'MMLDXVI', 'MMLDXVII', 'MMLDXVIII', 'MMLDXIX', 'MMLDXX', 'MMLDXXI', 'MMLDXXII', 'MMLDXXIII', 'MMLDXXIV', 'MMLDXXV', 'MMLDXXVI', 'MMLDXXVII', 'MMLDXXVIII', 'MMLDXXIX', 'MMLDXXX', 'MMLDXXXI', 'MMLDXXXII', 'MMLDXXXIII', 'MMLDXXXIV', 'MMLDXXXV', 'MMLDXXXVI', 'MMLDXXXVII', 'MMLDXXXVIII', 'MMLDXXXIX', 'MMXD', 'MMXDI', 'MMXDII', 'MMXDIII', 'MMXDIV', 'MMVD', 'MMVDI', 'MMVDII', 'MMVDIII', 'MMID', 'MMD', 'MMDI', 'MMDII', 'MMDIII', 'MMDIV', 'MMDV', 'MMDVI', 'MMDVII', 'MMDVIII', 'MMDIX', 'MMDX', 'MMDXI', 'MMDXII', 'MMDXIII', 'MMDXIV', 'MMDXV', 'MMDXVI', 'MMDXVII', 'MMDXVIII', 'MMDXIX', 'MMDXX', 'MMDXXI', 'MMDXXII', 'MMDXXIII', 'MMDXXIV', 'MMDXXV', 'MMDXXVI', 'MMDXXVII', 'MMDXXVIII', 'MMDXXIX', 'MMDXXX', 'MMDXXXI', 'MMDXXXII', 'MMDXXXIII', 'MMDXXXIV', 'MMDXXXV', 'MMDXXXVI', 'MMDXXXVII', 'MMDXXXVIII', 'MMDXXXIX', 'MMDXL', 'MMDXLI', 'MMDXLII', 'MMDXLIII', 'MMDXLIV', 'MMDVL', 'MMDVLI', 'MMDVLII', 'MMDVLIII', 'MMDIL', 'MMDL', 'MMDLI', 'MMDLII', 'MMDLIII', 'MMDLIV', 'MMDLV', 'MMDLVI', 'MMDLVII', 'MMDLVIII', 'MMDLIX', 'MMDLX', 'MMDLXI', 'MMDLXII', 'MMDLXIII', 'MMDLXIV', 'MMDLXV', 'MMDLXVI', 'MMDLXVII', 'MMDLXVIII', 'MMDLXIX', 'MMDLXX', 'MMDLXXI', 'MMDLXXII', 'MMDLXXIII', 'MMDLXXIV', 'MMDLXXV', 'MMDLXXVI', 'MMDLXXVII', 'MMDLXXVIII', 'MMDLXXIX', 'MMDLXXX', 'MMDLXXXI', 'MMDLXXXII', 'MMDLXXXIII', 'MMDLXXXIV', 'MMDLXXXV', 'MMDLXXXVI', 'MMDLXXXVII', 'MMDLXXXVIII', 'MMDLXXXIX', 'MMDXC', 'MMDXCI', 'MMDXCII', 'MMDXCIII', 'MMDXCIV', 'MMDVC', 'MMDVCI', 'MMDVCII', 'MMDVCIII', 'MMDIC', 'MMDC', 'MMDCI', 'MMDCII', 'MMDCIII', 'MMDCIV', 'MMDCV', 'MMDCVI', 'MMDCVII', 'MMDCVIII', 'MMDCIX', 'MMDCX', 'MMDCXI', 'MMDCXII', 'MMDCXIII', 'MMDCXIV', 'MMDCXV', 'MMDCXVI', 'MMDCXVII', 'MMDCXVIII', 'MMDCXIX', 'MMDCXX', 'MMDCXXI', 'MMDCXXII', 'MMDCXXIII', 'MMDCXXIV', 'MMDCXXV', 'MMDCXXVI', 'MMDCXXVII', 'MMDCXXVIII', 'MMDCXXIX', 'MMDCXXX', 'MMDCXXXI', 'MMDCXXXII', 'MMDCXXXIII', 'MMDCXXXIV', 'MMDCXXXV', 'MMDCXXXVI', 'MMDCXXXVII', 'MMDCXXXVIII', 'MMDCXXXIX', 'MMDCXL', 'MMDCXLI', 'MMDCXLII', 'MMDCXLIII', 'MMDCXLIV', 'MMDCVL', 'MMDCVLI', 'MMDCVLII', 'MMDCVLIII', 'MMDCIL', 'MMDCL', 'MMDCLI', 'MMDCLII', 'MMDCLIII', 'MMDCLIV', 'MMDCLV', 'MMDCLVI', 'MMDCLVII', 'MMDCLVIII', 'MMDCLIX', 'MMDCLX', 'MMDCLXI', 'MMDCLXII', 'MMDCLXIII', 'MMDCLXIV', 'MMDCLXV', 'MMDCLXVI', 'MMDCLXVII', 'MMDCLXVIII', 'MMDCLXIX', 'MMDCLXX', 'MMDCLXXI', 'MMDCLXXII', 'MMDCLXXIII', 'MMDCLXXIV', 'MMDCLXXV', 'MMDCLXXVI', 'MMDCLXXVII', 'MMDCLXXVIII', 'MMDCLXXIX', 'MMDCLXXX', 'MMDCLXXXI', 'MMDCLXXXII', 'MMDCLXXXIII', 'MMDCLXXXIV', 'MMDCLXXXV', 'MMDCLXXXVI', 'MMDCLXXXVII', 'MMDCLXXXVIII', 'MMDCLXXXIX', 'MMDCXC', 'MMDCXCI', 'MMDCXCII', 'MMDCXCIII', 'MMDCXCIV', 'MMDCVC', 'MMDCVCI', 'MMDCVCII', 'MMDCVCIII', 'MMDCIC', 'MMDCC', 'MMDCCI', 'MMDCCII', 'MMDCCIII', 'MMDCCIV', 'MMDCCV', 'MMDCCVI', 'MMDCCVII', 'MMDCCVIII', 'MMDCCIX', 'MMDCCX', 'MMDCCXI', 'MMDCCXII', 'MMDCCXIII', 'MMDCCXIV', 'MMDCCXV', 'MMDCCXVI', 'MMDCCXVII', 'MMDCCXVIII', 'MMDCCXIX', 'MMDCCXX', 'MMDCCXXI', 'MMDCCXXII', 'MMDCCXXIII', 'MMDCCXXIV', 'MMDCCXXV', 'MMDCCXXVI', 'MMDCCXXVII', 'MMDCCXXVIII', 'MMDCCXXIX', 'MMDCCXXX', 'MMDCCXXXI', 'MMDCCXXXII', 'MMDCCXXXIII', 'MMDCCXXXIV', 'MMDCCXXXV', 'MMDCCXXXVI', 'MMDCCXXXVII', 'MMDCCXXXVIII', 'MMDCCXXXIX', 'MMDCCXL', 'MMDCCXLI', 'MMDCCXLII', 'MMDCCXLIII', 'MMDCCXLIV', 'MMDCCVL', 'MMDCCVLI', 'MMDCCVLII', 'MMDCCVLIII', 'MMDCCIL', 'MMDCCL', 'MMDCCLI', 'MMDCCLII', 'MMDCCLIII', 'MMDCCLIV', 'MMDCCLV', 'MMDCCLVI', 'MMDCCLVII', 'MMDCCLVIII', 'MMDCCLIX', 'MMDCCLX', 'MMDCCLXI', 'MMDCCLXII', 'MMDCCLXIII', 'MMDCCLXIV', 'MMDCCLXV', 'MMDCCLXVI', 'MMDCCLXVII', 'MMDCCLXVIII', 'MMDCCLXIX', 'MMDCCLXX', 'MMDCCLXXI', 'MMDCCLXXII', 'MMDCCLXXIII', 'MMDCCLXXIV', 'MMDCCLXXV', 'MMDCCLXXVI', 'MMDCCLXXVII', 'MMDCCLXXVIII', 'MMDCCLXXIX', 'MMDCCLXXX', 'MMDCCLXXXI', 'MMDCCLXXXII', 'MMDCCLXXXIII', 'MMDCCLXXXIV', 'MMDCCLXXXV', 'MMDCCLXXXVI', 'MMDCCLXXXVII', 'MMDCCLXXXVIII', 'MMDCCLXXXIX', 'MMDCCXC', 'MMDCCXCI', 'MMDCCXCII', 'MMDCCXCIII', 'MMDCCXCIV', 'MMDCCVC', 'MMDCCVCI', 'MMDCCVCII', 'MMDCCVCIII', 'MMDCCIC', 'MMDCCC', 'MMDCCCI', 'MMDCCCII', 'MMDCCCIII', 'MMDCCCIV', 'MMDCCCV', 'MMDCCCVI', 'MMDCCCVII', 'MMDCCCVIII', 'MMDCCCIX', 'MMDCCCX', 'MMDCCCXI', 'MMDCCCXII', 'MMDCCCXIII', 'MMDCCCXIV', 'MMDCCCXV', 'MMDCCCXVI', 'MMDCCCXVII', 'MMDCCCXVIII', 'MMDCCCXIX', 'MMDCCCXX', 'MMDCCCXXI', 'MMDCCCXXII', 'MMDCCCXXIII', 'MMDCCCXXIV', 'MMDCCCXXV', 'MMDCCCXXVI', 'MMDCCCXXVII', 'MMDCCCXXVIII', 'MMDCCCXXIX', 'MMDCCCXXX', 'MMDCCCXXXI', 'MMDCCCXXXII', 'MMDCCCXXXIII', 'MMDCCCXXXIV', 'MMDCCCXXXV', 'MMDCCCXXXVI', 'MMDCCCXXXVII', 'MMDCCCXXXVIII', 'MMDCCCXXXIX', 'MMDCCCXL', 'MMDCCCXLI', 'MMDCCCXLII', 'MMDCCCXLIII', 'MMDCCCXLIV', 'MMDCCCVL', 'MMDCCCVLI', 'MMDCCCVLII', 'MMDCCCVLIII', 'MMDCCCIL', 'MMDCCCL', 'MMDCCCLI', 'MMDCCCLII', 'MMDCCCLIII', 'MMDCCCLIV', 'MMDCCCLV', 'MMDCCCLVI', 'MMDCCCLVII', 'MMDCCCLVIII', 'MMDCCCLIX', 'MMDCCCLX', 'MMDCCCLXI', 'MMDCCCLXII', 'MMDCCCLXIII', 'MMDCCCLXIV', 'MMDCCCLXV', 'MMDCCCLXVI', 'MMDCCCLXVII', 'MMDCCCLXVIII', 'MMDCCCLXIX', 'MMDCCCLXX', 'MMDCCCLXXI', 'MMDCCCLXXII', 'MMDCCCLXXIII', 'MMDCCCLXXIV', 'MMDCCCLXXV', 'MMDCCCLXXVI', 'MMDCCCLXXVII', 'MMDCCCLXXVIII', 'MMDCCCLXXIX', 'MMDCCCLXXX', 'MMDCCCLXXXI', 'MMDCCCLXXXII', 'MMDCCCLXXXIII', 'MMDCCCLXXXIV', 'MMDCCCLXXXV', 'MMDCCCLXXXVI', 'MMDCCCLXXXVII', 'MMDCCCLXXXVIII', 'MMDCCCLXXXIX', 'MMDCCCXC', 'MMDCCCXCI', 'MMDCCCXCII', 'MMDCCCXCIII', 'MMDCCCXCIV', 'MMDCCCVC', 'MMDCCCVCI', 'MMDCCCVCII', 'MMDCCCVCIII', 'MMDCCCIC', 'MMCM', 'MMCMI', 'MMCMII', 'MMCMIII', 'MMCMIV', 'MMCMV', 'MMCMVI', 'MMCMVII', 'MMCMVIII', 'MMCMIX', 'MMCMX', 'MMCMXI', 'MMCMXII', 'MMCMXIII', 'MMCMXIV', 'MMCMXV', 'MMCMXVI', 'MMCMXVII', 'MMCMXVIII', 'MMCMXIX', 'MMCMXX', 'MMCMXXI', 'MMCMXXII', 'MMCMXXIII', 'MMCMXXIV', 'MMCMXXV', 'MMCMXXVI', 'MMCMXXVII', 'MMCMXXVIII', 'MMCMXXIX', 'MMCMXXX', 'MMCMXXXI', 'MMCMXXXII', 'MMCMXXXIII', 'MMCMXXXIV', 'MMCMXXXV', 'MMCMXXXVI', 'MMCMXXXVII', 'MMCMXXXVIII', 'MMCMXXXIX', 'MMCMXL', 'MMCMXLI', 'MMCMXLII', 'MMCMXLIII', 'MMCMXLIV', 'MMCMVL', 'MMCMVLI', 'MMCMVLII', 'MMCMVLIII', 'MMCMIL', 'MMLM', 'MMLMI', 'MMLMII', 'MMLMIII', 'MMLMIV', 'MMLMV', 'MMLMVI', 'MMLMVII', 'MMLMVIII', 'MMLMIX', 'MMLMX', 'MMLMXI', 'MMLMXII', 'MMLMXIII', 'MMLMXIV', 'MMLMXV', 'MMLMXVI', 'MMLMXVII', 'MMLMXVIII', 'MMLMXIX', 'MMLMXX', 'MMLMXXI', 'MMLMXXII', 'MMLMXXIII', 'MMLMXXIV', 'MMLMXXV', 'MMLMXXVI', 'MMLMXXVII', 'MMLMXXVIII', 'MMLMXXIX', 'MMLMXXX', 'MMLMXXXI', 'MMLMXXXII', 'MMLMXXXIII', 'MMLMXXXIV', 'MMLMXXXV', 'MMLMXXXVI', 'MMLMXXXVII', 'MMLMXXXVIII', 'MMLMXXXIX', 'MMXM', 'MMXMI', 'MMXMII', 'MMXMIII', 'MMXMIV', 'MMVM', 'MMVMI', 'MMVMII', 'MMVMIII', 'MMIM', 'MMM', 'MMMI', 'MMMII', 'MMMIII', 'MMMIV', 'MMMV', 'MMMVI', 'MMMVII', 'MMMVIII', 'MMMIX', 'MMMX', 'MMMXI', 'MMMXII', 'MMMXIII', 'MMMXIV', 'MMMXV', 'MMMXVI', 'MMMXVII', 'MMMXVIII', 'MMMXIX', 'MMMXX', 'MMMXXI', 'MMMXXII', 'MMMXXIII', 'MMMXXIV', 'MMMXXV', 'MMMXXVI', 'MMMXXVII', 'MMMXXVIII', 'MMMXXIX', 'MMMXXX', 'MMMXXXI', 'MMMXXXII', 'MMMXXXIII', 'MMMXXXIV', 'MMMXXXV', 'MMMXXXVI', 'MMMXXXVII', 'MMMXXXVIII', 'MMMXXXIX', 'MMMXL', 'MMMXLI', 'MMMXLII', 'MMMXLIII', 'MMMXLIV', 'MMMVL', 'MMMVLI', 'MMMVLII', 'MMMVLIII', 'MMMIL', 'MMML', 'MMMLI', 'MMMLII', 'MMMLIII', 'MMMLIV', 'MMMLV', 'MMMLVI', 'MMMLVII', 'MMMLVIII', 'MMMLIX', 'MMMLX', 'MMMLXI', 'MMMLXII', 'MMMLXIII', 'MMMLXIV', 'MMMLXV', 'MMMLXVI', 'MMMLXVII', 'MMMLXVIII', 'MMMLXIX', 'MMMLXX', 'MMMLXXI', 'MMMLXXII', 'MMMLXXIII', 'MMMLXXIV', 'MMMLXXV', 'MMMLXXVI', 'MMMLXXVII', 'MMMLXXVIII', 'MMMLXXIX', 'MMMLXXX', 'MMMLXXXI', 'MMMLXXXII', 'MMMLXXXIII', 'MMMLXXXIV', 'MMMLXXXV', 'MMMLXXXVI', 'MMMLXXXVII', 'MMMLXXXVIII', 'MMMLXXXIX', 'MMMXC', 'MMMXCI', 'MMMXCII', 'MMMXCIII', 'MMMXCIV', 'MMMVC', 'MMMVCI', 'MMMVCII', 'MMMVCIII', 'MMMIC', 'MMMC', 'MMMCI', 'MMMCII', 'MMMCIII', 'MMMCIV', 'MMMCV', 'MMMCVI', 'MMMCVII', 'MMMCVIII', 'MMMCIX', 'MMMCX', 'MMMCXI', 'MMMCXII', 'MMMCXIII', 'MMMCXIV', 'MMMCXV', 'MMMCXVI', 'MMMCXVII', 'MMMCXVIII', 'MMMCXIX', 'MMMCXX', 'MMMCXXI', 'MMMCXXII', 'MMMCXXIII', 'MMMCXXIV', 'MMMCXXV', 'MMMCXXVI', 'MMMCXXVII', 'MMMCXXVIII', 'MMMCXXIX', 'MMMCXXX', 'MMMCXXXI', 'MMMCXXXII', 'MMMCXXXIII', 'MMMCXXXIV', 'MMMCXXXV', 'MMMCXXXVI', 'MMMCXXXVII', 'MMMCXXXVIII', 'MMMCXXXIX', 'MMMCXL', 'MMMCXLI', 'MMMCXLII', 'MMMCXLIII', 'MMMCXLIV', 'MMMCVL', 'MMMCVLI', 'MMMCVLII', 'MMMCVLIII', 'MMMCIL', 'MMMCL', 'MMMCLI', 'MMMCLII', 'MMMCLIII', 'MMMCLIV', 'MMMCLV', 'MMMCLVI', 'MMMCLVII', 'MMMCLVIII', 'MMMCLIX', 'MMMCLX', 'MMMCLXI', 'MMMCLXII', 'MMMCLXIII', 'MMMCLXIV', 'MMMCLXV', 'MMMCLXVI', 'MMMCLXVII', 'MMMCLXVIII', 'MMMCLXIX', 'MMMCLXX', 'MMMCLXXI', 'MMMCLXXII', 'MMMCLXXIII', 'MMMCLXXIV', 'MMMCLXXV', 'MMMCLXXVI', 'MMMCLXXVII', 'MMMCLXXVIII', 'MMMCLXXIX', 'MMMCLXXX', 'MMMCLXXXI', 'MMMCLXXXII', 'MMMCLXXXIII', 'MMMCLXXXIV', 'MMMCLXXXV', 'MMMCLXXXVI', 'MMMCLXXXVII', 'MMMCLXXXVIII', 'MMMCLXXXIX', 'MMMCXC', 'MMMCXCI', 'MMMCXCII', 'MMMCXCIII', 'MMMCXCIV', 'MMMCVC', 'MMMCVCI', 'MMMCVCII', 'MMMCVCIII', 'MMMCIC', 'MMMCC', 'MMMCCI', 'MMMCCII', 'MMMCCIII', 'MMMCCIV', 'MMMCCV', 'MMMCCVI', 'MMMCCVII', 'MMMCCVIII', 'MMMCCIX', 'MMMCCX', 'MMMCCXI', 'MMMCCXII', 'MMMCCXIII', 'MMMCCXIV', 'MMMCCXV', 'MMMCCXVI', 'MMMCCXVII', 'MMMCCXVIII', 'MMMCCXIX', 'MMMCCXX', 'MMMCCXXI', 'MMMCCXXII', 'MMMCCXXIII', 'MMMCCXXIV', 'MMMCCXXV', 'MMMCCXXVI', 'MMMCCXXVII', 'MMMCCXXVIII', 'MMMCCXXIX', 'MMMCCXXX', 'MMMCCXXXI', 'MMMCCXXXII', 'MMMCCXXXIII', 'MMMCCXXXIV', 'MMMCCXXXV', 'MMMCCXXXVI', 'MMMCCXXXVII', 'MMMCCXXXVIII', 'MMMCCXXXIX', 'MMMCCXL', 'MMMCCXLI', 'MMMCCXLII', 'MMMCCXLIII', 'MMMCCXLIV', 'MMMCCVL', 'MMMCCVLI', 'MMMCCVLII', 'MMMCCVLIII', 'MMMCCIL', 'MMMCCL', 'MMMCCLI', 'MMMCCLII', 'MMMCCLIII', 'MMMCCLIV', 'MMMCCLV', 'MMMCCLVI', 'MMMCCLVII', 'MMMCCLVIII', 'MMMCCLIX', 'MMMCCLX', 'MMMCCLXI', 'MMMCCLXII', 'MMMCCLXIII', 'MMMCCLXIV', 'MMMCCLXV', 'MMMCCLXVI', 'MMMCCLXVII', 'MMMCCLXVIII', 'MMMCCLXIX', 'MMMCCLXX', 'MMMCCLXXI', 'MMMCCLXXII', 'MMMCCLXXIII', 'MMMCCLXXIV', 'MMMCCLXXV', 'MMMCCLXXVI', 'MMMCCLXXVII', 'MMMCCLXXVIII', 'MMMCCLXXIX', 'MMMCCLXXX', 'MMMCCLXXXI', 'MMMCCLXXXII', 'MMMCCLXXXIII', 'MMMCCLXXXIV', 'MMMCCLXXXV', 'MMMCCLXXXVI', 'MMMCCLXXXVII', 'MMMCCLXXXVIII', 'MMMCCLXXXIX', 'MMMCCXC', 'MMMCCXCI', 'MMMCCXCII', 'MMMCCXCIII', 'MMMCCXCIV', 'MMMCCVC', 'MMMCCVCI', 'MMMCCVCII', 'MMMCCVCIII', 'MMMCCIC', 'MMMCCC', 'MMMCCCI', 'MMMCCCII', 'MMMCCCIII', 'MMMCCCIV', 'MMMCCCV', 'MMMCCCVI', 'MMMCCCVII', 'MMMCCCVIII', 'MMMCCCIX', 'MMMCCCX', 'MMMCCCXI', 'MMMCCCXII', 'MMMCCCXIII', 'MMMCCCXIV', 'MMMCCCXV', 'MMMCCCXVI', 'MMMCCCXVII', 'MMMCCCXVIII', 'MMMCCCXIX', 'MMMCCCXX', 'MMMCCCXXI', 'MMMCCCXXII', 'MMMCCCXXIII', 'MMMCCCXXIV', 'MMMCCCXXV', 'MMMCCCXXVI', 'MMMCCCXXVII', 'MMMCCCXXVIII', 'MMMCCCXXIX', 'MMMCCCXXX', 'MMMCCCXXXI', 'MMMCCCXXXII', 'MMMCCCXXXIII', 'MMMCCCXXXIV', 'MMMCCCXXXV', 'MMMCCCXXXVI', 'MMMCCCXXXVII', 'MMMCCCXXXVIII', 'MMMCCCXXXIX', 'MMMCCCXL', 'MMMCCCXLI', 'MMMCCCXLII', 'MMMCCCXLIII', 'MMMCCCXLIV', 'MMMCCCVL', 'MMMCCCVLI', 'MMMCCCVLII', 'MMMCCCVLIII', 'MMMCCCIL', 'MMMCCCL', 'MMMCCCLI', 'MMMCCCLII', 'MMMCCCLIII', 'MMMCCCLIV', 'MMMCCCLV', 'MMMCCCLVI', 'MMMCCCLVII', 'MMMCCCLVIII', 'MMMCCCLIX', 'MMMCCCLX', 'MMMCCCLXI', 'MMMCCCLXII', 'MMMCCCLXIII', 'MMMCCCLXIV', 'MMMCCCLXV', 'MMMCCCLXVI', 'MMMCCCLXVII', 'MMMCCCLXVIII', 'MMMCCCLXIX', 'MMMCCCLXX', 'MMMCCCLXXI', 'MMMCCCLXXII', 'MMMCCCLXXIII', 'MMMCCCLXXIV', 'MMMCCCLXXV', 'MMMCCCLXXVI', 'MMMCCCLXXVII', 'MMMCCCLXXVIII', 'MMMCCCLXXIX', 'MMMCCCLXXX', 'MMMCCCLXXXI', 'MMMCCCLXXXII', 'MMMCCCLXXXIII', 'MMMCCCLXXXIV', 'MMMCCCLXXXV', 'MMMCCCLXXXVI', 'MMMCCCLXXXVII', 'MMMCCCLXXXVIII', 'MMMCCCLXXXIX', 'MMMCCCXC', 'MMMCCCXCI', 'MMMCCCXCII', 'MMMCCCXCIII', 'MMMCCCXCIV', 'MMMCCCVC', 'MMMCCCVCI', 'MMMCCCVCII', 'MMMCCCVCIII', 'MMMCCCIC', 'MMMCD', 'MMMCDI', 'MMMCDII', 'MMMCDIII', 'MMMCDIV', 'MMMCDV', 'MMMCDVI', 'MMMCDVII', 'MMMCDVIII', 'MMMCDIX', 'MMMCDX', 'MMMCDXI', 'MMMCDXII', 'MMMCDXIII', 'MMMCDXIV', 'MMMCDXV', 'MMMCDXVI', 'MMMCDXVII', 'MMMCDXVIII', 'MMMCDXIX', 'MMMCDXX', 'MMMCDXXI', 'MMMCDXXII', 'MMMCDXXIII', 'MMMCDXXIV', 'MMMCDXXV', 'MMMCDXXVI', 'MMMCDXXVII', 'MMMCDXXVIII', 'MMMCDXXIX', 'MMMCDXXX', 'MMMCDXXXI', 'MMMCDXXXII', 'MMMCDXXXIII', 'MMMCDXXXIV', 'MMMCDXXXV', 'MMMCDXXXVI', 'MMMCDXXXVII', 'MMMCDXXXVIII', 'MMMCDXXXIX', 'MMMCDXL', 'MMMCDXLI', 'MMMCDXLII', 'MMMCDXLIII', 'MMMCDXLIV', 'MMMCDVL', 'MMMCDVLI', 'MMMCDVLII', 'MMMCDVLIII', 'MMMCDIL', 'MMMLD', 'MMMLDI', 'MMMLDII', 'MMMLDIII', 'MMMLDIV', 'MMMLDV', 'MMMLDVI', 'MMMLDVII', 'MMMLDVIII', 'MMMLDIX', 'MMMLDX', 'MMMLDXI', 'MMMLDXII', 'MMMLDXIII', 'MMMLDXIV', 'MMMLDXV', 'MMMLDXVI', 'MMMLDXVII', 'MMMLDXVIII', 'MMMLDXIX', 'MMMLDXX', 'MMMLDXXI', 'MMMLDXXII', 'MMMLDXXIII', 'MMMLDXXIV', 'MMMLDXXV', 'MMMLDXXVI', 'MMMLDXXVII', 'MMMLDXXVIII', 'MMMLDXXIX', 'MMMLDXXX', 'MMMLDXXXI', 'MMMLDXXXII', 'MMMLDXXXIII', 'MMMLDXXXIV', 'MMMLDXXXV', 'MMMLDXXXVI', 'MMMLDXXXVII', 'MMMLDXXXVIII', 'MMMLDXXXIX', 'MMMXD', 'MMMXDI', 'MMMXDII', 'MMMXDIII', 'MMMXDIV', 'MMMVD', 'MMMVDI', 'MMMVDII', 'MMMVDIII', 'MMMID', 'MMMD', 'MMMDI', 'MMMDII', 'MMMDIII', 'MMMDIV', 'MMMDV', 'MMMDVI', 'MMMDVII', 'MMMDVIII', 'MMMDIX', 'MMMDX', 'MMMDXI', 'MMMDXII', 'MMMDXIII', 'MMMDXIV', 'MMMDXV', 'MMMDXVI', 'MMMDXVII', 'MMMDXVIII', 'MMMDXIX', 'MMMDXX', 'MMMDXXI', 'MMMDXXII', 'MMMDXXIII', 'MMMDXXIV', 'MMMDXXV', 'MMMDXXVI', 'MMMDXXVII', 'MMMDXXVIII', 'MMMDXXIX', 'MMMDXXX', 'MMMDXXXI', 'MMMDXXXII', 'MMMDXXXIII', 'MMMDXXXIV', 'MMMDXXXV', 'MMMDXXXVI', 'MMMDXXXVII', 'MMMDXXXVIII', 'MMMDXXXIX', 'MMMDXL', 'MMMDXLI', 'MMMDXLII', 'MMMDXLIII', 'MMMDXLIV', 'MMMDVL', 'MMMDVLI', 'MMMDVLII', 'MMMDVLIII', 'MMMDIL', 'MMMDL', 'MMMDLI', 'MMMDLII', 'MMMDLIII', 'MMMDLIV', 'MMMDLV', 'MMMDLVI', 'MMMDLVII', 'MMMDLVIII', 'MMMDLIX', 'MMMDLX', 'MMMDLXI', 'MMMDLXII', 'MMMDLXIII', 'MMMDLXIV', 'MMMDLXV', 'MMMDLXVI', 'MMMDLXVII', 'MMMDLXVIII', 'MMMDLXIX', 'MMMDLXX', 'MMMDLXXI', 'MMMDLXXII', 'MMMDLXXIII', 'MMMDLXXIV', 'MMMDLXXV', 'MMMDLXXVI', 'MMMDLXXVII', 'MMMDLXXVIII', 'MMMDLXXIX', 'MMMDLXXX', 'MMMDLXXXI', 'MMMDLXXXII', 'MMMDLXXXIII', 'MMMDLXXXIV', 'MMMDLXXXV', 'MMMDLXXXVI', 'MMMDLXXXVII', 'MMMDLXXXVIII', 'MMMDLXXXIX', 'MMMDXC', 'MMMDXCI', 'MMMDXCII', 'MMMDXCIII', 'MMMDXCIV', 'MMMDVC', 'MMMDVCI', 'MMMDVCII', 'MMMDVCIII', 'MMMDIC', 'MMMDC', 'MMMDCI', 'MMMDCII', 'MMMDCIII', 'MMMDCIV', 'MMMDCV', 'MMMDCVI', 'MMMDCVII', 'MMMDCVIII', 'MMMDCIX', 'MMMDCX', 'MMMDCXI', 'MMMDCXII', 'MMMDCXIII', 'MMMDCXIV', 'MMMDCXV', 'MMMDCXVI', 'MMMDCXVII', 'MMMDCXVIII', 'MMMDCXIX', 'MMMDCXX', 'MMMDCXXI', 'MMMDCXXII', 'MMMDCXXIII', 'MMMDCXXIV', 'MMMDCXXV', 'MMMDCXXVI', 'MMMDCXXVII', 'MMMDCXXVIII', 'MMMDCXXIX', 'MMMDCXXX', 'MMMDCXXXI', 'MMMDCXXXII', 'MMMDCXXXIII', 'MMMDCXXXIV', 'MMMDCXXXV', 'MMMDCXXXVI', 'MMMDCXXXVII', 'MMMDCXXXVIII', 'MMMDCXXXIX', 'MMMDCXL', 'MMMDCXLI', 'MMMDCXLII', 'MMMDCXLIII', 'MMMDCXLIV', 'MMMDCVL', 'MMMDCVLI', 'MMMDCVLII', 'MMMDCVLIII', 'MMMDCIL', 'MMMDCL', 'MMMDCLI', 'MMMDCLII', 'MMMDCLIII', 'MMMDCLIV', 'MMMDCLV', 'MMMDCLVI', 'MMMDCLVII', 'MMMDCLVIII', 'MMMDCLIX', 'MMMDCLX', 'MMMDCLXI', 'MMMDCLXII', 'MMMDCLXIII', 'MMMDCLXIV', 'MMMDCLXV', 'MMMDCLXVI', 'MMMDCLXVII', 'MMMDCLXVIII', 'MMMDCLXIX', 'MMMDCLXX', 'MMMDCLXXI', 'MMMDCLXXII', 'MMMDCLXXIII', 'MMMDCLXXIV', 'MMMDCLXXV', 'MMMDCLXXVI', 'MMMDCLXXVII', 'MMMDCLXXVIII', 'MMMDCLXXIX', 'MMMDCLXXX', 'MMMDCLXXXI', 'MMMDCLXXXII', 'MMMDCLXXXIII', 'MMMDCLXXXIV', 'MMMDCLXXXV', 'MMMDCLXXXVI', 'MMMDCLXXXVII', 'MMMDCLXXXVIII', 'MMMDCLXXXIX', 'MMMDCXC', 'MMMDCXCI', 'MMMDCXCII', 'MMMDCXCIII', 'MMMDCXCIV', 'MMMDCVC', 'MMMDCVCI', 'MMMDCVCII', 'MMMDCVCIII', 'MMMDCIC', 'MMMDCC', 'MMMDCCI', 'MMMDCCII', 'MMMDCCIII', 'MMMDCCIV', 'MMMDCCV', 'MMMDCCVI', 'MMMDCCVII', 'MMMDCCVIII', 'MMMDCCIX', 'MMMDCCX', 'MMMDCCXI', 'MMMDCCXII', 'MMMDCCXIII', 'MMMDCCXIV', 'MMMDCCXV', 'MMMDCCXVI', 'MMMDCCXVII', 'MMMDCCXVIII', 'MMMDCCXIX', 'MMMDCCXX', 'MMMDCCXXI', 'MMMDCCXXII', 'MMMDCCXXIII', 'MMMDCCXXIV', 'MMMDCCXXV', 'MMMDCCXXVI', 'MMMDCCXXVII', 'MMMDCCXXVIII', 'MMMDCCXXIX', 'MMMDCCXXX', 'MMMDCCXXXI', 'MMMDCCXXXII', 'MMMDCCXXXIII', 'MMMDCCXXXIV', 'MMMDCCXXXV', 'MMMDCCXXXVI', 'MMMDCCXXXVII', 'MMMDCCXXXVIII', 'MMMDCCXXXIX', 'MMMDCCXL', 'MMMDCCXLI', 'MMMDCCXLII', 'MMMDCCXLIII', 'MMMDCCXLIV', 'MMMDCCVL', 'MMMDCCVLI', 'MMMDCCVLII', 'MMMDCCVLIII', 'MMMDCCIL', 'MMMDCCL', 'MMMDCCLI', 'MMMDCCLII', 'MMMDCCLIII', 'MMMDCCLIV', 'MMMDCCLV', 'MMMDCCLVI', 'MMMDCCLVII', 'MMMDCCLVIII', 'MMMDCCLIX', 'MMMDCCLX', 'MMMDCCLXI', 'MMMDCCLXII', 'MMMDCCLXIII', 'MMMDCCLXIV', 'MMMDCCLXV', 'MMMDCCLXVI', 'MMMDCCLXVII', 'MMMDCCLXVIII', 'MMMDCCLXIX', 'MMMDCCLXX', 'MMMDCCLXXI', 'MMMDCCLXXII', 'MMMDCCLXXIII', 'MMMDCCLXXIV', 'MMMDCCLXXV', 'MMMDCCLXXVI', 'MMMDCCLXXVII', 'MMMDCCLXXVIII', 'MMMDCCLXXIX', 'MMMDCCLXXX', 'MMMDCCLXXXI', 'MMMDCCLXXXII', 'MMMDCCLXXXIII', 'MMMDCCLXXXIV', 'MMMDCCLXXXV', 'MMMDCCLXXXVI', 'MMMDCCLXXXVII', 'MMMDCCLXXXVIII', 'MMMDCCLXXXIX', 'MMMDCCXC', 'MMMDCCXCI', 'MMMDCCXCII', 'MMMDCCXCIII', 'MMMDCCXCIV', 'MMMDCCVC', 'MMMDCCVCI', 'MMMDCCVCII', 'MMMDCCVCIII', 'MMMDCCIC', 'MMMDCCC', 'MMMDCCCI', 'MMMDCCCII', 'MMMDCCCIII', 'MMMDCCCIV', 'MMMDCCCV', 'MMMDCCCVI', 'MMMDCCCVII', 'MMMDCCCVIII', 'MMMDCCCIX', 'MMMDCCCX', 'MMMDCCCXI', 'MMMDCCCXII', 'MMMDCCCXIII', 'MMMDCCCXIV', 'MMMDCCCXV', 'MMMDCCCXVI', 'MMMDCCCXVII', 'MMMDCCCXVIII', 'MMMDCCCXIX', 'MMMDCCCXX', 'MMMDCCCXXI', 'MMMDCCCXXII', 'MMMDCCCXXIII', 'MMMDCCCXXIV', 'MMMDCCCXXV', 'MMMDCCCXXVI', 'MMMDCCCXXVII', 'MMMDCCCXXVIII', 'MMMDCCCXXIX', 'MMMDCCCXXX', 'MMMDCCCXXXI', 'MMMDCCCXXXII', 'MMMDCCCXXXIII', 'MMMDCCCXXXIV', 'MMMDCCCXXXV', 'MMMDCCCXXXVI', 'MMMDCCCXXXVII', 'MMMDCCCXXXVIII', 'MMMDCCCXXXIX', 'MMMDCCCXL', 'MMMDCCCXLI', 'MMMDCCCXLII', 'MMMDCCCXLIII', 'MMMDCCCXLIV', 'MMMDCCCVL', 'MMMDCCCVLI', 'MMMDCCCVLII', 'MMMDCCCVLIII', 'MMMDCCCIL', 'MMMDCCCL', 'MMMDCCCLI', 'MMMDCCCLII', 'MMMDCCCLIII', 'MMMDCCCLIV', 'MMMDCCCLV', 'MMMDCCCLVI', 'MMMDCCCLVII', 'MMMDCCCLVIII', 'MMMDCCCLIX', 'MMMDCCCLX', 'MMMDCCCLXI', 'MMMDCCCLXII', 'MMMDCCCLXIII', 'MMMDCCCLXIV', 'MMMDCCCLXV', 'MMMDCCCLXVI', 'MMMDCCCLXVII', 'MMMDCCCLXVIII', 'MMMDCCCLXIX', 'MMMDCCCLXX', 'MMMDCCCLXXI', 'MMMDCCCLXXII', 'MMMDCCCLXXIII', 'MMMDCCCLXXIV', 'MMMDCCCLXXV', 'MMMDCCCLXXVI', 'MMMDCCCLXXVII', 'MMMDCCCLXXVIII', 'MMMDCCCLXXIX', 'MMMDCCCLXXX', 'MMMDCCCLXXXI', 'MMMDCCCLXXXII', 'MMMDCCCLXXXIII', 'MMMDCCCLXXXIV', 'MMMDCCCLXXXV', 'MMMDCCCLXXXVI', 'MMMDCCCLXXXVII', 'MMMDCCCLXXXVIII', 'MMMDCCCLXXXIX', 'MMMDCCCXC', 'MMMDCCCXCI', 'MMMDCCCXCII', 'MMMDCCCXCIII', 'MMMDCCCXCIV', 'MMMDCCCVC', 'MMMDCCCVCI', 'MMMDCCCVCII', 'MMMDCCCVCIII', 'MMMDCCCIC', 'MMMCM', 'MMMCMI', 'MMMCMII', 'MMMCMIII', 'MMMCMIV', 'MMMCMV', 'MMMCMVI', 'MMMCMVII', 'MMMCMVIII', 'MMMCMIX', 'MMMCMX', 'MMMCMXI', 'MMMCMXII', 'MMMCMXIII', 'MMMCMXIV', 'MMMCMXV', 'MMMCMXVI', 'MMMCMXVII', 'MMMCMXVIII', 'MMMCMXIX', 'MMMCMXX', 'MMMCMXXI', 'MMMCMXXII', 'MMMCMXXIII', 'MMMCMXXIV', 'MMMCMXXV', 'MMMCMXXVI', 'MMMCMXXVII', 'MMMCMXXVIII', 'MMMCMXXIX', 'MMMCMXXX', 'MMMCMXXXI', 'MMMCMXXXII', 'MMMCMXXXIII', 'MMMCMXXXIV', 'MMMCMXXXV', 'MMMCMXXXVI', 'MMMCMXXXVII', 'MMMCMXXXVIII', 'MMMCMXXXIX', 'MMMCMXL', 'MMMCMXLI', 'MMMCMXLII', 'MMMCMXLIII', 'MMMCMXLIV', 'MMMCMVL', 'MMMCMVLI', 'MMMCMVLII', 'MMMCMVLIII', 'MMMCMIL', 'MMMLM', 'MMMLMI', 'MMMLMII', 'MMMLMIII', 'MMMLMIV', 'MMMLMV', 'MMMLMVI', 'MMMLMVII', 'MMMLMVIII', 'MMMLMIX', 'MMMLMX', 'MMMLMXI', 'MMMLMXII', 'MMMLMXIII', 'MMMLMXIV', 'MMMLMXV', 'MMMLMXVI', 'MMMLMXVII', 'MMMLMXVIII', 'MMMLMXIX', 'MMMLMXX', 'MMMLMXXI', 'MMMLMXXII', 'MMMLMXXIII', 'MMMLMXXIV', 'MMMLMXXV', 'MMMLMXXVI', 'MMMLMXXVII', 'MMMLMXXVIII', 'MMMLMXXIX', 'MMMLMXXX', 'MMMLMXXXI', 'MMMLMXXXII', 'MMMLMXXXIII', 'MMMLMXXXIV', 'MMMLMXXXV', 'MMMLMXXXVI', 'MMMLMXXXVII', 'MMMLMXXXVIII', 'MMMLMXXXIX', 'MMMXM', 'MMMXMI', 'MMMXMII', 'MMMXMIII', 'MMMXMIV', 'MMMVM', 'MMMVMI', 'MMMVMII', 'MMMVMIII', 'MMMIM', ] +const mode0 = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI', 'XII', 'XIII', 'XIV', 'XV', 'XVI', 'XVII', 'XVIII', 'XIX', 'XX', 'XXI', 'XXII', 'XXIII', 'XXIV', 'XXV', 'XXVI', 'XXVII', 'XXVIII', 'XXIX', 'XXX', 'XXXI', 'XXXII', 'XXXIII', 'XXXIV', 'XXXV', 'XXXVI', 'XXXVII', 'XXXVIII', 'XXXIX', 'XL', 'XLI', 'XLII', 'XLIII', 'XLIV', 'XLV', 'XLVI', 'XLVII', 'XLVIII', 'XLIX', 'L', 'LI', 'LII', 'LIII', 'LIV', 'LV', 'LVI', 'LVII', 'LVIII', 'LIX', 'LX', 'LXI', 'LXII', 'LXIII', 'LXIV', 'LXV', 'LXVI', 'LXVII', 'LXVIII', 'LXIX', 'LXX', 'LXXI', 'LXXII', 'LXXIII', 'LXXIV', 'LXXV', 'LXXVI', 'LXXVII', 'LXXVIII', 'LXXIX', 'LXXX', 'LXXXI', 'LXXXII', 'LXXXIII', 'LXXXIV', 'LXXXV', 'LXXXVI', 'LXXXVII', 'LXXXVIII', 'LXXXIX', 'XC', 'XCI', 'XCII', 'XCIII', 'XCIV', 'XCV', 'XCVI', 'XCVII', 'XCVIII', 'XCIX', 'C', 'CI', 'CII', 'CIII', 'CIV', 'CV', 'CVI', 'CVII', 'CVIII', 'CIX', 'CX', 'CXI', 'CXII', 'CXIII', 'CXIV', 'CXV', 'CXVI', 'CXVII', 'CXVIII', 'CXIX', 'CXX', 'CXXI', 'CXXII', 'CXXIII', 'CXXIV', 'CXXV', 'CXXVI', 'CXXVII', 'CXXVIII', 'CXXIX', 'CXXX', 'CXXXI', 'CXXXII', 'CXXXIII', 'CXXXIV', 'CXXXV', 'CXXXVI', 'CXXXVII', 'CXXXVIII', 'CXXXIX', 'CXL', 'CXLI', 'CXLII', 'CXLIII', 'CXLIV', 'CXLV', 'CXLVI', 'CXLVII', 'CXLVIII', 'CXLIX', 'CL', 'CLI', 'CLII', 'CLIII', 'CLIV', 'CLV', 'CLVI', 'CLVII', 'CLVIII', 'CLIX', 'CLX', 'CLXI', 'CLXII', 'CLXIII', 'CLXIV', 'CLXV', 'CLXVI', 'CLXVII', 'CLXVIII', 'CLXIX', 'CLXX', 'CLXXI', 'CLXXII', 'CLXXIII', 'CLXXIV', 'CLXXV', 'CLXXVI', 'CLXXVII', 'CLXXVIII', 'CLXXIX', 'CLXXX', 'CLXXXI', 'CLXXXII', 'CLXXXIII', 'CLXXXIV', 'CLXXXV', 'CLXXXVI', 'CLXXXVII', 'CLXXXVIII', 'CLXXXIX', 'CXC', 'CXCI', 'CXCII', 'CXCIII', 'CXCIV', 'CXCV', 'CXCVI', 'CXCVII', 'CXCVIII', 'CXCIX', 'CC', 'CCI', 'CCII', 'CCIII', 'CCIV', 'CCV', 'CCVI', 'CCVII', 'CCVIII', 'CCIX', 'CCX', 'CCXI', 'CCXII', 'CCXIII', 'CCXIV', 'CCXV', 'CCXVI', 'CCXVII', 'CCXVIII', 'CCXIX', 'CCXX', 'CCXXI', 'CCXXII', 'CCXXIII', 'CCXXIV', 'CCXXV', 'CCXXVI', 'CCXXVII', 'CCXXVIII', 'CCXXIX', 'CCXXX', 'CCXXXI', 'CCXXXII', 'CCXXXIII', 'CCXXXIV', 'CCXXXV', 'CCXXXVI', 'CCXXXVII', 'CCXXXVIII', 'CCXXXIX', 'CCXL', 'CCXLI', 'CCXLII', 'CCXLIII', 'CCXLIV', 'CCXLV', 'CCXLVI', 'CCXLVII', 'CCXLVIII', 'CCXLIX', 'CCL', 'CCLI', 'CCLII', 'CCLIII', 'CCLIV', 'CCLV', 'CCLVI', 'CCLVII', 'CCLVIII', 'CCLIX', 'CCLX', 'CCLXI', 'CCLXII', 'CCLXIII', 'CCLXIV', 'CCLXV', 'CCLXVI', 'CCLXVII', 'CCLXVIII', 'CCLXIX', 'CCLXX', 'CCLXXI', 'CCLXXII', 'CCLXXIII', 'CCLXXIV', 'CCLXXV', 'CCLXXVI', 'CCLXXVII', 'CCLXXVIII', 'CCLXXIX', 'CCLXXX', 'CCLXXXI', 'CCLXXXII', 'CCLXXXIII', 'CCLXXXIV', 'CCLXXXV', 'CCLXXXVI', 'CCLXXXVII', 'CCLXXXVIII', 'CCLXXXIX', 'CCXC', 'CCXCI', 'CCXCII', 'CCXCIII', 'CCXCIV', 'CCXCV', 'CCXCVI', 'CCXCVII', 'CCXCVIII', 'CCXCIX', 'CCC', 'CCCI', 'CCCII', 'CCCIII', 'CCCIV', 'CCCV', 'CCCVI', 'CCCVII', 'CCCVIII', 'CCCIX', 'CCCX', 'CCCXI', 'CCCXII', 'CCCXIII', 'CCCXIV', 'CCCXV', 'CCCXVI', 'CCCXVII', 'CCCXVIII', 'CCCXIX', 'CCCXX', 'CCCXXI', 'CCCXXII', 'CCCXXIII', 'CCCXXIV', 'CCCXXV', 'CCCXXVI', 'CCCXXVII', 'CCCXXVIII', 'CCCXXIX', 'CCCXXX', 'CCCXXXI', 'CCCXXXII', 'CCCXXXIII', 'CCCXXXIV', 'CCCXXXV', 'CCCXXXVI', 'CCCXXXVII', 'CCCXXXVIII', 'CCCXXXIX', 'CCCXL', 'CCCXLI', 'CCCXLII', 'CCCXLIII', 'CCCXLIV', 'CCCXLV', 'CCCXLVI', 'CCCXLVII', 'CCCXLVIII', 'CCCXLIX', 'CCCL', 'CCCLI', 'CCCLII', 'CCCLIII', 'CCCLIV', 'CCCLV', 'CCCLVI', 'CCCLVII', 'CCCLVIII', 'CCCLIX', 'CCCLX', 'CCCLXI', 'CCCLXII', 'CCCLXIII', 'CCCLXIV', 'CCCLXV', 'CCCLXVI', 'CCCLXVII', 'CCCLXVIII', 'CCCLXIX', 'CCCLXX', 'CCCLXXI', 'CCCLXXII', 'CCCLXXIII', 'CCCLXXIV', 'CCCLXXV', 'CCCLXXVI', 'CCCLXXVII', 'CCCLXXVIII', 'CCCLXXIX', 'CCCLXXX', 'CCCLXXXI', 'CCCLXXXII', 'CCCLXXXIII', 'CCCLXXXIV', 'CCCLXXXV', 'CCCLXXXVI', 'CCCLXXXVII', 'CCCLXXXVIII', 'CCCLXXXIX', 'CCCXC', 'CCCXCI', 'CCCXCII', 'CCCXCIII', 'CCCXCIV', 'CCCXCV', 'CCCXCVI', 'CCCXCVII', 'CCCXCVIII', 'CCCXCIX', 'CD', 'CDI', 'CDII', 'CDIII', 'CDIV', 'CDV', 'CDVI', 'CDVII', 'CDVIII', 'CDIX', 'CDX', 'CDXI', 'CDXII', 'CDXIII', 'CDXIV', 'CDXV', 'CDXVI', 'CDXVII', 'CDXVIII', 'CDXIX', 'CDXX', 'CDXXI', 'CDXXII', 'CDXXIII', 'CDXXIV', 'CDXXV', 'CDXXVI', 'CDXXVII', 'CDXXVIII', 'CDXXIX', 'CDXXX', 'CDXXXI', 'CDXXXII', 'CDXXXIII', 'CDXXXIV', 'CDXXXV', 'CDXXXVI', 'CDXXXVII', 'CDXXXVIII', 'CDXXXIX', 'CDXL', 'CDXLI', 'CDXLII', 'CDXLIII', 'CDXLIV', 'CDXLV', 'CDXLVI', 'CDXLVII', 'CDXLVIII', 'CDXLIX', 'CDL', 'CDLI', 'CDLII', 'CDLIII', 'CDLIV', 'CDLV', 'CDLVI', 'CDLVII', 'CDLVIII', 'CDLIX', 'CDLX', 'CDLXI', 'CDLXII', 'CDLXIII', 'CDLXIV', 'CDLXV', 'CDLXVI', 'CDLXVII', 'CDLXVIII', 'CDLXIX', 'CDLXX', 'CDLXXI', 'CDLXXII', 'CDLXXIII', 'CDLXXIV', 'CDLXXV', 'CDLXXVI', 'CDLXXVII', 'CDLXXVIII', 'CDLXXIX', 'CDLXXX', 'CDLXXXI', 'CDLXXXII', 'CDLXXXIII', 'CDLXXXIV', 'CDLXXXV', 'CDLXXXVI', 'CDLXXXVII', 'CDLXXXVIII', 'CDLXXXIX', 'CDXC', 'CDXCI', 'CDXCII', 'CDXCIII', 'CDXCIV', 'CDXCV', 'CDXCVI', 'CDXCVII', 'CDXCVIII', 'CDXCIX', 'D', 'DI', 'DII', 'DIII', 'DIV', 'DV', 'DVI', 'DVII', 'DVIII', 'DIX', 'DX', 'DXI', 'DXII', 'DXIII', 'DXIV', 'DXV', 'DXVI', 'DXVII', 'DXVIII', 'DXIX', 'DXX', 'DXXI', 'DXXII', 'DXXIII', 'DXXIV', 'DXXV', 'DXXVI', 'DXXVII', 'DXXVIII', 'DXXIX', 'DXXX', 'DXXXI', 'DXXXII', 'DXXXIII', 'DXXXIV', 'DXXXV', 'DXXXVI', 'DXXXVII', 'DXXXVIII', 'DXXXIX', 'DXL', 'DXLI', 'DXLII', 'DXLIII', 'DXLIV', 'DXLV', 'DXLVI', 'DXLVII', 'DXLVIII', 'DXLIX', 'DL', 'DLI', 'DLII', 'DLIII', 'DLIV', 'DLV', 'DLVI', 'DLVII', 'DLVIII', 'DLIX', 'DLX', 'DLXI', 'DLXII', 'DLXIII', 'DLXIV', 'DLXV', 'DLXVI', 'DLXVII', 'DLXVIII', 'DLXIX', 'DLXX', 'DLXXI', 'DLXXII', 'DLXXIII', 'DLXXIV', 'DLXXV', 'DLXXVI', 'DLXXVII', 'DLXXVIII', 'DLXXIX', 'DLXXX', 'DLXXXI', 'DLXXXII', 'DLXXXIII', 'DLXXXIV', 'DLXXXV', 'DLXXXVI', 'DLXXXVII', 'DLXXXVIII', 'DLXXXIX', 'DXC', 'DXCI', 'DXCII', 'DXCIII', 'DXCIV', 'DXCV', 'DXCVI', 'DXCVII', 'DXCVIII', 'DXCIX', 'DC', 'DCI', 'DCII', 'DCIII', 'DCIV', 'DCV', 'DCVI', 'DCVII', 'DCVIII', 'DCIX', 'DCX', 'DCXI', 'DCXII', 'DCXIII', 'DCXIV', 'DCXV', 'DCXVI', 'DCXVII', 'DCXVIII', 'DCXIX', 'DCXX', 'DCXXI', 'DCXXII', 'DCXXIII', 'DCXXIV', 'DCXXV', 'DCXXVI', 'DCXXVII', 'DCXXVIII', 'DCXXIX', 'DCXXX', 'DCXXXI', 'DCXXXII', 'DCXXXIII', 'DCXXXIV', 'DCXXXV', 'DCXXXVI', 'DCXXXVII', 'DCXXXVIII', 'DCXXXIX', 'DCXL', 'DCXLI', 'DCXLII', 'DCXLIII', 'DCXLIV', 'DCXLV', 'DCXLVI', 'DCXLVII', 'DCXLVIII', 'DCXLIX', 'DCL', 'DCLI', 'DCLII', 'DCLIII', 'DCLIV', 'DCLV', 'DCLVI', 'DCLVII', 'DCLVIII', 'DCLIX', 'DCLX', 'DCLXI', 'DCLXII', 'DCLXIII', 'DCLXIV', 'DCLXV', 'DCLXVI', 'DCLXVII', 'DCLXVIII', 'DCLXIX', 'DCLXX', 'DCLXXI', 'DCLXXII', 'DCLXXIII', 'DCLXXIV', 'DCLXXV', 'DCLXXVI', 'DCLXXVII', 'DCLXXVIII', 'DCLXXIX', 'DCLXXX', 'DCLXXXI', 'DCLXXXII', 'DCLXXXIII', 'DCLXXXIV', 'DCLXXXV', 'DCLXXXVI', 'DCLXXXVII', 'DCLXXXVIII', 'DCLXXXIX', 'DCXC', 'DCXCI', 'DCXCII', 'DCXCIII', 'DCXCIV', 'DCXCV', 'DCXCVI', 'DCXCVII', 'DCXCVIII', 'DCXCIX', 'DCC', 'DCCI', 'DCCII', 'DCCIII', 'DCCIV', 'DCCV', 'DCCVI', 'DCCVII', 'DCCVIII', 'DCCIX', 'DCCX', 'DCCXI', 'DCCXII', 'DCCXIII', 'DCCXIV', 'DCCXV', 'DCCXVI', 'DCCXVII', 'DCCXVIII', 'DCCXIX', 'DCCXX', 'DCCXXI', 'DCCXXII', 'DCCXXIII', 'DCCXXIV', 'DCCXXV', 'DCCXXVI', 'DCCXXVII', 'DCCXXVIII', 'DCCXXIX', 'DCCXXX', 'DCCXXXI', 'DCCXXXII', 'DCCXXXIII', 'DCCXXXIV', 'DCCXXXV', 'DCCXXXVI', 'DCCXXXVII', 'DCCXXXVIII', 'DCCXXXIX', 'DCCXL', 'DCCXLI', 'DCCXLII', 'DCCXLIII', 'DCCXLIV', 'DCCXLV', 'DCCXLVI', 'DCCXLVII', 'DCCXLVIII', 'DCCXLIX', 'DCCL', 'DCCLI', 'DCCLII', 'DCCLIII', 'DCCLIV', 'DCCLV', 'DCCLVI', 'DCCLVII', 'DCCLVIII', 'DCCLIX', 'DCCLX', 'DCCLXI', 'DCCLXII', 'DCCLXIII', 'DCCLXIV', 'DCCLXV', 'DCCLXVI', 'DCCLXVII', 'DCCLXVIII', 'DCCLXIX', 'DCCLXX', 'DCCLXXI', 'DCCLXXII', 'DCCLXXIII', 'DCCLXXIV', 'DCCLXXV', 'DCCLXXVI', 'DCCLXXVII', 'DCCLXXVIII', 'DCCLXXIX', 'DCCLXXX', 'DCCLXXXI', 'DCCLXXXII', 'DCCLXXXIII', 'DCCLXXXIV', 'DCCLXXXV', 'DCCLXXXVI', 'DCCLXXXVII', 'DCCLXXXVIII', 'DCCLXXXIX', 'DCCXC', 'DCCXCI', 'DCCXCII', 'DCCXCIII', 'DCCXCIV', 'DCCXCV', 'DCCXCVI', 'DCCXCVII', 'DCCXCVIII', 'DCCXCIX', 'DCCC', 'DCCCI', 'DCCCII', 'DCCCIII', 'DCCCIV', 'DCCCV', 'DCCCVI', 'DCCCVII', 'DCCCVIII', 'DCCCIX', 'DCCCX', 'DCCCXI', 'DCCCXII', 'DCCCXIII', 'DCCCXIV', 'DCCCXV', 'DCCCXVI', 'DCCCXVII', 'DCCCXVIII', 'DCCCXIX', 'DCCCXX', 'DCCCXXI', 'DCCCXXII', 'DCCCXXIII', 'DCCCXXIV', 'DCCCXXV', 'DCCCXXVI', 'DCCCXXVII', 'DCCCXXVIII', 'DCCCXXIX', 'DCCCXXX', 'DCCCXXXI', 'DCCCXXXII', 'DCCCXXXIII', 'DCCCXXXIV', 'DCCCXXXV', 'DCCCXXXVI', 'DCCCXXXVII', 'DCCCXXXVIII', 'DCCCXXXIX', 'DCCCXL', 'DCCCXLI', 'DCCCXLII', 'DCCCXLIII', 'DCCCXLIV', 'DCCCXLV', 'DCCCXLVI', 'DCCCXLVII', 'DCCCXLVIII', 'DCCCXLIX', 'DCCCL', 'DCCCLI', 'DCCCLII', 'DCCCLIII', 'DCCCLIV', 'DCCCLV', 'DCCCLVI', 'DCCCLVII', 'DCCCLVIII', 'DCCCLIX', 'DCCCLX', 'DCCCLXI', 'DCCCLXII', 'DCCCLXIII', 'DCCCLXIV', 'DCCCLXV', 'DCCCLXVI', 'DCCCLXVII', 'DCCCLXVIII', 'DCCCLXIX', 'DCCCLXX', 'DCCCLXXI', 'DCCCLXXII', 'DCCCLXXIII', 'DCCCLXXIV', 'DCCCLXXV', 'DCCCLXXVI', 'DCCCLXXVII', 'DCCCLXXVIII', 'DCCCLXXIX', 'DCCCLXXX', 'DCCCLXXXI', 'DCCCLXXXII', 'DCCCLXXXIII', 'DCCCLXXXIV', 'DCCCLXXXV', 'DCCCLXXXVI', 'DCCCLXXXVII', 'DCCCLXXXVIII', 'DCCCLXXXIX', 'DCCCXC', 'DCCCXCI', 'DCCCXCII', 'DCCCXCIII', 'DCCCXCIV', 'DCCCXCV', 'DCCCXCVI', 'DCCCXCVII', 'DCCCXCVIII', 'DCCCXCIX', 'CM', 'CMI', 'CMII', 'CMIII', 'CMIV', 'CMV', 'CMVI', 'CMVII', 'CMVIII', 'CMIX', 'CMX', 'CMXI', 'CMXII', 'CMXIII', 'CMXIV', 'CMXV', 'CMXVI', 'CMXVII', 'CMXVIII', 'CMXIX', 'CMXX', 'CMXXI', 'CMXXII', 'CMXXIII', 'CMXXIV', 'CMXXV', 'CMXXVI', 'CMXXVII', 'CMXXVIII', 'CMXXIX', 'CMXXX', 'CMXXXI', 'CMXXXII', 'CMXXXIII', 'CMXXXIV', 'CMXXXV', 'CMXXXVI', 'CMXXXVII', 'CMXXXVIII', 'CMXXXIX', 'CMXL', 'CMXLI', 'CMXLII', 'CMXLIII', 'CMXLIV', 'CMXLV', 'CMXLVI', 'CMXLVII', 'CMXLVIII', 'CMXLIX', 'CML', 'CMLI', 'CMLII', 'CMLIII', 'CMLIV', 'CMLV', 'CMLVI', 'CMLVII', 'CMLVIII', 'CMLIX', 'CMLX', 'CMLXI', 'CMLXII', 'CMLXIII', 'CMLXIV', 'CMLXV', 'CMLXVI', 'CMLXVII', 'CMLXVIII', 'CMLXIX', 'CMLXX', 'CMLXXI', 'CMLXXII', 'CMLXXIII', 'CMLXXIV', 'CMLXXV', 'CMLXXVI', 'CMLXXVII', 'CMLXXVIII', 'CMLXXIX', 'CMLXXX', 'CMLXXXI', 'CMLXXXII', 'CMLXXXIII', 'CMLXXXIV', 'CMLXXXV', 'CMLXXXVI', 'CMLXXXVII', 'CMLXXXVIII', 'CMLXXXIX', 'CMXC', 'CMXCI', 'CMXCII', 'CMXCIII', 'CMXCIV', 'CMXCV', 'CMXCVI', 'CMXCVII', 'CMXCVIII', 'CMXCIX', 'M', 'MI', 'MII', 'MIII', 'MIV', 'MV', 'MVI', 'MVII', 'MVIII', 'MIX', 'MX', 'MXI', 'MXII', 'MXIII', 'MXIV', 'MXV', 'MXVI', 'MXVII', 'MXVIII', 'MXIX', 'MXX', 'MXXI', 'MXXII', 'MXXIII', 'MXXIV', 'MXXV', 'MXXVI', 'MXXVII', 'MXXVIII', 'MXXIX', 'MXXX', 'MXXXI', 'MXXXII', 'MXXXIII', 'MXXXIV', 'MXXXV', 'MXXXVI', 'MXXXVII', 'MXXXVIII', 'MXXXIX', 'MXL', 'MXLI', 'MXLII', 'MXLIII', 'MXLIV', 'MXLV', 'MXLVI', 'MXLVII', 'MXLVIII', 'MXLIX', 'ML', 'MLI', 'MLII', 'MLIII', 'MLIV', 'MLV', 'MLVI', 'MLVII', 'MLVIII', 'MLIX', 'MLX', 'MLXI', 'MLXII', 'MLXIII', 'MLXIV', 'MLXV', 'MLXVI', 'MLXVII', 'MLXVIII', 'MLXIX', 'MLXX', 'MLXXI', 'MLXXII', 'MLXXIII', 'MLXXIV', 'MLXXV', 'MLXXVI', 'MLXXVII', 'MLXXVIII', 'MLXXIX', 'MLXXX', 'MLXXXI', 'MLXXXII', 'MLXXXIII', 'MLXXXIV', 'MLXXXV', 'MLXXXVI', 'MLXXXVII', 'MLXXXVIII', 'MLXXXIX', 'MXC', 'MXCI', 'MXCII', 'MXCIII', 'MXCIV', 'MXCV', 'MXCVI', 'MXCVII', 'MXCVIII', 'MXCIX', 'MC', 'MCI', 'MCII', 'MCIII', 'MCIV', 'MCV', 'MCVI', 'MCVII', 'MCVIII', 'MCIX', 'MCX', 'MCXI', 'MCXII', 'MCXIII', 'MCXIV', 'MCXV', 'MCXVI', 'MCXVII', 'MCXVIII', 'MCXIX', 'MCXX', 'MCXXI', 'MCXXII', 'MCXXIII', 'MCXXIV', 'MCXXV', 'MCXXVI', 'MCXXVII', 'MCXXVIII', 'MCXXIX', 'MCXXX', 'MCXXXI', 'MCXXXII', 'MCXXXIII', 'MCXXXIV', 'MCXXXV', 'MCXXXVI', 'MCXXXVII', 'MCXXXVIII', 'MCXXXIX', 'MCXL', 'MCXLI', 'MCXLII', 'MCXLIII', 'MCXLIV', 'MCXLV', 'MCXLVI', 'MCXLVII', 'MCXLVIII', 'MCXLIX', 'MCL', 'MCLI', 'MCLII', 'MCLIII', 'MCLIV', 'MCLV', 'MCLVI', 'MCLVII', 'MCLVIII', 'MCLIX', 'MCLX', 'MCLXI', 'MCLXII', 'MCLXIII', 'MCLXIV', 'MCLXV', 'MCLXVI', 'MCLXVII', 'MCLXVIII', 'MCLXIX', 'MCLXX', 'MCLXXI', 'MCLXXII', 'MCLXXIII', 'MCLXXIV', 'MCLXXV', 'MCLXXVI', 'MCLXXVII', 'MCLXXVIII', 'MCLXXIX', 'MCLXXX', 'MCLXXXI', 'MCLXXXII', 'MCLXXXIII', 'MCLXXXIV', 'MCLXXXV', 'MCLXXXVI', 'MCLXXXVII', 'MCLXXXVIII', 'MCLXXXIX', 'MCXC', 'MCXCI', 'MCXCII', 'MCXCIII', 'MCXCIV', 'MCXCV', 'MCXCVI', 'MCXCVII', 'MCXCVIII', 'MCXCIX', 'MCC', 'MCCI', 'MCCII', 'MCCIII', 'MCCIV', 'MCCV', 'MCCVI', 'MCCVII', 'MCCVIII', 'MCCIX', 'MCCX', 'MCCXI', 'MCCXII', 'MCCXIII', 'MCCXIV', 'MCCXV', 'MCCXVI', 'MCCXVII', 'MCCXVIII', 'MCCXIX', 'MCCXX', 'MCCXXI', 'MCCXXII', 'MCCXXIII', 'MCCXXIV', 'MCCXXV', 'MCCXXVI', 'MCCXXVII', 'MCCXXVIII', 'MCCXXIX', 'MCCXXX', 'MCCXXXI', 'MCCXXXII', 'MCCXXXIII', 'MCCXXXIV', 'MCCXXXV', 'MCCXXXVI', 'MCCXXXVII', 'MCCXXXVIII', 'MCCXXXIX', 'MCCXL', 'MCCXLI', 'MCCXLII', 'MCCXLIII', 'MCCXLIV', 'MCCXLV', 'MCCXLVI', 'MCCXLVII', 'MCCXLVIII', 'MCCXLIX', 'MCCL', 'MCCLI', 'MCCLII', 'MCCLIII', 'MCCLIV', 'MCCLV', 'MCCLVI', 'MCCLVII', 'MCCLVIII', 'MCCLIX', 'MCCLX', 'MCCLXI', 'MCCLXII', 'MCCLXIII', 'MCCLXIV', 'MCCLXV', 'MCCLXVI', 'MCCLXVII', 'MCCLXVIII', 'MCCLXIX', 'MCCLXX', 'MCCLXXI', 'MCCLXXII', 'MCCLXXIII', 'MCCLXXIV', 'MCCLXXV', 'MCCLXXVI', 'MCCLXXVII', 'MCCLXXVIII', 'MCCLXXIX', 'MCCLXXX', 'MCCLXXXI', 'MCCLXXXII', 'MCCLXXXIII', 'MCCLXXXIV', 'MCCLXXXV', 'MCCLXXXVI', 'MCCLXXXVII', 'MCCLXXXVIII', 'MCCLXXXIX', 'MCCXC', 'MCCXCI', 'MCCXCII', 'MCCXCIII', 'MCCXCIV', 'MCCXCV', 'MCCXCVI', 'MCCXCVII', 'MCCXCVIII', 'MCCXCIX', 'MCCC', 'MCCCI', 'MCCCII', 'MCCCIII', 'MCCCIV', 'MCCCV', 'MCCCVI', 'MCCCVII', 'MCCCVIII', 'MCCCIX', 'MCCCX', 'MCCCXI', 'MCCCXII', 'MCCCXIII', 'MCCCXIV', 'MCCCXV', 'MCCCXVI', 'MCCCXVII', 'MCCCXVIII', 'MCCCXIX', 'MCCCXX', 'MCCCXXI', 'MCCCXXII', 'MCCCXXIII', 'MCCCXXIV', 'MCCCXXV', 'MCCCXXVI', 'MCCCXXVII', 'MCCCXXVIII', 'MCCCXXIX', 'MCCCXXX', 'MCCCXXXI', 'MCCCXXXII', 'MCCCXXXIII', 'MCCCXXXIV', 'MCCCXXXV', 'MCCCXXXVI', 'MCCCXXXVII', 'MCCCXXXVIII', 'MCCCXXXIX', 'MCCCXL', 'MCCCXLI', 'MCCCXLII', 'MCCCXLIII', 'MCCCXLIV', 'MCCCXLV', 'MCCCXLVI', 'MCCCXLVII', 'MCCCXLVIII', 'MCCCXLIX', 'MCCCL', 'MCCCLI', 'MCCCLII', 'MCCCLIII', 'MCCCLIV', 'MCCCLV', 'MCCCLVI', 'MCCCLVII', 'MCCCLVIII', 'MCCCLIX', 'MCCCLX', 'MCCCLXI', 'MCCCLXII', 'MCCCLXIII', 'MCCCLXIV', 'MCCCLXV', 'MCCCLXVI', 'MCCCLXVII', 'MCCCLXVIII', 'MCCCLXIX', 'MCCCLXX', 'MCCCLXXI', 'MCCCLXXII', 'MCCCLXXIII', 'MCCCLXXIV', 'MCCCLXXV', 'MCCCLXXVI', 'MCCCLXXVII', 'MCCCLXXVIII', 'MCCCLXXIX', 'MCCCLXXX', 'MCCCLXXXI', 'MCCCLXXXII', 'MCCCLXXXIII', 'MCCCLXXXIV', 'MCCCLXXXV', 'MCCCLXXXVI', 'MCCCLXXXVII', 'MCCCLXXXVIII', 'MCCCLXXXIX', 'MCCCXC', 'MCCCXCI', 'MCCCXCII', 'MCCCXCIII', 'MCCCXCIV', 'MCCCXCV', 'MCCCXCVI', 'MCCCXCVII', 'MCCCXCVIII', 'MCCCXCIX', 'MCD', 'MCDI', 'MCDII', 'MCDIII', 'MCDIV', 'MCDV', 'MCDVI', 'MCDVII', 'MCDVIII', 'MCDIX', 'MCDX', 'MCDXI', 'MCDXII', 'MCDXIII', 'MCDXIV', 'MCDXV', 'MCDXVI', 'MCDXVII', 'MCDXVIII', 'MCDXIX', 'MCDXX', 'MCDXXI', 'MCDXXII', 'MCDXXIII', 'MCDXXIV', 'MCDXXV', 'MCDXXVI', 'MCDXXVII', 'MCDXXVIII', 'MCDXXIX', 'MCDXXX', 'MCDXXXI', 'MCDXXXII', 'MCDXXXIII', 'MCDXXXIV', 'MCDXXXV', 'MCDXXXVI', 'MCDXXXVII', 'MCDXXXVIII', 'MCDXXXIX', 'MCDXL', 'MCDXLI', 'MCDXLII', 'MCDXLIII', 'MCDXLIV', 'MCDXLV', 'MCDXLVI', 'MCDXLVII', 'MCDXLVIII', 'MCDXLIX', 'MCDL', 'MCDLI', 'MCDLII', 'MCDLIII', 'MCDLIV', 'MCDLV', 'MCDLVI', 'MCDLVII', 'MCDLVIII', 'MCDLIX', 'MCDLX', 'MCDLXI', 'MCDLXII', 'MCDLXIII', 'MCDLXIV', 'MCDLXV', 'MCDLXVI', 'MCDLXVII', 'MCDLXVIII', 'MCDLXIX', 'MCDLXX', 'MCDLXXI', 'MCDLXXII', 'MCDLXXIII', 'MCDLXXIV', 'MCDLXXV', 'MCDLXXVI', 'MCDLXXVII', 'MCDLXXVIII', 'MCDLXXIX', 'MCDLXXX', 'MCDLXXXI', 'MCDLXXXII', 'MCDLXXXIII', 'MCDLXXXIV', 'MCDLXXXV', 'MCDLXXXVI', 'MCDLXXXVII', 'MCDLXXXVIII', 'MCDLXXXIX', 'MCDXC', 'MCDXCI', 'MCDXCII', 'MCDXCIII', 'MCDXCIV', 'MCDXCV', 'MCDXCVI', 'MCDXCVII', 'MCDXCVIII', 'MCDXCIX', 'MD', 'MDI', 'MDII', 'MDIII', 'MDIV', 'MDV', 'MDVI', 'MDVII', 'MDVIII', 'MDIX', 'MDX', 'MDXI', 'MDXII', 'MDXIII', 'MDXIV', 'MDXV', 'MDXVI', 'MDXVII', 'MDXVIII', 'MDXIX', 'MDXX', 'MDXXI', 'MDXXII', 'MDXXIII', 'MDXXIV', 'MDXXV', 'MDXXVI', 'MDXXVII', 'MDXXVIII', 'MDXXIX', 'MDXXX', 'MDXXXI', 'MDXXXII', 'MDXXXIII', 'MDXXXIV', 'MDXXXV', 'MDXXXVI', 'MDXXXVII', 'MDXXXVIII', 'MDXXXIX', 'MDXL', 'MDXLI', 'MDXLII', 'MDXLIII', 'MDXLIV', 'MDXLV', 'MDXLVI', 'MDXLVII', 'MDXLVIII', 'MDXLIX', 'MDL', 'MDLI', 'MDLII', 'MDLIII', 'MDLIV', 'MDLV', 'MDLVI', 'MDLVII', 'MDLVIII', 'MDLIX', 'MDLX', 'MDLXI', 'MDLXII', 'MDLXIII', 'MDLXIV', 'MDLXV', 'MDLXVI', 'MDLXVII', 'MDLXVIII', 'MDLXIX', 'MDLXX', 'MDLXXI', 'MDLXXII', 'MDLXXIII', 'MDLXXIV', 'MDLXXV', 'MDLXXVI', 'MDLXXVII', 'MDLXXVIII', 'MDLXXIX', 'MDLXXX', 'MDLXXXI', 'MDLXXXII', 'MDLXXXIII', 'MDLXXXIV', 'MDLXXXV', 'MDLXXXVI', 'MDLXXXVII', 'MDLXXXVIII', 'MDLXXXIX', 'MDXC', 'MDXCI', 'MDXCII', 'MDXCIII', 'MDXCIV', 'MDXCV', 'MDXCVI', 'MDXCVII', 'MDXCVIII', 'MDXCIX', 'MDC', 'MDCI', 'MDCII', 'MDCIII', 'MDCIV', 'MDCV', 'MDCVI', 'MDCVII', 'MDCVIII', 'MDCIX', 'MDCX', 'MDCXI', 'MDCXII', 'MDCXIII', 'MDCXIV', 'MDCXV', 'MDCXVI', 'MDCXVII', 'MDCXVIII', 'MDCXIX', 'MDCXX', 'MDCXXI', 'MDCXXII', 'MDCXXIII', 'MDCXXIV', 'MDCXXV', 'MDCXXVI', 'MDCXXVII', 'MDCXXVIII', 'MDCXXIX', 'MDCXXX', 'MDCXXXI', 'MDCXXXII', 'MDCXXXIII', 'MDCXXXIV', 'MDCXXXV', 'MDCXXXVI', 'MDCXXXVII', 'MDCXXXVIII', 'MDCXXXIX', 'MDCXL', 'MDCXLI', 'MDCXLII', 'MDCXLIII', 'MDCXLIV', 'MDCXLV', 'MDCXLVI', 'MDCXLVII', 'MDCXLVIII', 'MDCXLIX', 'MDCL', 'MDCLI', 'MDCLII', 'MDCLIII', 'MDCLIV', 'MDCLV', 'MDCLVI', 'MDCLVII', 'MDCLVIII', 'MDCLIX', 'MDCLX', 'MDCLXI', 'MDCLXII', 'MDCLXIII', 'MDCLXIV', 'MDCLXV', 'MDCLXVI', 'MDCLXVII', 'MDCLXVIII', 'MDCLXIX', 'MDCLXX', 'MDCLXXI', 'MDCLXXII', 'MDCLXXIII', 'MDCLXXIV', 'MDCLXXV', 'MDCLXXVI', 'MDCLXXVII', 'MDCLXXVIII', 'MDCLXXIX', 'MDCLXXX', 'MDCLXXXI', 'MDCLXXXII', 'MDCLXXXIII', 'MDCLXXXIV', 'MDCLXXXV', 'MDCLXXXVI', 'MDCLXXXVII', 'MDCLXXXVIII', 'MDCLXXXIX', 'MDCXC', 'MDCXCI', 'MDCXCII', 'MDCXCIII', 'MDCXCIV', 'MDCXCV', 'MDCXCVI', 'MDCXCVII', 'MDCXCVIII', 'MDCXCIX', 'MDCC', 'MDCCI', 'MDCCII', 'MDCCIII', 'MDCCIV', 'MDCCV', 'MDCCVI', 'MDCCVII', 'MDCCVIII', 'MDCCIX', 'MDCCX', 'MDCCXI', 'MDCCXII', 'MDCCXIII', 'MDCCXIV', 'MDCCXV', 'MDCCXVI', 'MDCCXVII', 'MDCCXVIII', 'MDCCXIX', 'MDCCXX', 'MDCCXXI', 'MDCCXXII', 'MDCCXXIII', 'MDCCXXIV', 'MDCCXXV', 'MDCCXXVI', 'MDCCXXVII', 'MDCCXXVIII', 'MDCCXXIX', 'MDCCXXX', 'MDCCXXXI', 'MDCCXXXII', 'MDCCXXXIII', 'MDCCXXXIV', 'MDCCXXXV', 'MDCCXXXVI', 'MDCCXXXVII', 'MDCCXXXVIII', 'MDCCXXXIX', 'MDCCXL', 'MDCCXLI', 'MDCCXLII', 'MDCCXLIII', 'MDCCXLIV', 'MDCCXLV', 'MDCCXLVI', 'MDCCXLVII', 'MDCCXLVIII', 'MDCCXLIX', 'MDCCL', 'MDCCLI', 'MDCCLII', 'MDCCLIII', 'MDCCLIV', 'MDCCLV', 'MDCCLVI', 'MDCCLVII', 'MDCCLVIII', 'MDCCLIX', 'MDCCLX', 'MDCCLXI', 'MDCCLXII', 'MDCCLXIII', 'MDCCLXIV', 'MDCCLXV', 'MDCCLXVI', 'MDCCLXVII', 'MDCCLXVIII', 'MDCCLXIX', 'MDCCLXX', 'MDCCLXXI', 'MDCCLXXII', 'MDCCLXXIII', 'MDCCLXXIV', 'MDCCLXXV', 'MDCCLXXVI', 'MDCCLXXVII', 'MDCCLXXVIII', 'MDCCLXXIX', 'MDCCLXXX', 'MDCCLXXXI', 'MDCCLXXXII', 'MDCCLXXXIII', 'MDCCLXXXIV', 'MDCCLXXXV', 'MDCCLXXXVI', 'MDCCLXXXVII', 'MDCCLXXXVIII', 'MDCCLXXXIX', 'MDCCXC', 'MDCCXCI', 'MDCCXCII', 'MDCCXCIII', 'MDCCXCIV', 'MDCCXCV', 'MDCCXCVI', 'MDCCXCVII', 'MDCCXCVIII', 'MDCCXCIX', 'MDCCC', 'MDCCCI', 'MDCCCII', 'MDCCCIII', 'MDCCCIV', 'MDCCCV', 'MDCCCVI', 'MDCCCVII', 'MDCCCVIII', 'MDCCCIX', 'MDCCCX', 'MDCCCXI', 'MDCCCXII', 'MDCCCXIII', 'MDCCCXIV', 'MDCCCXV', 'MDCCCXVI', 'MDCCCXVII', 'MDCCCXVIII', 'MDCCCXIX', 'MDCCCXX', 'MDCCCXXI', 'MDCCCXXII', 'MDCCCXXIII', 'MDCCCXXIV', 'MDCCCXXV', 'MDCCCXXVI', 'MDCCCXXVII', 'MDCCCXXVIII', 'MDCCCXXIX', 'MDCCCXXX', 'MDCCCXXXI', 'MDCCCXXXII', 'MDCCCXXXIII', 'MDCCCXXXIV', 'MDCCCXXXV', 'MDCCCXXXVI', 'MDCCCXXXVII', 'MDCCCXXXVIII', 'MDCCCXXXIX', 'MDCCCXL', 'MDCCCXLI', 'MDCCCXLII', 'MDCCCXLIII', 'MDCCCXLIV', 'MDCCCXLV', 'MDCCCXLVI', 'MDCCCXLVII', 'MDCCCXLVIII', 'MDCCCXLIX', 'MDCCCL', 'MDCCCLI', 'MDCCCLII', 'MDCCCLIII', 'MDCCCLIV', 'MDCCCLV', 'MDCCCLVI', 'MDCCCLVII', 'MDCCCLVIII', 'MDCCCLIX', 'MDCCCLX', 'MDCCCLXI', 'MDCCCLXII', 'MDCCCLXIII', 'MDCCCLXIV', 'MDCCCLXV', 'MDCCCLXVI', 'MDCCCLXVII', 'MDCCCLXVIII', 'MDCCCLXIX', 'MDCCCLXX', 'MDCCCLXXI', 'MDCCCLXXII', 'MDCCCLXXIII', 'MDCCCLXXIV', 'MDCCCLXXV', 'MDCCCLXXVI', 'MDCCCLXXVII', 'MDCCCLXXVIII', 'MDCCCLXXIX', 'MDCCCLXXX', 'MDCCCLXXXI', 'MDCCCLXXXII', 'MDCCCLXXXIII', 'MDCCCLXXXIV', 'MDCCCLXXXV', 'MDCCCLXXXVI', 'MDCCCLXXXVII', 'MDCCCLXXXVIII', 'MDCCCLXXXIX', 'MDCCCXC', 'MDCCCXCI', 'MDCCCXCII', 'MDCCCXCIII', 'MDCCCXCIV', 'MDCCCXCV', 'MDCCCXCVI', 'MDCCCXCVII', 'MDCCCXCVIII', 'MDCCCXCIX', 'MCM', 'MCMI', 'MCMII', 'MCMIII', 'MCMIV', 'MCMV', 'MCMVI', 'MCMVII', 'MCMVIII', 'MCMIX', 'MCMX', 'MCMXI', 'MCMXII', 'MCMXIII', 'MCMXIV', 'MCMXV', 'MCMXVI', 'MCMXVII', 'MCMXVIII', 'MCMXIX', 'MCMXX', 'MCMXXI', 'MCMXXII', 'MCMXXIII', 'MCMXXIV', 'MCMXXV', 'MCMXXVI', 'MCMXXVII', 'MCMXXVIII', 'MCMXXIX', 'MCMXXX', 'MCMXXXI', 'MCMXXXII', 'MCMXXXIII', 'MCMXXXIV', 'MCMXXXV', 'MCMXXXVI', 'MCMXXXVII', 'MCMXXXVIII', 'MCMXXXIX', 'MCMXL', 'MCMXLI', 'MCMXLII', 'MCMXLIII', 'MCMXLIV', 'MCMXLV', 'MCMXLVI', 'MCMXLVII', 'MCMXLVIII', 'MCMXLIX', 'MCML', 'MCMLI', 'MCMLII', 'MCMLIII', 'MCMLIV', 'MCMLV', 'MCMLVI', 'MCMLVII', 'MCMLVIII', 'MCMLIX', 'MCMLX', 'MCMLXI', 'MCMLXII', 'MCMLXIII', 'MCMLXIV', 'MCMLXV', 'MCMLXVI', 'MCMLXVII', 'MCMLXVIII', 'MCMLXIX', 'MCMLXX', 'MCMLXXI', 'MCMLXXII', 'MCMLXXIII', 'MCMLXXIV', 'MCMLXXV', 'MCMLXXVI', 'MCMLXXVII', 'MCMLXXVIII', 'MCMLXXIX', 'MCMLXXX', 'MCMLXXXI', 'MCMLXXXII', 'MCMLXXXIII', 'MCMLXXXIV', 'MCMLXXXV', 'MCMLXXXVI', 'MCMLXXXVII', 'MCMLXXXVIII', 'MCMLXXXIX', 'MCMXC', 'MCMXCI', 'MCMXCII', 'MCMXCIII', 'MCMXCIV', 'MCMXCV', 'MCMXCVI', 'MCMXCVII', 'MCMXCVIII', 'MCMXCIX', 'MM', 'MMI', 'MMII', 'MMIII', 'MMIV', 'MMV', 'MMVI', 'MMVII', 'MMVIII', 'MMIX', 'MMX', 'MMXI', 'MMXII', 'MMXIII', 'MMXIV', 'MMXV', 'MMXVI', 'MMXVII', 'MMXVIII', 'MMXIX', 'MMXX', 'MMXXI', 'MMXXII', 'MMXXIII', 'MMXXIV', 'MMXXV', 'MMXXVI', 'MMXXVII', 'MMXXVIII', 'MMXXIX', 'MMXXX', 'MMXXXI', 'MMXXXII', 'MMXXXIII', 'MMXXXIV', 'MMXXXV', 'MMXXXVI', 'MMXXXVII', 'MMXXXVIII', 'MMXXXIX', 'MMXL', 'MMXLI', 'MMXLII', 'MMXLIII', 'MMXLIV', 'MMXLV', 'MMXLVI', 'MMXLVII', 'MMXLVIII', 'MMXLIX', 'MML', 'MMLI', 'MMLII', 'MMLIII', 'MMLIV', 'MMLV', 'MMLVI', 'MMLVII', 'MMLVIII', 'MMLIX', 'MMLX', 'MMLXI', 'MMLXII', 'MMLXIII', 'MMLXIV', 'MMLXV', 'MMLXVI', 'MMLXVII', 'MMLXVIII', 'MMLXIX', 'MMLXX', 'MMLXXI', 'MMLXXII', 'MMLXXIII', 'MMLXXIV', 'MMLXXV', 'MMLXXVI', 'MMLXXVII', 'MMLXXVIII', 'MMLXXIX', 'MMLXXX', 'MMLXXXI', 'MMLXXXII', 'MMLXXXIII', 'MMLXXXIV', 'MMLXXXV', 'MMLXXXVI', 'MMLXXXVII', 'MMLXXXVIII', 'MMLXXXIX', 'MMXC', 'MMXCI', 'MMXCII', 'MMXCIII', 'MMXCIV', 'MMXCV', 'MMXCVI', 'MMXCVII', 'MMXCVIII', 'MMXCIX', 'MMC', 'MMCI', 'MMCII', 'MMCIII', 'MMCIV', 'MMCV', 'MMCVI', 'MMCVII', 'MMCVIII', 'MMCIX', 'MMCX', 'MMCXI', 'MMCXII', 'MMCXIII', 'MMCXIV', 'MMCXV', 'MMCXVI', 'MMCXVII', 'MMCXVIII', 'MMCXIX', 'MMCXX', 'MMCXXI', 'MMCXXII', 'MMCXXIII', 'MMCXXIV', 'MMCXXV', 'MMCXXVI', 'MMCXXVII', 'MMCXXVIII', 'MMCXXIX', 'MMCXXX', 'MMCXXXI', 'MMCXXXII', 'MMCXXXIII', 'MMCXXXIV', 'MMCXXXV', 'MMCXXXVI', 'MMCXXXVII', 'MMCXXXVIII', 'MMCXXXIX', 'MMCXL', 'MMCXLI', 'MMCXLII', 'MMCXLIII', 'MMCXLIV', 'MMCXLV', 'MMCXLVI', 'MMCXLVII', 'MMCXLVIII', 'MMCXLIX', 'MMCL', 'MMCLI', 'MMCLII', 'MMCLIII', 'MMCLIV', 'MMCLV', 'MMCLVI', 'MMCLVII', 'MMCLVIII', 'MMCLIX', 'MMCLX', 'MMCLXI', 'MMCLXII', 'MMCLXIII', 'MMCLXIV', 'MMCLXV', 'MMCLXVI', 'MMCLXVII', 'MMCLXVIII', 'MMCLXIX', 'MMCLXX', 'MMCLXXI', 'MMCLXXII', 'MMCLXXIII', 'MMCLXXIV', 'MMCLXXV', 'MMCLXXVI', 'MMCLXXVII', 'MMCLXXVIII', 'MMCLXXIX', 'MMCLXXX', 'MMCLXXXI', 'MMCLXXXII', 'MMCLXXXIII', 'MMCLXXXIV', 'MMCLXXXV', 'MMCLXXXVI', 'MMCLXXXVII', 'MMCLXXXVIII', 'MMCLXXXIX', 'MMCXC', 'MMCXCI', 'MMCXCII', 'MMCXCIII', 'MMCXCIV', 'MMCXCV', 'MMCXCVI', 'MMCXCVII', 'MMCXCVIII', 'MMCXCIX', 'MMCC', 'MMCCI', 'MMCCII', 'MMCCIII', 'MMCCIV', 'MMCCV', 'MMCCVI', 'MMCCVII', 'MMCCVIII', 'MMCCIX', 'MMCCX', 'MMCCXI', 'MMCCXII', 'MMCCXIII', 'MMCCXIV', 'MMCCXV', 'MMCCXVI', 'MMCCXVII', 'MMCCXVIII', 'MMCCXIX', 'MMCCXX', 'MMCCXXI', 'MMCCXXII', 'MMCCXXIII', 'MMCCXXIV', 'MMCCXXV', 'MMCCXXVI', 'MMCCXXVII', 'MMCCXXVIII', 'MMCCXXIX', 'MMCCXXX', 'MMCCXXXI', 'MMCCXXXII', 'MMCCXXXIII', 'MMCCXXXIV', 'MMCCXXXV', 'MMCCXXXVI', 'MMCCXXXVII', 'MMCCXXXVIII', 'MMCCXXXIX', 'MMCCXL', 'MMCCXLI', 'MMCCXLII', 'MMCCXLIII', 'MMCCXLIV', 'MMCCXLV', 'MMCCXLVI', 'MMCCXLVII', 'MMCCXLVIII', 'MMCCXLIX', 'MMCCL', 'MMCCLI', 'MMCCLII', 'MMCCLIII', 'MMCCLIV', 'MMCCLV', 'MMCCLVI', 'MMCCLVII', 'MMCCLVIII', 'MMCCLIX', 'MMCCLX', 'MMCCLXI', 'MMCCLXII', 'MMCCLXIII', 'MMCCLXIV', 'MMCCLXV', 'MMCCLXVI', 'MMCCLXVII', 'MMCCLXVIII', 'MMCCLXIX', 'MMCCLXX', 'MMCCLXXI', 'MMCCLXXII', 'MMCCLXXIII', 'MMCCLXXIV', 'MMCCLXXV', 'MMCCLXXVI', 'MMCCLXXVII', 'MMCCLXXVIII', 'MMCCLXXIX', 'MMCCLXXX', 'MMCCLXXXI', 'MMCCLXXXII', 'MMCCLXXXIII', 'MMCCLXXXIV', 'MMCCLXXXV', 'MMCCLXXXVI', 'MMCCLXXXVII', 'MMCCLXXXVIII', 'MMCCLXXXIX', 'MMCCXC', 'MMCCXCI', 'MMCCXCII', 'MMCCXCIII', 'MMCCXCIV', 'MMCCXCV', 'MMCCXCVI', 'MMCCXCVII', 'MMCCXCVIII', 'MMCCXCIX', 'MMCCC', 'MMCCCI', 'MMCCCII', 'MMCCCIII', 'MMCCCIV', 'MMCCCV', 'MMCCCVI', 'MMCCCVII', 'MMCCCVIII', 'MMCCCIX', 'MMCCCX', 'MMCCCXI', 'MMCCCXII', 'MMCCCXIII', 'MMCCCXIV', 'MMCCCXV', 'MMCCCXVI', 'MMCCCXVII', 'MMCCCXVIII', 'MMCCCXIX', 'MMCCCXX', 'MMCCCXXI', 'MMCCCXXII', 'MMCCCXXIII', 'MMCCCXXIV', 'MMCCCXXV', 'MMCCCXXVI', 'MMCCCXXVII', 'MMCCCXXVIII', 'MMCCCXXIX', 'MMCCCXXX', 'MMCCCXXXI', 'MMCCCXXXII', 'MMCCCXXXIII', 'MMCCCXXXIV', 'MMCCCXXXV', 'MMCCCXXXVI', 'MMCCCXXXVII', 'MMCCCXXXVIII', 'MMCCCXXXIX', 'MMCCCXL', 'MMCCCXLI', 'MMCCCXLII', 'MMCCCXLIII', 'MMCCCXLIV', 'MMCCCXLV', 'MMCCCXLVI', 'MMCCCXLVII', 'MMCCCXLVIII', 'MMCCCXLIX', 'MMCCCL', 'MMCCCLI', 'MMCCCLII', 'MMCCCLIII', 'MMCCCLIV', 'MMCCCLV', 'MMCCCLVI', 'MMCCCLVII', 'MMCCCLVIII', 'MMCCCLIX', 'MMCCCLX', 'MMCCCLXI', 'MMCCCLXII', 'MMCCCLXIII', 'MMCCCLXIV', 'MMCCCLXV', 'MMCCCLXVI', 'MMCCCLXVII', 'MMCCCLXVIII', 'MMCCCLXIX', 'MMCCCLXX', 'MMCCCLXXI', 'MMCCCLXXII', 'MMCCCLXXIII', 'MMCCCLXXIV', 'MMCCCLXXV', 'MMCCCLXXVI', 'MMCCCLXXVII', 'MMCCCLXXVIII', 'MMCCCLXXIX', 'MMCCCLXXX', 'MMCCCLXXXI', 'MMCCCLXXXII', 'MMCCCLXXXIII', 'MMCCCLXXXIV', 'MMCCCLXXXV', 'MMCCCLXXXVI', 'MMCCCLXXXVII', 'MMCCCLXXXVIII', 'MMCCCLXXXIX', 'MMCCCXC', 'MMCCCXCI', 'MMCCCXCII', 'MMCCCXCIII', 'MMCCCXCIV', 'MMCCCXCV', 'MMCCCXCVI', 'MMCCCXCVII', 'MMCCCXCVIII', 'MMCCCXCIX', 'MMCD', 'MMCDI', 'MMCDII', 'MMCDIII', 'MMCDIV', 'MMCDV', 'MMCDVI', 'MMCDVII', 'MMCDVIII', 'MMCDIX', 'MMCDX', 'MMCDXI', 'MMCDXII', 'MMCDXIII', 'MMCDXIV', 'MMCDXV', 'MMCDXVI', 'MMCDXVII', 'MMCDXVIII', 'MMCDXIX', 'MMCDXX', 'MMCDXXI', 'MMCDXXII', 'MMCDXXIII', 'MMCDXXIV', 'MMCDXXV', 'MMCDXXVI', 'MMCDXXVII', 'MMCDXXVIII', 'MMCDXXIX', 'MMCDXXX', 'MMCDXXXI', 'MMCDXXXII', 'MMCDXXXIII', 'MMCDXXXIV', 'MMCDXXXV', 'MMCDXXXVI', 'MMCDXXXVII', 'MMCDXXXVIII', 'MMCDXXXIX', 'MMCDXL', 'MMCDXLI', 'MMCDXLII', 'MMCDXLIII', 'MMCDXLIV', 'MMCDXLV', 'MMCDXLVI', 'MMCDXLVII', 'MMCDXLVIII', 'MMCDXLIX', 'MMCDL', 'MMCDLI', 'MMCDLII', 'MMCDLIII', 'MMCDLIV', 'MMCDLV', 'MMCDLVI', 'MMCDLVII', 'MMCDLVIII', 'MMCDLIX', 'MMCDLX', 'MMCDLXI', 'MMCDLXII', 'MMCDLXIII', 'MMCDLXIV', 'MMCDLXV', 'MMCDLXVI', 'MMCDLXVII', 'MMCDLXVIII', 'MMCDLXIX', 'MMCDLXX', 'MMCDLXXI', 'MMCDLXXII', 'MMCDLXXIII', 'MMCDLXXIV', 'MMCDLXXV', 'MMCDLXXVI', 'MMCDLXXVII', 'MMCDLXXVIII', 'MMCDLXXIX', 'MMCDLXXX', 'MMCDLXXXI', 'MMCDLXXXII', 'MMCDLXXXIII', 'MMCDLXXXIV', 'MMCDLXXXV', 'MMCDLXXXVI', 'MMCDLXXXVII', 'MMCDLXXXVIII', 'MMCDLXXXIX', 'MMCDXC', 'MMCDXCI', 'MMCDXCII', 'MMCDXCIII', 'MMCDXCIV', 'MMCDXCV', 'MMCDXCVI', 'MMCDXCVII', 'MMCDXCVIII', 'MMCDXCIX', 'MMD', 'MMDI', 'MMDII', 'MMDIII', 'MMDIV', 'MMDV', 'MMDVI', 'MMDVII', 'MMDVIII', 'MMDIX', 'MMDX', 'MMDXI', 'MMDXII', 'MMDXIII', 'MMDXIV', 'MMDXV', 'MMDXVI', 'MMDXVII', 'MMDXVIII', 'MMDXIX', 'MMDXX', 'MMDXXI', 'MMDXXII', 'MMDXXIII', 'MMDXXIV', 'MMDXXV', 'MMDXXVI', 'MMDXXVII', 'MMDXXVIII', 'MMDXXIX', 'MMDXXX', 'MMDXXXI', 'MMDXXXII', 'MMDXXXIII', 'MMDXXXIV', 'MMDXXXV', 'MMDXXXVI', 'MMDXXXVII', 'MMDXXXVIII', 'MMDXXXIX', 'MMDXL', 'MMDXLI', 'MMDXLII', 'MMDXLIII', 'MMDXLIV', 'MMDXLV', 'MMDXLVI', 'MMDXLVII', 'MMDXLVIII', 'MMDXLIX', 'MMDL', 'MMDLI', 'MMDLII', 'MMDLIII', 'MMDLIV', 'MMDLV', 'MMDLVI', 'MMDLVII', 'MMDLVIII', 'MMDLIX', 'MMDLX', 'MMDLXI', 'MMDLXII', 'MMDLXIII', 'MMDLXIV', 'MMDLXV', 'MMDLXVI', 'MMDLXVII', 'MMDLXVIII', 'MMDLXIX', 'MMDLXX', 'MMDLXXI', 'MMDLXXII', 'MMDLXXIII', 'MMDLXXIV', 'MMDLXXV', 'MMDLXXVI', 'MMDLXXVII', 'MMDLXXVIII', 'MMDLXXIX', 'MMDLXXX', 'MMDLXXXI', 'MMDLXXXII', 'MMDLXXXIII', 'MMDLXXXIV', 'MMDLXXXV', 'MMDLXXXVI', 'MMDLXXXVII', 'MMDLXXXVIII', 'MMDLXXXIX', 'MMDXC', 'MMDXCI', 'MMDXCII', 'MMDXCIII', 'MMDXCIV', 'MMDXCV', 'MMDXCVI', 'MMDXCVII', 'MMDXCVIII', 'MMDXCIX', 'MMDC', 'MMDCI', 'MMDCII', 'MMDCIII', 'MMDCIV', 'MMDCV', 'MMDCVI', 'MMDCVII', 'MMDCVIII', 'MMDCIX', 'MMDCX', 'MMDCXI', 'MMDCXII', 'MMDCXIII', 'MMDCXIV', 'MMDCXV', 'MMDCXVI', 'MMDCXVII', 'MMDCXVIII', 'MMDCXIX', 'MMDCXX', 'MMDCXXI', 'MMDCXXII', 'MMDCXXIII', 'MMDCXXIV', 'MMDCXXV', 'MMDCXXVI', 'MMDCXXVII', 'MMDCXXVIII', 'MMDCXXIX', 'MMDCXXX', 'MMDCXXXI', 'MMDCXXXII', 'MMDCXXXIII', 'MMDCXXXIV', 'MMDCXXXV', 'MMDCXXXVI', 'MMDCXXXVII', 'MMDCXXXVIII', 'MMDCXXXIX', 'MMDCXL', 'MMDCXLI', 'MMDCXLII', 'MMDCXLIII', 'MMDCXLIV', 'MMDCXLV', 'MMDCXLVI', 'MMDCXLVII', 'MMDCXLVIII', 'MMDCXLIX', 'MMDCL', 'MMDCLI', 'MMDCLII', 'MMDCLIII', 'MMDCLIV', 'MMDCLV', 'MMDCLVI', 'MMDCLVII', 'MMDCLVIII', 'MMDCLIX', 'MMDCLX', 'MMDCLXI', 'MMDCLXII', 'MMDCLXIII', 'MMDCLXIV', 'MMDCLXV', 'MMDCLXVI', 'MMDCLXVII', 'MMDCLXVIII', 'MMDCLXIX', 'MMDCLXX', 'MMDCLXXI', 'MMDCLXXII', 'MMDCLXXIII', 'MMDCLXXIV', 'MMDCLXXV', 'MMDCLXXVI', 'MMDCLXXVII', 'MMDCLXXVIII', 'MMDCLXXIX', 'MMDCLXXX', 'MMDCLXXXI', 'MMDCLXXXII', 'MMDCLXXXIII', 'MMDCLXXXIV', 'MMDCLXXXV', 'MMDCLXXXVI', 'MMDCLXXXVII', 'MMDCLXXXVIII', 'MMDCLXXXIX', 'MMDCXC', 'MMDCXCI', 'MMDCXCII', 'MMDCXCIII', 'MMDCXCIV', 'MMDCXCV', 'MMDCXCVI', 'MMDCXCVII', 'MMDCXCVIII', 'MMDCXCIX', 'MMDCC', 'MMDCCI', 'MMDCCII', 'MMDCCIII', 'MMDCCIV', 'MMDCCV', 'MMDCCVI', 'MMDCCVII', 'MMDCCVIII', 'MMDCCIX', 'MMDCCX', 'MMDCCXI', 'MMDCCXII', 'MMDCCXIII', 'MMDCCXIV', 'MMDCCXV', 'MMDCCXVI', 'MMDCCXVII', 'MMDCCXVIII', 'MMDCCXIX', 'MMDCCXX', 'MMDCCXXI', 'MMDCCXXII', 'MMDCCXXIII', 'MMDCCXXIV', 'MMDCCXXV', 'MMDCCXXVI', 'MMDCCXXVII', 'MMDCCXXVIII', 'MMDCCXXIX', 'MMDCCXXX', 'MMDCCXXXI', 'MMDCCXXXII', 'MMDCCXXXIII', 'MMDCCXXXIV', 'MMDCCXXXV', 'MMDCCXXXVI', 'MMDCCXXXVII', 'MMDCCXXXVIII', 'MMDCCXXXIX', 'MMDCCXL', 'MMDCCXLI', 'MMDCCXLII', 'MMDCCXLIII', 'MMDCCXLIV', 'MMDCCXLV', 'MMDCCXLVI', 'MMDCCXLVII', 'MMDCCXLVIII', 'MMDCCXLIX', 'MMDCCL', 'MMDCCLI', 'MMDCCLII', 'MMDCCLIII', 'MMDCCLIV', 'MMDCCLV', 'MMDCCLVI', 'MMDCCLVII', 'MMDCCLVIII', 'MMDCCLIX', 'MMDCCLX', 'MMDCCLXI', 'MMDCCLXII', 'MMDCCLXIII', 'MMDCCLXIV', 'MMDCCLXV', 'MMDCCLXVI', 'MMDCCLXVII', 'MMDCCLXVIII', 'MMDCCLXIX', 'MMDCCLXX', 'MMDCCLXXI', 'MMDCCLXXII', 'MMDCCLXXIII', 'MMDCCLXXIV', 'MMDCCLXXV', 'MMDCCLXXVI', 'MMDCCLXXVII', 'MMDCCLXXVIII', 'MMDCCLXXIX', 'MMDCCLXXX', 'MMDCCLXXXI', 'MMDCCLXXXII', 'MMDCCLXXXIII', 'MMDCCLXXXIV', 'MMDCCLXXXV', 'MMDCCLXXXVI', 'MMDCCLXXXVII', 'MMDCCLXXXVIII', 'MMDCCLXXXIX', 'MMDCCXC', 'MMDCCXCI', 'MMDCCXCII', 'MMDCCXCIII', 'MMDCCXCIV', 'MMDCCXCV', 'MMDCCXCVI', 'MMDCCXCVII', 'MMDCCXCVIII', 'MMDCCXCIX', 'MMDCCC', 'MMDCCCI', 'MMDCCCII', 'MMDCCCIII', 'MMDCCCIV', 'MMDCCCV', 'MMDCCCVI', 'MMDCCCVII', 'MMDCCCVIII', 'MMDCCCIX', 'MMDCCCX', 'MMDCCCXI', 'MMDCCCXII', 'MMDCCCXIII', 'MMDCCCXIV', 'MMDCCCXV', 'MMDCCCXVI', 'MMDCCCXVII', 'MMDCCCXVIII', 'MMDCCCXIX', 'MMDCCCXX', 'MMDCCCXXI', 'MMDCCCXXII', 'MMDCCCXXIII', 'MMDCCCXXIV', 'MMDCCCXXV', 'MMDCCCXXVI', 'MMDCCCXXVII', 'MMDCCCXXVIII', 'MMDCCCXXIX', 'MMDCCCXXX', 'MMDCCCXXXI', 'MMDCCCXXXII', 'MMDCCCXXXIII', 'MMDCCCXXXIV', 'MMDCCCXXXV', 'MMDCCCXXXVI', 'MMDCCCXXXVII', 'MMDCCCXXXVIII', 'MMDCCCXXXIX', 'MMDCCCXL', 'MMDCCCXLI', 'MMDCCCXLII', 'MMDCCCXLIII', 'MMDCCCXLIV', 'MMDCCCXLV', 'MMDCCCXLVI', 'MMDCCCXLVII', 'MMDCCCXLVIII', 'MMDCCCXLIX', 'MMDCCCL', 'MMDCCCLI', 'MMDCCCLII', 'MMDCCCLIII', 'MMDCCCLIV', 'MMDCCCLV', 'MMDCCCLVI', 'MMDCCCLVII', 'MMDCCCLVIII', 'MMDCCCLIX', 'MMDCCCLX', 'MMDCCCLXI', 'MMDCCCLXII', 'MMDCCCLXIII', 'MMDCCCLXIV', 'MMDCCCLXV', 'MMDCCCLXVI', 'MMDCCCLXVII', 'MMDCCCLXVIII', 'MMDCCCLXIX', 'MMDCCCLXX', 'MMDCCCLXXI', 'MMDCCCLXXII', 'MMDCCCLXXIII', 'MMDCCCLXXIV', 'MMDCCCLXXV', 'MMDCCCLXXVI', 'MMDCCCLXXVII', 'MMDCCCLXXVIII', 'MMDCCCLXXIX', 'MMDCCCLXXX', 'MMDCCCLXXXI', 'MMDCCCLXXXII', 'MMDCCCLXXXIII', 'MMDCCCLXXXIV', 'MMDCCCLXXXV', 'MMDCCCLXXXVI', 'MMDCCCLXXXVII', 'MMDCCCLXXXVIII', 'MMDCCCLXXXIX', 'MMDCCCXC', 'MMDCCCXCI', 'MMDCCCXCII', 'MMDCCCXCIII', 'MMDCCCXCIV', 'MMDCCCXCV', 'MMDCCCXCVI', 'MMDCCCXCVII', 'MMDCCCXCVIII', 'MMDCCCXCIX', 'MMCM', 'MMCMI', 'MMCMII', 'MMCMIII', 'MMCMIV', 'MMCMV', 'MMCMVI', 'MMCMVII', 'MMCMVIII', 'MMCMIX', 'MMCMX', 'MMCMXI', 'MMCMXII', 'MMCMXIII', 'MMCMXIV', 'MMCMXV', 'MMCMXVI', 'MMCMXVII', 'MMCMXVIII', 'MMCMXIX', 'MMCMXX', 'MMCMXXI', 'MMCMXXII', 'MMCMXXIII', 'MMCMXXIV', 'MMCMXXV', 'MMCMXXVI', 'MMCMXXVII', 'MMCMXXVIII', 'MMCMXXIX', 'MMCMXXX', 'MMCMXXXI', 'MMCMXXXII', 'MMCMXXXIII', 'MMCMXXXIV', 'MMCMXXXV', 'MMCMXXXVI', 'MMCMXXXVII', 'MMCMXXXVIII', 'MMCMXXXIX', 'MMCMXL', 'MMCMXLI', 'MMCMXLII', 'MMCMXLIII', 'MMCMXLIV', 'MMCMXLV', 'MMCMXLVI', 'MMCMXLVII', 'MMCMXLVIII', 'MMCMXLIX', 'MMCML', 'MMCMLI', 'MMCMLII', 'MMCMLIII', 'MMCMLIV', 'MMCMLV', 'MMCMLVI', 'MMCMLVII', 'MMCMLVIII', 'MMCMLIX', 'MMCMLX', 'MMCMLXI', 'MMCMLXII', 'MMCMLXIII', 'MMCMLXIV', 'MMCMLXV', 'MMCMLXVI', 'MMCMLXVII', 'MMCMLXVIII', 'MMCMLXIX', 'MMCMLXX', 'MMCMLXXI', 'MMCMLXXII', 'MMCMLXXIII', 'MMCMLXXIV', 'MMCMLXXV', 'MMCMLXXVI', 'MMCMLXXVII', 'MMCMLXXVIII', 'MMCMLXXIX', 'MMCMLXXX', 'MMCMLXXXI', 'MMCMLXXXII', 'MMCMLXXXIII', 'MMCMLXXXIV', 'MMCMLXXXV', 'MMCMLXXXVI', 'MMCMLXXXVII', 'MMCMLXXXVIII', 'MMCMLXXXIX', 'MMCMXC', 'MMCMXCI', 'MMCMXCII', 'MMCMXCIII', 'MMCMXCIV', 'MMCMXCV', 'MMCMXCVI', 'MMCMXCVII', 'MMCMXCVIII', 'MMCMXCIX', 'MMM', 'MMMI', 'MMMII', 'MMMIII', 'MMMIV', 'MMMV', 'MMMVI', 'MMMVII', 'MMMVIII', 'MMMIX', 'MMMX', 'MMMXI', 'MMMXII', 'MMMXIII', 'MMMXIV', 'MMMXV', 'MMMXVI', 'MMMXVII', 'MMMXVIII', 'MMMXIX', 'MMMXX', 'MMMXXI', 'MMMXXII', 'MMMXXIII', 'MMMXXIV', 'MMMXXV', 'MMMXXVI', 'MMMXXVII', 'MMMXXVIII', 'MMMXXIX', 'MMMXXX', 'MMMXXXI', 'MMMXXXII', 'MMMXXXIII', 'MMMXXXIV', 'MMMXXXV', 'MMMXXXVI', 'MMMXXXVII', 'MMMXXXVIII', 'MMMXXXIX', 'MMMXL', 'MMMXLI', 'MMMXLII', 'MMMXLIII', 'MMMXLIV', 'MMMXLV', 'MMMXLVI', 'MMMXLVII', 'MMMXLVIII', 'MMMXLIX', 'MMML', 'MMMLI', 'MMMLII', 'MMMLIII', 'MMMLIV', 'MMMLV', 'MMMLVI', 'MMMLVII', 'MMMLVIII', 'MMMLIX', 'MMMLX', 'MMMLXI', 'MMMLXII', 'MMMLXIII', 'MMMLXIV', 'MMMLXV', 'MMMLXVI', 'MMMLXVII', 'MMMLXVIII', 'MMMLXIX', 'MMMLXX', 'MMMLXXI', 'MMMLXXII', 'MMMLXXIII', 'MMMLXXIV', 'MMMLXXV', 'MMMLXXVI', 'MMMLXXVII', 'MMMLXXVIII', 'MMMLXXIX', 'MMMLXXX', 'MMMLXXXI', 'MMMLXXXII', 'MMMLXXXIII', 'MMMLXXXIV', 'MMMLXXXV', 'MMMLXXXVI', 'MMMLXXXVII', 'MMMLXXXVIII', 'MMMLXXXIX', 'MMMXC', 'MMMXCI', 'MMMXCII', 'MMMXCIII', 'MMMXCIV', 'MMMXCV', 'MMMXCVI', 'MMMXCVII', 'MMMXCVIII', 'MMMXCIX', 'MMMC', 'MMMCI', 'MMMCII', 'MMMCIII', 'MMMCIV', 'MMMCV', 'MMMCVI', 'MMMCVII', 'MMMCVIII', 'MMMCIX', 'MMMCX', 'MMMCXI', 'MMMCXII', 'MMMCXIII', 'MMMCXIV', 'MMMCXV', 'MMMCXVI', 'MMMCXVII', 'MMMCXVIII', 'MMMCXIX', 'MMMCXX', 'MMMCXXI', 'MMMCXXII', 'MMMCXXIII', 'MMMCXXIV', 'MMMCXXV', 'MMMCXXVI', 'MMMCXXVII', 'MMMCXXVIII', 'MMMCXXIX', 'MMMCXXX', 'MMMCXXXI', 'MMMCXXXII', 'MMMCXXXIII', 'MMMCXXXIV', 'MMMCXXXV', 'MMMCXXXVI', 'MMMCXXXVII', 'MMMCXXXVIII', 'MMMCXXXIX', 'MMMCXL', 'MMMCXLI', 'MMMCXLII', 'MMMCXLIII', 'MMMCXLIV', 'MMMCXLV', 'MMMCXLVI', 'MMMCXLVII', 'MMMCXLVIII', 'MMMCXLIX', 'MMMCL', 'MMMCLI', 'MMMCLII', 'MMMCLIII', 'MMMCLIV', 'MMMCLV', 'MMMCLVI', 'MMMCLVII', 'MMMCLVIII', 'MMMCLIX', 'MMMCLX', 'MMMCLXI', 'MMMCLXII', 'MMMCLXIII', 'MMMCLXIV', 'MMMCLXV', 'MMMCLXVI', 'MMMCLXVII', 'MMMCLXVIII', 'MMMCLXIX', 'MMMCLXX', 'MMMCLXXI', 'MMMCLXXII', 'MMMCLXXIII', 'MMMCLXXIV', 'MMMCLXXV', 'MMMCLXXVI', 'MMMCLXXVII', 'MMMCLXXVIII', 'MMMCLXXIX', 'MMMCLXXX', 'MMMCLXXXI', 'MMMCLXXXII', 'MMMCLXXXIII', 'MMMCLXXXIV', 'MMMCLXXXV', 'MMMCLXXXVI', 'MMMCLXXXVII', 'MMMCLXXXVIII', 'MMMCLXXXIX', 'MMMCXC', 'MMMCXCI', 'MMMCXCII', 'MMMCXCIII', 'MMMCXCIV', 'MMMCXCV', 'MMMCXCVI', 'MMMCXCVII', 'MMMCXCVIII', 'MMMCXCIX', 'MMMCC', 'MMMCCI', 'MMMCCII', 'MMMCCIII', 'MMMCCIV', 'MMMCCV', 'MMMCCVI', 'MMMCCVII', 'MMMCCVIII', 'MMMCCIX', 'MMMCCX', 'MMMCCXI', 'MMMCCXII', 'MMMCCXIII', 'MMMCCXIV', 'MMMCCXV', 'MMMCCXVI', 'MMMCCXVII', 'MMMCCXVIII', 'MMMCCXIX', 'MMMCCXX', 'MMMCCXXI', 'MMMCCXXII', 'MMMCCXXIII', 'MMMCCXXIV', 'MMMCCXXV', 'MMMCCXXVI', 'MMMCCXXVII', 'MMMCCXXVIII', 'MMMCCXXIX', 'MMMCCXXX', 'MMMCCXXXI', 'MMMCCXXXII', 'MMMCCXXXIII', 'MMMCCXXXIV', 'MMMCCXXXV', 'MMMCCXXXVI', 'MMMCCXXXVII', 'MMMCCXXXVIII', 'MMMCCXXXIX', 'MMMCCXL', 'MMMCCXLI', 'MMMCCXLII', 'MMMCCXLIII', 'MMMCCXLIV', 'MMMCCXLV', 'MMMCCXLVI', 'MMMCCXLVII', 'MMMCCXLVIII', 'MMMCCXLIX', 'MMMCCL', 'MMMCCLI', 'MMMCCLII', 'MMMCCLIII', 'MMMCCLIV', 'MMMCCLV', 'MMMCCLVI', 'MMMCCLVII', 'MMMCCLVIII', 'MMMCCLIX', 'MMMCCLX', 'MMMCCLXI', 'MMMCCLXII', 'MMMCCLXIII', 'MMMCCLXIV', 'MMMCCLXV', 'MMMCCLXVI', 'MMMCCLXVII', 'MMMCCLXVIII', 'MMMCCLXIX', 'MMMCCLXX', 'MMMCCLXXI', 'MMMCCLXXII', 'MMMCCLXXIII', 'MMMCCLXXIV', 'MMMCCLXXV', 'MMMCCLXXVI', 'MMMCCLXXVII', 'MMMCCLXXVIII', 'MMMCCLXXIX', 'MMMCCLXXX', 'MMMCCLXXXI', 'MMMCCLXXXII', 'MMMCCLXXXIII', 'MMMCCLXXXIV', 'MMMCCLXXXV', 'MMMCCLXXXVI', 'MMMCCLXXXVII', 'MMMCCLXXXVIII', 'MMMCCLXXXIX', 'MMMCCXC', 'MMMCCXCI', 'MMMCCXCII', 'MMMCCXCIII', 'MMMCCXCIV', 'MMMCCXCV', 'MMMCCXCVI', 'MMMCCXCVII', 'MMMCCXCVIII', 'MMMCCXCIX', 'MMMCCC', 'MMMCCCI', 'MMMCCCII', 'MMMCCCIII', 'MMMCCCIV', 'MMMCCCV', 'MMMCCCVI', 'MMMCCCVII', 'MMMCCCVIII', 'MMMCCCIX', 'MMMCCCX', 'MMMCCCXI', 'MMMCCCXII', 'MMMCCCXIII', 'MMMCCCXIV', 'MMMCCCXV', 'MMMCCCXVI', 'MMMCCCXVII', 'MMMCCCXVIII', 'MMMCCCXIX', 'MMMCCCXX', 'MMMCCCXXI', 'MMMCCCXXII', 'MMMCCCXXIII', 'MMMCCCXXIV', 'MMMCCCXXV', 'MMMCCCXXVI', 'MMMCCCXXVII', 'MMMCCCXXVIII', 'MMMCCCXXIX', 'MMMCCCXXX', 'MMMCCCXXXI', 'MMMCCCXXXII', 'MMMCCCXXXIII', 'MMMCCCXXXIV', 'MMMCCCXXXV', 'MMMCCCXXXVI', 'MMMCCCXXXVII', 'MMMCCCXXXVIII', 'MMMCCCXXXIX', 'MMMCCCXL', 'MMMCCCXLI', 'MMMCCCXLII', 'MMMCCCXLIII', 'MMMCCCXLIV', 'MMMCCCXLV', 'MMMCCCXLVI', 'MMMCCCXLVII', 'MMMCCCXLVIII', 'MMMCCCXLIX', 'MMMCCCL', 'MMMCCCLI', 'MMMCCCLII', 'MMMCCCLIII', 'MMMCCCLIV', 'MMMCCCLV', 'MMMCCCLVI', 'MMMCCCLVII', 'MMMCCCLVIII', 'MMMCCCLIX', 'MMMCCCLX', 'MMMCCCLXI', 'MMMCCCLXII', 'MMMCCCLXIII', 'MMMCCCLXIV', 'MMMCCCLXV', 'MMMCCCLXVI', 'MMMCCCLXVII', 'MMMCCCLXVIII', 'MMMCCCLXIX', 'MMMCCCLXX', 'MMMCCCLXXI', 'MMMCCCLXXII', 'MMMCCCLXXIII', 'MMMCCCLXXIV', 'MMMCCCLXXV', 'MMMCCCLXXVI', 'MMMCCCLXXVII', 'MMMCCCLXXVIII', 'MMMCCCLXXIX', 'MMMCCCLXXX', 'MMMCCCLXXXI', 'MMMCCCLXXXII', 'MMMCCCLXXXIII', 'MMMCCCLXXXIV', 'MMMCCCLXXXV', 'MMMCCCLXXXVI', 'MMMCCCLXXXVII', 'MMMCCCLXXXVIII', 'MMMCCCLXXXIX', 'MMMCCCXC', 'MMMCCCXCI', 'MMMCCCXCII', 'MMMCCCXCIII', 'MMMCCCXCIV', 'MMMCCCXCV', 'MMMCCCXCVI', 'MMMCCCXCVII', 'MMMCCCXCVIII', 'MMMCCCXCIX', 'MMMCD', 'MMMCDI', 'MMMCDII', 'MMMCDIII', 'MMMCDIV', 'MMMCDV', 'MMMCDVI', 'MMMCDVII', 'MMMCDVIII', 'MMMCDIX', 'MMMCDX', 'MMMCDXI', 'MMMCDXII', 'MMMCDXIII', 'MMMCDXIV', 'MMMCDXV', 'MMMCDXVI', 'MMMCDXVII', 'MMMCDXVIII', 'MMMCDXIX', 'MMMCDXX', 'MMMCDXXI', 'MMMCDXXII', 'MMMCDXXIII', 'MMMCDXXIV', 'MMMCDXXV', 'MMMCDXXVI', 'MMMCDXXVII', 'MMMCDXXVIII', 'MMMCDXXIX', 'MMMCDXXX', 'MMMCDXXXI', 'MMMCDXXXII', 'MMMCDXXXIII', 'MMMCDXXXIV', 'MMMCDXXXV', 'MMMCDXXXVI', 'MMMCDXXXVII', 'MMMCDXXXVIII', 'MMMCDXXXIX', 'MMMCDXL', 'MMMCDXLI', 'MMMCDXLII', 'MMMCDXLIII', 'MMMCDXLIV', 'MMMCDXLV', 'MMMCDXLVI', 'MMMCDXLVII', 'MMMCDXLVIII', 'MMMCDXLIX', 'MMMCDL', 'MMMCDLI', 'MMMCDLII', 'MMMCDLIII', 'MMMCDLIV', 'MMMCDLV', 'MMMCDLVI', 'MMMCDLVII', 'MMMCDLVIII', 'MMMCDLIX', 'MMMCDLX', 'MMMCDLXI', 'MMMCDLXII', 'MMMCDLXIII', 'MMMCDLXIV', 'MMMCDLXV', 'MMMCDLXVI', 'MMMCDLXVII', 'MMMCDLXVIII', 'MMMCDLXIX', 'MMMCDLXX', 'MMMCDLXXI', 'MMMCDLXXII', 'MMMCDLXXIII', 'MMMCDLXXIV', 'MMMCDLXXV', 'MMMCDLXXVI', 'MMMCDLXXVII', 'MMMCDLXXVIII', 'MMMCDLXXIX', 'MMMCDLXXX', 'MMMCDLXXXI', 'MMMCDLXXXII', 'MMMCDLXXXIII', 'MMMCDLXXXIV', 'MMMCDLXXXV', 'MMMCDLXXXVI', 'MMMCDLXXXVII', 'MMMCDLXXXVIII', 'MMMCDLXXXIX', 'MMMCDXC', 'MMMCDXCI', 'MMMCDXCII', 'MMMCDXCIII', 'MMMCDXCIV', 'MMMCDXCV', 'MMMCDXCVI', 'MMMCDXCVII', 'MMMCDXCVIII', 'MMMCDXCIX', 'MMMD', 'MMMDI', 'MMMDII', 'MMMDIII', 'MMMDIV', 'MMMDV', 'MMMDVI', 'MMMDVII', 'MMMDVIII', 'MMMDIX', 'MMMDX', 'MMMDXI', 'MMMDXII', 'MMMDXIII', 'MMMDXIV', 'MMMDXV', 'MMMDXVI', 'MMMDXVII', 'MMMDXVIII', 'MMMDXIX', 'MMMDXX', 'MMMDXXI', 'MMMDXXII', 'MMMDXXIII', 'MMMDXXIV', 'MMMDXXV', 'MMMDXXVI', 'MMMDXXVII', 'MMMDXXVIII', 'MMMDXXIX', 'MMMDXXX', 'MMMDXXXI', 'MMMDXXXII', 'MMMDXXXIII', 'MMMDXXXIV', 'MMMDXXXV', 'MMMDXXXVI', 'MMMDXXXVII', 'MMMDXXXVIII', 'MMMDXXXIX', 'MMMDXL', 'MMMDXLI', 'MMMDXLII', 'MMMDXLIII', 'MMMDXLIV', 'MMMDXLV', 'MMMDXLVI', 'MMMDXLVII', 'MMMDXLVIII', 'MMMDXLIX', 'MMMDL', 'MMMDLI', 'MMMDLII', 'MMMDLIII', 'MMMDLIV', 'MMMDLV', 'MMMDLVI', 'MMMDLVII', 'MMMDLVIII', 'MMMDLIX', 'MMMDLX', 'MMMDLXI', 'MMMDLXII', 'MMMDLXIII', 'MMMDLXIV', 'MMMDLXV', 'MMMDLXVI', 'MMMDLXVII', 'MMMDLXVIII', 'MMMDLXIX', 'MMMDLXX', 'MMMDLXXI', 'MMMDLXXII', 'MMMDLXXIII', 'MMMDLXXIV', 'MMMDLXXV', 'MMMDLXXVI', 'MMMDLXXVII', 'MMMDLXXVIII', 'MMMDLXXIX', 'MMMDLXXX', 'MMMDLXXXI', 'MMMDLXXXII', 'MMMDLXXXIII', 'MMMDLXXXIV', 'MMMDLXXXV', 'MMMDLXXXVI', 'MMMDLXXXVII', 'MMMDLXXXVIII', 'MMMDLXXXIX', 'MMMDXC', 'MMMDXCI', 'MMMDXCII', 'MMMDXCIII', 'MMMDXCIV', 'MMMDXCV', 'MMMDXCVI', 'MMMDXCVII', 'MMMDXCVIII', 'MMMDXCIX', 'MMMDC', 'MMMDCI', 'MMMDCII', 'MMMDCIII', 'MMMDCIV', 'MMMDCV', 'MMMDCVI', 'MMMDCVII', 'MMMDCVIII', 'MMMDCIX', 'MMMDCX', 'MMMDCXI', 'MMMDCXII', 'MMMDCXIII', 'MMMDCXIV', 'MMMDCXV', 'MMMDCXVI', 'MMMDCXVII', 'MMMDCXVIII', 'MMMDCXIX', 'MMMDCXX', 'MMMDCXXI', 'MMMDCXXII', 'MMMDCXXIII', 'MMMDCXXIV', 'MMMDCXXV', 'MMMDCXXVI', 'MMMDCXXVII', 'MMMDCXXVIII', 'MMMDCXXIX', 'MMMDCXXX', 'MMMDCXXXI', 'MMMDCXXXII', 'MMMDCXXXIII', 'MMMDCXXXIV', 'MMMDCXXXV', 'MMMDCXXXVI', 'MMMDCXXXVII', 'MMMDCXXXVIII', 'MMMDCXXXIX', 'MMMDCXL', 'MMMDCXLI', 'MMMDCXLII', 'MMMDCXLIII', 'MMMDCXLIV', 'MMMDCXLV', 'MMMDCXLVI', 'MMMDCXLVII', 'MMMDCXLVIII', 'MMMDCXLIX', 'MMMDCL', 'MMMDCLI', 'MMMDCLII', 'MMMDCLIII', 'MMMDCLIV', 'MMMDCLV', 'MMMDCLVI', 'MMMDCLVII', 'MMMDCLVIII', 'MMMDCLIX', 'MMMDCLX', 'MMMDCLXI', 'MMMDCLXII', 'MMMDCLXIII', 'MMMDCLXIV', 'MMMDCLXV', 'MMMDCLXVI', 'MMMDCLXVII', 'MMMDCLXVIII', 'MMMDCLXIX', 'MMMDCLXX', 'MMMDCLXXI', 'MMMDCLXXII', 'MMMDCLXXIII', 'MMMDCLXXIV', 'MMMDCLXXV', 'MMMDCLXXVI', 'MMMDCLXXVII', 'MMMDCLXXVIII', 'MMMDCLXXIX', 'MMMDCLXXX', 'MMMDCLXXXI', 'MMMDCLXXXII', 'MMMDCLXXXIII', 'MMMDCLXXXIV', 'MMMDCLXXXV', 'MMMDCLXXXVI', 'MMMDCLXXXVII', 'MMMDCLXXXVIII', 'MMMDCLXXXIX', 'MMMDCXC', 'MMMDCXCI', 'MMMDCXCII', 'MMMDCXCIII', 'MMMDCXCIV', 'MMMDCXCV', 'MMMDCXCVI', 'MMMDCXCVII', 'MMMDCXCVIII', 'MMMDCXCIX', 'MMMDCC', 'MMMDCCI', 'MMMDCCII', 'MMMDCCIII', 'MMMDCCIV', 'MMMDCCV', 'MMMDCCVI', 'MMMDCCVII', 'MMMDCCVIII', 'MMMDCCIX', 'MMMDCCX', 'MMMDCCXI', 'MMMDCCXII', 'MMMDCCXIII', 'MMMDCCXIV', 'MMMDCCXV', 'MMMDCCXVI', 'MMMDCCXVII', 'MMMDCCXVIII', 'MMMDCCXIX', 'MMMDCCXX', 'MMMDCCXXI', 'MMMDCCXXII', 'MMMDCCXXIII', 'MMMDCCXXIV', 'MMMDCCXXV', 'MMMDCCXXVI', 'MMMDCCXXVII', 'MMMDCCXXVIII', 'MMMDCCXXIX', 'MMMDCCXXX', 'MMMDCCXXXI', 'MMMDCCXXXII', 'MMMDCCXXXIII', 'MMMDCCXXXIV', 'MMMDCCXXXV', 'MMMDCCXXXVI', 'MMMDCCXXXVII', 'MMMDCCXXXVIII', 'MMMDCCXXXIX', 'MMMDCCXL', 'MMMDCCXLI', 'MMMDCCXLII', 'MMMDCCXLIII', 'MMMDCCXLIV', 'MMMDCCXLV', 'MMMDCCXLVI', 'MMMDCCXLVII', 'MMMDCCXLVIII', 'MMMDCCXLIX', 'MMMDCCL', 'MMMDCCLI', 'MMMDCCLII', 'MMMDCCLIII', 'MMMDCCLIV', 'MMMDCCLV', 'MMMDCCLVI', 'MMMDCCLVII', 'MMMDCCLVIII', 'MMMDCCLIX', 'MMMDCCLX', 'MMMDCCLXI', 'MMMDCCLXII', 'MMMDCCLXIII', 'MMMDCCLXIV', 'MMMDCCLXV', 'MMMDCCLXVI', 'MMMDCCLXVII', 'MMMDCCLXVIII', 'MMMDCCLXIX', 'MMMDCCLXX', 'MMMDCCLXXI', 'MMMDCCLXXII', 'MMMDCCLXXIII', 'MMMDCCLXXIV', 'MMMDCCLXXV', 'MMMDCCLXXVI', 'MMMDCCLXXVII', 'MMMDCCLXXVIII', 'MMMDCCLXXIX', 'MMMDCCLXXX', 'MMMDCCLXXXI', 'MMMDCCLXXXII', 'MMMDCCLXXXIII', 'MMMDCCLXXXIV', 'MMMDCCLXXXV', 'MMMDCCLXXXVI', 'MMMDCCLXXXVII', 'MMMDCCLXXXVIII', 'MMMDCCLXXXIX', 'MMMDCCXC', 'MMMDCCXCI', 'MMMDCCXCII', 'MMMDCCXCIII', 'MMMDCCXCIV', 'MMMDCCXCV', 'MMMDCCXCVI', 'MMMDCCXCVII', 'MMMDCCXCVIII', 'MMMDCCXCIX', 'MMMDCCC', 'MMMDCCCI', 'MMMDCCCII', 'MMMDCCCIII', 'MMMDCCCIV', 'MMMDCCCV', 'MMMDCCCVI', 'MMMDCCCVII', 'MMMDCCCVIII', 'MMMDCCCIX', 'MMMDCCCX', 'MMMDCCCXI', 'MMMDCCCXII', 'MMMDCCCXIII', 'MMMDCCCXIV', 'MMMDCCCXV', 'MMMDCCCXVI', 'MMMDCCCXVII', 'MMMDCCCXVIII', 'MMMDCCCXIX', 'MMMDCCCXX', 'MMMDCCCXXI', 'MMMDCCCXXII', 'MMMDCCCXXIII', 'MMMDCCCXXIV', 'MMMDCCCXXV', 'MMMDCCCXXVI', 'MMMDCCCXXVII', 'MMMDCCCXXVIII', 'MMMDCCCXXIX', 'MMMDCCCXXX', 'MMMDCCCXXXI', 'MMMDCCCXXXII', 'MMMDCCCXXXIII', 'MMMDCCCXXXIV', 'MMMDCCCXXXV', 'MMMDCCCXXXVI', 'MMMDCCCXXXVII', 'MMMDCCCXXXVIII', 'MMMDCCCXXXIX', 'MMMDCCCXL', 'MMMDCCCXLI', 'MMMDCCCXLII', 'MMMDCCCXLIII', 'MMMDCCCXLIV', 'MMMDCCCXLV', 'MMMDCCCXLVI', 'MMMDCCCXLVII', 'MMMDCCCXLVIII', 'MMMDCCCXLIX', 'MMMDCCCL', 'MMMDCCCLI', 'MMMDCCCLII', 'MMMDCCCLIII', 'MMMDCCCLIV', 'MMMDCCCLV', 'MMMDCCCLVI', 'MMMDCCCLVII', 'MMMDCCCLVIII', 'MMMDCCCLIX', 'MMMDCCCLX', 'MMMDCCCLXI', 'MMMDCCCLXII', 'MMMDCCCLXIII', 'MMMDCCCLXIV', 'MMMDCCCLXV', 'MMMDCCCLXVI', 'MMMDCCCLXVII', 'MMMDCCCLXVIII', 'MMMDCCCLXIX', 'MMMDCCCLXX', 'MMMDCCCLXXI', 'MMMDCCCLXXII', 'MMMDCCCLXXIII', 'MMMDCCCLXXIV', 'MMMDCCCLXXV', 'MMMDCCCLXXVI', 'MMMDCCCLXXVII', 'MMMDCCCLXXVIII', 'MMMDCCCLXXIX', 'MMMDCCCLXXX', 'MMMDCCCLXXXI', 'MMMDCCCLXXXII', 'MMMDCCCLXXXIII', 'MMMDCCCLXXXIV', 'MMMDCCCLXXXV', 'MMMDCCCLXXXVI', 'MMMDCCCLXXXVII', 'MMMDCCCLXXXVIII', 'MMMDCCCLXXXIX', 'MMMDCCCXC', 'MMMDCCCXCI', 'MMMDCCCXCII', 'MMMDCCCXCIII', 'MMMDCCCXCIV', 'MMMDCCCXCV', 'MMMDCCCXCVI', 'MMMDCCCXCVII', 'MMMDCCCXCVIII', 'MMMDCCCXCIX', 'MMMCM', 'MMMCMI', 'MMMCMII', 'MMMCMIII', 'MMMCMIV', 'MMMCMV', 'MMMCMVI', 'MMMCMVII', 'MMMCMVIII', 'MMMCMIX', 'MMMCMX', 'MMMCMXI', 'MMMCMXII', 'MMMCMXIII', 'MMMCMXIV', 'MMMCMXV', 'MMMCMXVI', 'MMMCMXVII', 'MMMCMXVIII', 'MMMCMXIX', 'MMMCMXX', 'MMMCMXXI', 'MMMCMXXII', 'MMMCMXXIII', 'MMMCMXXIV', 'MMMCMXXV', 'MMMCMXXVI', 'MMMCMXXVII', 'MMMCMXXVIII', 'MMMCMXXIX', 'MMMCMXXX', 'MMMCMXXXI', 'MMMCMXXXII', 'MMMCMXXXIII', 'MMMCMXXXIV', 'MMMCMXXXV', 'MMMCMXXXVI', 'MMMCMXXXVII', 'MMMCMXXXVIII', 'MMMCMXXXIX', 'MMMCMXL', 'MMMCMXLI', 'MMMCMXLII', 'MMMCMXLIII', 'MMMCMXLIV', 'MMMCMXLV', 'MMMCMXLVI', 'MMMCMXLVII', 'MMMCMXLVIII', 'MMMCMXLIX', 'MMMCML', 'MMMCMLI', 'MMMCMLII', 'MMMCMLIII', 'MMMCMLIV', 'MMMCMLV', 'MMMCMLVI', 'MMMCMLVII', 'MMMCMLVIII', 'MMMCMLIX', 'MMMCMLX', 'MMMCMLXI', 'MMMCMLXII', 'MMMCMLXIII', 'MMMCMLXIV', 'MMMCMLXV', 'MMMCMLXVI', 'MMMCMLXVII', 'MMMCMLXVIII', 'MMMCMLXIX', 'MMMCMLXX', 'MMMCMLXXI', 'MMMCMLXXII', 'MMMCMLXXIII', 'MMMCMLXXIV', 'MMMCMLXXV', 'MMMCMLXXVI', 'MMMCMLXXVII', 'MMMCMLXXVIII', 'MMMCMLXXIX', 'MMMCMLXXX', 'MMMCMLXXXI', 'MMMCMLXXXII', 'MMMCMLXXXIII', 'MMMCMLXXXIV', 'MMMCMLXXXV', 'MMMCMLXXXVI', 'MMMCMLXXXVII', 'MMMCMLXXXVIII', 'MMMCMLXXXIX', 'MMMCMXC', 'MMMCMXCI', 'MMMCMXCII', 'MMMCMXCIII', 'MMMCMXCIV', 'MMMCMXCV', 'MMMCMXCVI', 'MMMCMXCVII', 'MMMCMXCVIII', 'MMMCMXCIX', ] +const mode1 = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI', 'XII', 'XIII', 'XIV', 'XV', 'XVI', 'XVII', 'XVIII', 'XIX', 'XX', 'XXI', 'XXII', 'XXIII', 'XXIV', 'XXV', 'XXVI', 'XXVII', 'XXVIII', 'XXIX', 'XXX', 'XXXI', 'XXXII', 'XXXIII', 'XXXIV', 'XXXV', 'XXXVI', 'XXXVII', 'XXXVIII', 'XXXIX', 'XL', 'XLI', 'XLII', 'XLIII', 'XLIV', 'VL', 'VLI', 'VLII', 'VLIII', 'VLIV', 'L', 'LI', 'LII', 'LIII', 'LIV', 'LV', 'LVI', 'LVII', 'LVIII', 'LIX', 'LX', 'LXI', 'LXII', 'LXIII', 'LXIV', 'LXV', 'LXVI', 'LXVII', 'LXVIII', 'LXIX', 'LXX', 'LXXI', 'LXXII', 'LXXIII', 'LXXIV', 'LXXV', 'LXXVI', 'LXXVII', 'LXXVIII', 'LXXIX', 'LXXX', 'LXXXI', 'LXXXII', 'LXXXIII', 'LXXXIV', 'LXXXV', 'LXXXVI', 'LXXXVII', 'LXXXVIII', 'LXXXIX', 'XC', 'XCI', 'XCII', 'XCIII', 'XCIV', 'VC', 'VCI', 'VCII', 'VCIII', 'VCIV', 'C', 'CI', 'CII', 'CIII', 'CIV', 'CV', 'CVI', 'CVII', 'CVIII', 'CIX', 'CX', 'CXI', 'CXII', 'CXIII', 'CXIV', 'CXV', 'CXVI', 'CXVII', 'CXVIII', 'CXIX', 'CXX', 'CXXI', 'CXXII', 'CXXIII', 'CXXIV', 'CXXV', 'CXXVI', 'CXXVII', 'CXXVIII', 'CXXIX', 'CXXX', 'CXXXI', 'CXXXII', 'CXXXIII', 'CXXXIV', 'CXXXV', 'CXXXVI', 'CXXXVII', 'CXXXVIII', 'CXXXIX', 'CXL', 'CXLI', 'CXLII', 'CXLIII', 'CXLIV', 'CVL', 'CVLI', 'CVLII', 'CVLIII', 'CVLIV', 'CL', 'CLI', 'CLII', 'CLIII', 'CLIV', 'CLV', 'CLVI', 'CLVII', 'CLVIII', 'CLIX', 'CLX', 'CLXI', 'CLXII', 'CLXIII', 'CLXIV', 'CLXV', 'CLXVI', 'CLXVII', 'CLXVIII', 'CLXIX', 'CLXX', 'CLXXI', 'CLXXII', 'CLXXIII', 'CLXXIV', 'CLXXV', 'CLXXVI', 'CLXXVII', 'CLXXVIII', 'CLXXIX', 'CLXXX', 'CLXXXI', 'CLXXXII', 'CLXXXIII', 'CLXXXIV', 'CLXXXV', 'CLXXXVI', 'CLXXXVII', 'CLXXXVIII', 'CLXXXIX', 'CXC', 'CXCI', 'CXCII', 'CXCIII', 'CXCIV', 'CVC', 'CVCI', 'CVCII', 'CVCIII', 'CVCIV', 'CC', 'CCI', 'CCII', 'CCIII', 'CCIV', 'CCV', 'CCVI', 'CCVII', 'CCVIII', 'CCIX', 'CCX', 'CCXI', 'CCXII', 'CCXIII', 'CCXIV', 'CCXV', 'CCXVI', 'CCXVII', 'CCXVIII', 'CCXIX', 'CCXX', 'CCXXI', 'CCXXII', 'CCXXIII', 'CCXXIV', 'CCXXV', 'CCXXVI', 'CCXXVII', 'CCXXVIII', 'CCXXIX', 'CCXXX', 'CCXXXI', 'CCXXXII', 'CCXXXIII', 'CCXXXIV', 'CCXXXV', 'CCXXXVI', 'CCXXXVII', 'CCXXXVIII', 'CCXXXIX', 'CCXL', 'CCXLI', 'CCXLII', 'CCXLIII', 'CCXLIV', 'CCVL', 'CCVLI', 'CCVLII', 'CCVLIII', 'CCVLIV', 'CCL', 'CCLI', 'CCLII', 'CCLIII', 'CCLIV', 'CCLV', 'CCLVI', 'CCLVII', 'CCLVIII', 'CCLIX', 'CCLX', 'CCLXI', 'CCLXII', 'CCLXIII', 'CCLXIV', 'CCLXV', 'CCLXVI', 'CCLXVII', 'CCLXVIII', 'CCLXIX', 'CCLXX', 'CCLXXI', 'CCLXXII', 'CCLXXIII', 'CCLXXIV', 'CCLXXV', 'CCLXXVI', 'CCLXXVII', 'CCLXXVIII', 'CCLXXIX', 'CCLXXX', 'CCLXXXI', 'CCLXXXII', 'CCLXXXIII', 'CCLXXXIV', 'CCLXXXV', 'CCLXXXVI', 'CCLXXXVII', 'CCLXXXVIII', 'CCLXXXIX', 'CCXC', 'CCXCI', 'CCXCII', 'CCXCIII', 'CCXCIV', 'CCVC', 'CCVCI', 'CCVCII', 'CCVCIII', 'CCVCIV', 'CCC', 'CCCI', 'CCCII', 'CCCIII', 'CCCIV', 'CCCV', 'CCCVI', 'CCCVII', 'CCCVIII', 'CCCIX', 'CCCX', 'CCCXI', 'CCCXII', 'CCCXIII', 'CCCXIV', 'CCCXV', 'CCCXVI', 'CCCXVII', 'CCCXVIII', 'CCCXIX', 'CCCXX', 'CCCXXI', 'CCCXXII', 'CCCXXIII', 'CCCXXIV', 'CCCXXV', 'CCCXXVI', 'CCCXXVII', 'CCCXXVIII', 'CCCXXIX', 'CCCXXX', 'CCCXXXI', 'CCCXXXII', 'CCCXXXIII', 'CCCXXXIV', 'CCCXXXV', 'CCCXXXVI', 'CCCXXXVII', 'CCCXXXVIII', 'CCCXXXIX', 'CCCXL', 'CCCXLI', 'CCCXLII', 'CCCXLIII', 'CCCXLIV', 'CCCVL', 'CCCVLI', 'CCCVLII', 'CCCVLIII', 'CCCVLIV', 'CCCL', 'CCCLI', 'CCCLII', 'CCCLIII', 'CCCLIV', 'CCCLV', 'CCCLVI', 'CCCLVII', 'CCCLVIII', 'CCCLIX', 'CCCLX', 'CCCLXI', 'CCCLXII', 'CCCLXIII', 'CCCLXIV', 'CCCLXV', 'CCCLXVI', 'CCCLXVII', 'CCCLXVIII', 'CCCLXIX', 'CCCLXX', 'CCCLXXI', 'CCCLXXII', 'CCCLXXIII', 'CCCLXXIV', 'CCCLXXV', 'CCCLXXVI', 'CCCLXXVII', 'CCCLXXVIII', 'CCCLXXIX', 'CCCLXXX', 'CCCLXXXI', 'CCCLXXXII', 'CCCLXXXIII', 'CCCLXXXIV', 'CCCLXXXV', 'CCCLXXXVI', 'CCCLXXXVII', 'CCCLXXXVIII', 'CCCLXXXIX', 'CCCXC', 'CCCXCI', 'CCCXCII', 'CCCXCIII', 'CCCXCIV', 'CCCVC', 'CCCVCI', 'CCCVCII', 'CCCVCIII', 'CCCVCIV', 'CD', 'CDI', 'CDII', 'CDIII', 'CDIV', 'CDV', 'CDVI', 'CDVII', 'CDVIII', 'CDIX', 'CDX', 'CDXI', 'CDXII', 'CDXIII', 'CDXIV', 'CDXV', 'CDXVI', 'CDXVII', 'CDXVIII', 'CDXIX', 'CDXX', 'CDXXI', 'CDXXII', 'CDXXIII', 'CDXXIV', 'CDXXV', 'CDXXVI', 'CDXXVII', 'CDXXVIII', 'CDXXIX', 'CDXXX', 'CDXXXI', 'CDXXXII', 'CDXXXIII', 'CDXXXIV', 'CDXXXV', 'CDXXXVI', 'CDXXXVII', 'CDXXXVIII', 'CDXXXIX', 'CDXL', 'CDXLI', 'CDXLII', 'CDXLIII', 'CDXLIV', 'CDVL', 'CDVLI', 'CDVLII', 'CDVLIII', 'CDVLIV', 'LD', 'LDI', 'LDII', 'LDIII', 'LDIV', 'LDV', 'LDVI', 'LDVII', 'LDVIII', 'LDIX', 'LDX', 'LDXI', 'LDXII', 'LDXIII', 'LDXIV', 'LDXV', 'LDXVI', 'LDXVII', 'LDXVIII', 'LDXIX', 'LDXX', 'LDXXI', 'LDXXII', 'LDXXIII', 'LDXXIV', 'LDXXV', 'LDXXVI', 'LDXXVII', 'LDXXVIII', 'LDXXIX', 'LDXXX', 'LDXXXI', 'LDXXXII', 'LDXXXIII', 'LDXXXIV', 'LDXXXV', 'LDXXXVI', 'LDXXXVII', 'LDXXXVIII', 'LDXXXIX', 'LDXL', 'LDXLI', 'LDXLII', 'LDXLIII', 'LDXLIV', 'LDVL', 'LDVLI', 'LDVLII', 'LDVLIII', 'LDVLIV', 'D', 'DI', 'DII', 'DIII', 'DIV', 'DV', 'DVI', 'DVII', 'DVIII', 'DIX', 'DX', 'DXI', 'DXII', 'DXIII', 'DXIV', 'DXV', 'DXVI', 'DXVII', 'DXVIII', 'DXIX', 'DXX', 'DXXI', 'DXXII', 'DXXIII', 'DXXIV', 'DXXV', 'DXXVI', 'DXXVII', 'DXXVIII', 'DXXIX', 'DXXX', 'DXXXI', 'DXXXII', 'DXXXIII', 'DXXXIV', 'DXXXV', 'DXXXVI', 'DXXXVII', 'DXXXVIII', 'DXXXIX', 'DXL', 'DXLI', 'DXLII', 'DXLIII', 'DXLIV', 'DVL', 'DVLI', 'DVLII', 'DVLIII', 'DVLIV', 'DL', 'DLI', 'DLII', 'DLIII', 'DLIV', 'DLV', 'DLVI', 'DLVII', 'DLVIII', 'DLIX', 'DLX', 'DLXI', 'DLXII', 'DLXIII', 'DLXIV', 'DLXV', 'DLXVI', 'DLXVII', 'DLXVIII', 'DLXIX', 'DLXX', 'DLXXI', 'DLXXII', 'DLXXIII', 'DLXXIV', 'DLXXV', 'DLXXVI', 'DLXXVII', 'DLXXVIII', 'DLXXIX', 'DLXXX', 'DLXXXI', 'DLXXXII', 'DLXXXIII', 'DLXXXIV', 'DLXXXV', 'DLXXXVI', 'DLXXXVII', 'DLXXXVIII', 'DLXXXIX', 'DXC', 'DXCI', 'DXCII', 'DXCIII', 'DXCIV', 'DVC', 'DVCI', 'DVCII', 'DVCIII', 'DVCIV', 'DC', 'DCI', 'DCII', 'DCIII', 'DCIV', 'DCV', 'DCVI', 'DCVII', 'DCVIII', 'DCIX', 'DCX', 'DCXI', 'DCXII', 'DCXIII', 'DCXIV', 'DCXV', 'DCXVI', 'DCXVII', 'DCXVIII', 'DCXIX', 'DCXX', 'DCXXI', 'DCXXII', 'DCXXIII', 'DCXXIV', 'DCXXV', 'DCXXVI', 'DCXXVII', 'DCXXVIII', 'DCXXIX', 'DCXXX', 'DCXXXI', 'DCXXXII', 'DCXXXIII', 'DCXXXIV', 'DCXXXV', 'DCXXXVI', 'DCXXXVII', 'DCXXXVIII', 'DCXXXIX', 'DCXL', 'DCXLI', 'DCXLII', 'DCXLIII', 'DCXLIV', 'DCVL', 'DCVLI', 'DCVLII', 'DCVLIII', 'DCVLIV', 'DCL', 'DCLI', 'DCLII', 'DCLIII', 'DCLIV', 'DCLV', 'DCLVI', 'DCLVII', 'DCLVIII', 'DCLIX', 'DCLX', 'DCLXI', 'DCLXII', 'DCLXIII', 'DCLXIV', 'DCLXV', 'DCLXVI', 'DCLXVII', 'DCLXVIII', 'DCLXIX', 'DCLXX', 'DCLXXI', 'DCLXXII', 'DCLXXIII', 'DCLXXIV', 'DCLXXV', 'DCLXXVI', 'DCLXXVII', 'DCLXXVIII', 'DCLXXIX', 'DCLXXX', 'DCLXXXI', 'DCLXXXII', 'DCLXXXIII', 'DCLXXXIV', 'DCLXXXV', 'DCLXXXVI', 'DCLXXXVII', 'DCLXXXVIII', 'DCLXXXIX', 'DCXC', 'DCXCI', 'DCXCII', 'DCXCIII', 'DCXCIV', 'DCVC', 'DCVCI', 'DCVCII', 'DCVCIII', 'DCVCIV', 'DCC', 'DCCI', 'DCCII', 'DCCIII', 'DCCIV', 'DCCV', 'DCCVI', 'DCCVII', 'DCCVIII', 'DCCIX', 'DCCX', 'DCCXI', 'DCCXII', 'DCCXIII', 'DCCXIV', 'DCCXV', 'DCCXVI', 'DCCXVII', 'DCCXVIII', 'DCCXIX', 'DCCXX', 'DCCXXI', 'DCCXXII', 'DCCXXIII', 'DCCXXIV', 'DCCXXV', 'DCCXXVI', 'DCCXXVII', 'DCCXXVIII', 'DCCXXIX', 'DCCXXX', 'DCCXXXI', 'DCCXXXII', 'DCCXXXIII', 'DCCXXXIV', 'DCCXXXV', 'DCCXXXVI', 'DCCXXXVII', 'DCCXXXVIII', 'DCCXXXIX', 'DCCXL', 'DCCXLI', 'DCCXLII', 'DCCXLIII', 'DCCXLIV', 'DCCVL', 'DCCVLI', 'DCCVLII', 'DCCVLIII', 'DCCVLIV', 'DCCL', 'DCCLI', 'DCCLII', 'DCCLIII', 'DCCLIV', 'DCCLV', 'DCCLVI', 'DCCLVII', 'DCCLVIII', 'DCCLIX', 'DCCLX', 'DCCLXI', 'DCCLXII', 'DCCLXIII', 'DCCLXIV', 'DCCLXV', 'DCCLXVI', 'DCCLXVII', 'DCCLXVIII', 'DCCLXIX', 'DCCLXX', 'DCCLXXI', 'DCCLXXII', 'DCCLXXIII', 'DCCLXXIV', 'DCCLXXV', 'DCCLXXVI', 'DCCLXXVII', 'DCCLXXVIII', 'DCCLXXIX', 'DCCLXXX', 'DCCLXXXI', 'DCCLXXXII', 'DCCLXXXIII', 'DCCLXXXIV', 'DCCLXXXV', 'DCCLXXXVI', 'DCCLXXXVII', 'DCCLXXXVIII', 'DCCLXXXIX', 'DCCXC', 'DCCXCI', 'DCCXCII', 'DCCXCIII', 'DCCXCIV', 'DCCVC', 'DCCVCI', 'DCCVCII', 'DCCVCIII', 'DCCVCIV', 'DCCC', 'DCCCI', 'DCCCII', 'DCCCIII', 'DCCCIV', 'DCCCV', 'DCCCVI', 'DCCCVII', 'DCCCVIII', 'DCCCIX', 'DCCCX', 'DCCCXI', 'DCCCXII', 'DCCCXIII', 'DCCCXIV', 'DCCCXV', 'DCCCXVI', 'DCCCXVII', 'DCCCXVIII', 'DCCCXIX', 'DCCCXX', 'DCCCXXI', 'DCCCXXII', 'DCCCXXIII', 'DCCCXXIV', 'DCCCXXV', 'DCCCXXVI', 'DCCCXXVII', 'DCCCXXVIII', 'DCCCXXIX', 'DCCCXXX', 'DCCCXXXI', 'DCCCXXXII', 'DCCCXXXIII', 'DCCCXXXIV', 'DCCCXXXV', 'DCCCXXXVI', 'DCCCXXXVII', 'DCCCXXXVIII', 'DCCCXXXIX', 'DCCCXL', 'DCCCXLI', 'DCCCXLII', 'DCCCXLIII', 'DCCCXLIV', 'DCCCVL', 'DCCCVLI', 'DCCCVLII', 'DCCCVLIII', 'DCCCVLIV', 'DCCCL', 'DCCCLI', 'DCCCLII', 'DCCCLIII', 'DCCCLIV', 'DCCCLV', 'DCCCLVI', 'DCCCLVII', 'DCCCLVIII', 'DCCCLIX', 'DCCCLX', 'DCCCLXI', 'DCCCLXII', 'DCCCLXIII', 'DCCCLXIV', 'DCCCLXV', 'DCCCLXVI', 'DCCCLXVII', 'DCCCLXVIII', 'DCCCLXIX', 'DCCCLXX', 'DCCCLXXI', 'DCCCLXXII', 'DCCCLXXIII', 'DCCCLXXIV', 'DCCCLXXV', 'DCCCLXXVI', 'DCCCLXXVII', 'DCCCLXXVIII', 'DCCCLXXIX', 'DCCCLXXX', 'DCCCLXXXI', 'DCCCLXXXII', 'DCCCLXXXIII', 'DCCCLXXXIV', 'DCCCLXXXV', 'DCCCLXXXVI', 'DCCCLXXXVII', 'DCCCLXXXVIII', 'DCCCLXXXIX', 'DCCCXC', 'DCCCXCI', 'DCCCXCII', 'DCCCXCIII', 'DCCCXCIV', 'DCCCVC', 'DCCCVCI', 'DCCCVCII', 'DCCCVCIII', 'DCCCVCIV', 'CM', 'CMI', 'CMII', 'CMIII', 'CMIV', 'CMV', 'CMVI', 'CMVII', 'CMVIII', 'CMIX', 'CMX', 'CMXI', 'CMXII', 'CMXIII', 'CMXIV', 'CMXV', 'CMXVI', 'CMXVII', 'CMXVIII', 'CMXIX', 'CMXX', 'CMXXI', 'CMXXII', 'CMXXIII', 'CMXXIV', 'CMXXV', 'CMXXVI', 'CMXXVII', 'CMXXVIII', 'CMXXIX', 'CMXXX', 'CMXXXI', 'CMXXXII', 'CMXXXIII', 'CMXXXIV', 'CMXXXV', 'CMXXXVI', 'CMXXXVII', 'CMXXXVIII', 'CMXXXIX', 'CMXL', 'CMXLI', 'CMXLII', 'CMXLIII', 'CMXLIV', 'CMVL', 'CMVLI', 'CMVLII', 'CMVLIII', 'CMVLIV', 'LM', 'LMI', 'LMII', 'LMIII', 'LMIV', 'LMV', 'LMVI', 'LMVII', 'LMVIII', 'LMIX', 'LMX', 'LMXI', 'LMXII', 'LMXIII', 'LMXIV', 'LMXV', 'LMXVI', 'LMXVII', 'LMXVIII', 'LMXIX', 'LMXX', 'LMXXI', 'LMXXII', 'LMXXIII', 'LMXXIV', 'LMXXV', 'LMXXVI', 'LMXXVII', 'LMXXVIII', 'LMXXIX', 'LMXXX', 'LMXXXI', 'LMXXXII', 'LMXXXIII', 'LMXXXIV', 'LMXXXV', 'LMXXXVI', 'LMXXXVII', 'LMXXXVIII', 'LMXXXIX', 'LMXL', 'LMXLI', 'LMXLII', 'LMXLIII', 'LMXLIV', 'LMVL', 'LMVLI', 'LMVLII', 'LMVLIII', 'LMVLIV', 'M', 'MI', 'MII', 'MIII', 'MIV', 'MV', 'MVI', 'MVII', 'MVIII', 'MIX', 'MX', 'MXI', 'MXII', 'MXIII', 'MXIV', 'MXV', 'MXVI', 'MXVII', 'MXVIII', 'MXIX', 'MXX', 'MXXI', 'MXXII', 'MXXIII', 'MXXIV', 'MXXV', 'MXXVI', 'MXXVII', 'MXXVIII', 'MXXIX', 'MXXX', 'MXXXI', 'MXXXII', 'MXXXIII', 'MXXXIV', 'MXXXV', 'MXXXVI', 'MXXXVII', 'MXXXVIII', 'MXXXIX', 'MXL', 'MXLI', 'MXLII', 'MXLIII', 'MXLIV', 'MVL', 'MVLI', 'MVLII', 'MVLIII', 'MVLIV', 'ML', 'MLI', 'MLII', 'MLIII', 'MLIV', 'MLV', 'MLVI', 'MLVII', 'MLVIII', 'MLIX', 'MLX', 'MLXI', 'MLXII', 'MLXIII', 'MLXIV', 'MLXV', 'MLXVI', 'MLXVII', 'MLXVIII', 'MLXIX', 'MLXX', 'MLXXI', 'MLXXII', 'MLXXIII', 'MLXXIV', 'MLXXV', 'MLXXVI', 'MLXXVII', 'MLXXVIII', 'MLXXIX', 'MLXXX', 'MLXXXI', 'MLXXXII', 'MLXXXIII', 'MLXXXIV', 'MLXXXV', 'MLXXXVI', 'MLXXXVII', 'MLXXXVIII', 'MLXXXIX', 'MXC', 'MXCI', 'MXCII', 'MXCIII', 'MXCIV', 'MVC', 'MVCI', 'MVCII', 'MVCIII', 'MVCIV', 'MC', 'MCI', 'MCII', 'MCIII', 'MCIV', 'MCV', 'MCVI', 'MCVII', 'MCVIII', 'MCIX', 'MCX', 'MCXI', 'MCXII', 'MCXIII', 'MCXIV', 'MCXV', 'MCXVI', 'MCXVII', 'MCXVIII', 'MCXIX', 'MCXX', 'MCXXI', 'MCXXII', 'MCXXIII', 'MCXXIV', 'MCXXV', 'MCXXVI', 'MCXXVII', 'MCXXVIII', 'MCXXIX', 'MCXXX', 'MCXXXI', 'MCXXXII', 'MCXXXIII', 'MCXXXIV', 'MCXXXV', 'MCXXXVI', 'MCXXXVII', 'MCXXXVIII', 'MCXXXIX', 'MCXL', 'MCXLI', 'MCXLII', 'MCXLIII', 'MCXLIV', 'MCVL', 'MCVLI', 'MCVLII', 'MCVLIII', 'MCVLIV', 'MCL', 'MCLI', 'MCLII', 'MCLIII', 'MCLIV', 'MCLV', 'MCLVI', 'MCLVII', 'MCLVIII', 'MCLIX', 'MCLX', 'MCLXI', 'MCLXII', 'MCLXIII', 'MCLXIV', 'MCLXV', 'MCLXVI', 'MCLXVII', 'MCLXVIII', 'MCLXIX', 'MCLXX', 'MCLXXI', 'MCLXXII', 'MCLXXIII', 'MCLXXIV', 'MCLXXV', 'MCLXXVI', 'MCLXXVII', 'MCLXXVIII', 'MCLXXIX', 'MCLXXX', 'MCLXXXI', 'MCLXXXII', 'MCLXXXIII', 'MCLXXXIV', 'MCLXXXV', 'MCLXXXVI', 'MCLXXXVII', 'MCLXXXVIII', 'MCLXXXIX', 'MCXC', 'MCXCI', 'MCXCII', 'MCXCIII', 'MCXCIV', 'MCVC', 'MCVCI', 'MCVCII', 'MCVCIII', 'MCVCIV', 'MCC', 'MCCI', 'MCCII', 'MCCIII', 'MCCIV', 'MCCV', 'MCCVI', 'MCCVII', 'MCCVIII', 'MCCIX', 'MCCX', 'MCCXI', 'MCCXII', 'MCCXIII', 'MCCXIV', 'MCCXV', 'MCCXVI', 'MCCXVII', 'MCCXVIII', 'MCCXIX', 'MCCXX', 'MCCXXI', 'MCCXXII', 'MCCXXIII', 'MCCXXIV', 'MCCXXV', 'MCCXXVI', 'MCCXXVII', 'MCCXXVIII', 'MCCXXIX', 'MCCXXX', 'MCCXXXI', 'MCCXXXII', 'MCCXXXIII', 'MCCXXXIV', 'MCCXXXV', 'MCCXXXVI', 'MCCXXXVII', 'MCCXXXVIII', 'MCCXXXIX', 'MCCXL', 'MCCXLI', 'MCCXLII', 'MCCXLIII', 'MCCXLIV', 'MCCVL', 'MCCVLI', 'MCCVLII', 'MCCVLIII', 'MCCVLIV', 'MCCL', 'MCCLI', 'MCCLII', 'MCCLIII', 'MCCLIV', 'MCCLV', 'MCCLVI', 'MCCLVII', 'MCCLVIII', 'MCCLIX', 'MCCLX', 'MCCLXI', 'MCCLXII', 'MCCLXIII', 'MCCLXIV', 'MCCLXV', 'MCCLXVI', 'MCCLXVII', 'MCCLXVIII', 'MCCLXIX', 'MCCLXX', 'MCCLXXI', 'MCCLXXII', 'MCCLXXIII', 'MCCLXXIV', 'MCCLXXV', 'MCCLXXVI', 'MCCLXXVII', 'MCCLXXVIII', 'MCCLXXIX', 'MCCLXXX', 'MCCLXXXI', 'MCCLXXXII', 'MCCLXXXIII', 'MCCLXXXIV', 'MCCLXXXV', 'MCCLXXXVI', 'MCCLXXXVII', 'MCCLXXXVIII', 'MCCLXXXIX', 'MCCXC', 'MCCXCI', 'MCCXCII', 'MCCXCIII', 'MCCXCIV', 'MCCVC', 'MCCVCI', 'MCCVCII', 'MCCVCIII', 'MCCVCIV', 'MCCC', 'MCCCI', 'MCCCII', 'MCCCIII', 'MCCCIV', 'MCCCV', 'MCCCVI', 'MCCCVII', 'MCCCVIII', 'MCCCIX', 'MCCCX', 'MCCCXI', 'MCCCXII', 'MCCCXIII', 'MCCCXIV', 'MCCCXV', 'MCCCXVI', 'MCCCXVII', 'MCCCXVIII', 'MCCCXIX', 'MCCCXX', 'MCCCXXI', 'MCCCXXII', 'MCCCXXIII', 'MCCCXXIV', 'MCCCXXV', 'MCCCXXVI', 'MCCCXXVII', 'MCCCXXVIII', 'MCCCXXIX', 'MCCCXXX', 'MCCCXXXI', 'MCCCXXXII', 'MCCCXXXIII', 'MCCCXXXIV', 'MCCCXXXV', 'MCCCXXXVI', 'MCCCXXXVII', 'MCCCXXXVIII', 'MCCCXXXIX', 'MCCCXL', 'MCCCXLI', 'MCCCXLII', 'MCCCXLIII', 'MCCCXLIV', 'MCCCVL', 'MCCCVLI', 'MCCCVLII', 'MCCCVLIII', 'MCCCVLIV', 'MCCCL', 'MCCCLI', 'MCCCLII', 'MCCCLIII', 'MCCCLIV', 'MCCCLV', 'MCCCLVI', 'MCCCLVII', 'MCCCLVIII', 'MCCCLIX', 'MCCCLX', 'MCCCLXI', 'MCCCLXII', 'MCCCLXIII', 'MCCCLXIV', 'MCCCLXV', 'MCCCLXVI', 'MCCCLXVII', 'MCCCLXVIII', 'MCCCLXIX', 'MCCCLXX', 'MCCCLXXI', 'MCCCLXXII', 'MCCCLXXIII', 'MCCCLXXIV', 'MCCCLXXV', 'MCCCLXXVI', 'MCCCLXXVII', 'MCCCLXXVIII', 'MCCCLXXIX', 'MCCCLXXX', 'MCCCLXXXI', 'MCCCLXXXII', 'MCCCLXXXIII', 'MCCCLXXXIV', 'MCCCLXXXV', 'MCCCLXXXVI', 'MCCCLXXXVII', 'MCCCLXXXVIII', 'MCCCLXXXIX', 'MCCCXC', 'MCCCXCI', 'MCCCXCII', 'MCCCXCIII', 'MCCCXCIV', 'MCCCVC', 'MCCCVCI', 'MCCCVCII', 'MCCCVCIII', 'MCCCVCIV', 'MCD', 'MCDI', 'MCDII', 'MCDIII', 'MCDIV', 'MCDV', 'MCDVI', 'MCDVII', 'MCDVIII', 'MCDIX', 'MCDX', 'MCDXI', 'MCDXII', 'MCDXIII', 'MCDXIV', 'MCDXV', 'MCDXVI', 'MCDXVII', 'MCDXVIII', 'MCDXIX', 'MCDXX', 'MCDXXI', 'MCDXXII', 'MCDXXIII', 'MCDXXIV', 'MCDXXV', 'MCDXXVI', 'MCDXXVII', 'MCDXXVIII', 'MCDXXIX', 'MCDXXX', 'MCDXXXI', 'MCDXXXII', 'MCDXXXIII', 'MCDXXXIV', 'MCDXXXV', 'MCDXXXVI', 'MCDXXXVII', 'MCDXXXVIII', 'MCDXXXIX', 'MCDXL', 'MCDXLI', 'MCDXLII', 'MCDXLIII', 'MCDXLIV', 'MCDVL', 'MCDVLI', 'MCDVLII', 'MCDVLIII', 'MCDVLIV', 'MLD', 'MLDI', 'MLDII', 'MLDIII', 'MLDIV', 'MLDV', 'MLDVI', 'MLDVII', 'MLDVIII', 'MLDIX', 'MLDX', 'MLDXI', 'MLDXII', 'MLDXIII', 'MLDXIV', 'MLDXV', 'MLDXVI', 'MLDXVII', 'MLDXVIII', 'MLDXIX', 'MLDXX', 'MLDXXI', 'MLDXXII', 'MLDXXIII', 'MLDXXIV', 'MLDXXV', 'MLDXXVI', 'MLDXXVII', 'MLDXXVIII', 'MLDXXIX', 'MLDXXX', 'MLDXXXI', 'MLDXXXII', 'MLDXXXIII', 'MLDXXXIV', 'MLDXXXV', 'MLDXXXVI', 'MLDXXXVII', 'MLDXXXVIII', 'MLDXXXIX', 'MLDXL', 'MLDXLI', 'MLDXLII', 'MLDXLIII', 'MLDXLIV', 'MLDVL', 'MLDVLI', 'MLDVLII', 'MLDVLIII', 'MLDVLIV', 'MD', 'MDI', 'MDII', 'MDIII', 'MDIV', 'MDV', 'MDVI', 'MDVII', 'MDVIII', 'MDIX', 'MDX', 'MDXI', 'MDXII', 'MDXIII', 'MDXIV', 'MDXV', 'MDXVI', 'MDXVII', 'MDXVIII', 'MDXIX', 'MDXX', 'MDXXI', 'MDXXII', 'MDXXIII', 'MDXXIV', 'MDXXV', 'MDXXVI', 'MDXXVII', 'MDXXVIII', 'MDXXIX', 'MDXXX', 'MDXXXI', 'MDXXXII', 'MDXXXIII', 'MDXXXIV', 'MDXXXV', 'MDXXXVI', 'MDXXXVII', 'MDXXXVIII', 'MDXXXIX', 'MDXL', 'MDXLI', 'MDXLII', 'MDXLIII', 'MDXLIV', 'MDVL', 'MDVLI', 'MDVLII', 'MDVLIII', 'MDVLIV', 'MDL', 'MDLI', 'MDLII', 'MDLIII', 'MDLIV', 'MDLV', 'MDLVI', 'MDLVII', 'MDLVIII', 'MDLIX', 'MDLX', 'MDLXI', 'MDLXII', 'MDLXIII', 'MDLXIV', 'MDLXV', 'MDLXVI', 'MDLXVII', 'MDLXVIII', 'MDLXIX', 'MDLXX', 'MDLXXI', 'MDLXXII', 'MDLXXIII', 'MDLXXIV', 'MDLXXV', 'MDLXXVI', 'MDLXXVII', 'MDLXXVIII', 'MDLXXIX', 'MDLXXX', 'MDLXXXI', 'MDLXXXII', 'MDLXXXIII', 'MDLXXXIV', 'MDLXXXV', 'MDLXXXVI', 'MDLXXXVII', 'MDLXXXVIII', 'MDLXXXIX', 'MDXC', 'MDXCI', 'MDXCII', 'MDXCIII', 'MDXCIV', 'MDVC', 'MDVCI', 'MDVCII', 'MDVCIII', 'MDVCIV', 'MDC', 'MDCI', 'MDCII', 'MDCIII', 'MDCIV', 'MDCV', 'MDCVI', 'MDCVII', 'MDCVIII', 'MDCIX', 'MDCX', 'MDCXI', 'MDCXII', 'MDCXIII', 'MDCXIV', 'MDCXV', 'MDCXVI', 'MDCXVII', 'MDCXVIII', 'MDCXIX', 'MDCXX', 'MDCXXI', 'MDCXXII', 'MDCXXIII', 'MDCXXIV', 'MDCXXV', 'MDCXXVI', 'MDCXXVII', 'MDCXXVIII', 'MDCXXIX', 'MDCXXX', 'MDCXXXI', 'MDCXXXII', 'MDCXXXIII', 'MDCXXXIV', 'MDCXXXV', 'MDCXXXVI', 'MDCXXXVII', 'MDCXXXVIII', 'MDCXXXIX', 'MDCXL', 'MDCXLI', 'MDCXLII', 'MDCXLIII', 'MDCXLIV', 'MDCVL', 'MDCVLI', 'MDCVLII', 'MDCVLIII', 'MDCVLIV', 'MDCL', 'MDCLI', 'MDCLII', 'MDCLIII', 'MDCLIV', 'MDCLV', 'MDCLVI', 'MDCLVII', 'MDCLVIII', 'MDCLIX', 'MDCLX', 'MDCLXI', 'MDCLXII', 'MDCLXIII', 'MDCLXIV', 'MDCLXV', 'MDCLXVI', 'MDCLXVII', 'MDCLXVIII', 'MDCLXIX', 'MDCLXX', 'MDCLXXI', 'MDCLXXII', 'MDCLXXIII', 'MDCLXXIV', 'MDCLXXV', 'MDCLXXVI', 'MDCLXXVII', 'MDCLXXVIII', 'MDCLXXIX', 'MDCLXXX', 'MDCLXXXI', 'MDCLXXXII', 'MDCLXXXIII', 'MDCLXXXIV', 'MDCLXXXV', 'MDCLXXXVI', 'MDCLXXXVII', 'MDCLXXXVIII', 'MDCLXXXIX', 'MDCXC', 'MDCXCI', 'MDCXCII', 'MDCXCIII', 'MDCXCIV', 'MDCVC', 'MDCVCI', 'MDCVCII', 'MDCVCIII', 'MDCVCIV', 'MDCC', 'MDCCI', 'MDCCII', 'MDCCIII', 'MDCCIV', 'MDCCV', 'MDCCVI', 'MDCCVII', 'MDCCVIII', 'MDCCIX', 'MDCCX', 'MDCCXI', 'MDCCXII', 'MDCCXIII', 'MDCCXIV', 'MDCCXV', 'MDCCXVI', 'MDCCXVII', 'MDCCXVIII', 'MDCCXIX', 'MDCCXX', 'MDCCXXI', 'MDCCXXII', 'MDCCXXIII', 'MDCCXXIV', 'MDCCXXV', 'MDCCXXVI', 'MDCCXXVII', 'MDCCXXVIII', 'MDCCXXIX', 'MDCCXXX', 'MDCCXXXI', 'MDCCXXXII', 'MDCCXXXIII', 'MDCCXXXIV', 'MDCCXXXV', 'MDCCXXXVI', 'MDCCXXXVII', 'MDCCXXXVIII', 'MDCCXXXIX', 'MDCCXL', 'MDCCXLI', 'MDCCXLII', 'MDCCXLIII', 'MDCCXLIV', 'MDCCVL', 'MDCCVLI', 'MDCCVLII', 'MDCCVLIII', 'MDCCVLIV', 'MDCCL', 'MDCCLI', 'MDCCLII', 'MDCCLIII', 'MDCCLIV', 'MDCCLV', 'MDCCLVI', 'MDCCLVII', 'MDCCLVIII', 'MDCCLIX', 'MDCCLX', 'MDCCLXI', 'MDCCLXII', 'MDCCLXIII', 'MDCCLXIV', 'MDCCLXV', 'MDCCLXVI', 'MDCCLXVII', 'MDCCLXVIII', 'MDCCLXIX', 'MDCCLXX', 'MDCCLXXI', 'MDCCLXXII', 'MDCCLXXIII', 'MDCCLXXIV', 'MDCCLXXV', 'MDCCLXXVI', 'MDCCLXXVII', 'MDCCLXXVIII', 'MDCCLXXIX', 'MDCCLXXX', 'MDCCLXXXI', 'MDCCLXXXII', 'MDCCLXXXIII', 'MDCCLXXXIV', 'MDCCLXXXV', 'MDCCLXXXVI', 'MDCCLXXXVII', 'MDCCLXXXVIII', 'MDCCLXXXIX', 'MDCCXC', 'MDCCXCI', 'MDCCXCII', 'MDCCXCIII', 'MDCCXCIV', 'MDCCVC', 'MDCCVCI', 'MDCCVCII', 'MDCCVCIII', 'MDCCVCIV', 'MDCCC', 'MDCCCI', 'MDCCCII', 'MDCCCIII', 'MDCCCIV', 'MDCCCV', 'MDCCCVI', 'MDCCCVII', 'MDCCCVIII', 'MDCCCIX', 'MDCCCX', 'MDCCCXI', 'MDCCCXII', 'MDCCCXIII', 'MDCCCXIV', 'MDCCCXV', 'MDCCCXVI', 'MDCCCXVII', 'MDCCCXVIII', 'MDCCCXIX', 'MDCCCXX', 'MDCCCXXI', 'MDCCCXXII', 'MDCCCXXIII', 'MDCCCXXIV', 'MDCCCXXV', 'MDCCCXXVI', 'MDCCCXXVII', 'MDCCCXXVIII', 'MDCCCXXIX', 'MDCCCXXX', 'MDCCCXXXI', 'MDCCCXXXII', 'MDCCCXXXIII', 'MDCCCXXXIV', 'MDCCCXXXV', 'MDCCCXXXVI', 'MDCCCXXXVII', 'MDCCCXXXVIII', 'MDCCCXXXIX', 'MDCCCXL', 'MDCCCXLI', 'MDCCCXLII', 'MDCCCXLIII', 'MDCCCXLIV', 'MDCCCVL', 'MDCCCVLI', 'MDCCCVLII', 'MDCCCVLIII', 'MDCCCVLIV', 'MDCCCL', 'MDCCCLI', 'MDCCCLII', 'MDCCCLIII', 'MDCCCLIV', 'MDCCCLV', 'MDCCCLVI', 'MDCCCLVII', 'MDCCCLVIII', 'MDCCCLIX', 'MDCCCLX', 'MDCCCLXI', 'MDCCCLXII', 'MDCCCLXIII', 'MDCCCLXIV', 'MDCCCLXV', 'MDCCCLXVI', 'MDCCCLXVII', 'MDCCCLXVIII', 'MDCCCLXIX', 'MDCCCLXX', 'MDCCCLXXI', 'MDCCCLXXII', 'MDCCCLXXIII', 'MDCCCLXXIV', 'MDCCCLXXV', 'MDCCCLXXVI', 'MDCCCLXXVII', 'MDCCCLXXVIII', 'MDCCCLXXIX', 'MDCCCLXXX', 'MDCCCLXXXI', 'MDCCCLXXXII', 'MDCCCLXXXIII', 'MDCCCLXXXIV', 'MDCCCLXXXV', 'MDCCCLXXXVI', 'MDCCCLXXXVII', 'MDCCCLXXXVIII', 'MDCCCLXXXIX', 'MDCCCXC', 'MDCCCXCI', 'MDCCCXCII', 'MDCCCXCIII', 'MDCCCXCIV', 'MDCCCVC', 'MDCCCVCI', 'MDCCCVCII', 'MDCCCVCIII', 'MDCCCVCIV', 'MCM', 'MCMI', 'MCMII', 'MCMIII', 'MCMIV', 'MCMV', 'MCMVI', 'MCMVII', 'MCMVIII', 'MCMIX', 'MCMX', 'MCMXI', 'MCMXII', 'MCMXIII', 'MCMXIV', 'MCMXV', 'MCMXVI', 'MCMXVII', 'MCMXVIII', 'MCMXIX', 'MCMXX', 'MCMXXI', 'MCMXXII', 'MCMXXIII', 'MCMXXIV', 'MCMXXV', 'MCMXXVI', 'MCMXXVII', 'MCMXXVIII', 'MCMXXIX', 'MCMXXX', 'MCMXXXI', 'MCMXXXII', 'MCMXXXIII', 'MCMXXXIV', 'MCMXXXV', 'MCMXXXVI', 'MCMXXXVII', 'MCMXXXVIII', 'MCMXXXIX', 'MCMXL', 'MCMXLI', 'MCMXLII', 'MCMXLIII', 'MCMXLIV', 'MCMVL', 'MCMVLI', 'MCMVLII', 'MCMVLIII', 'MCMVLIV', 'MLM', 'MLMI', 'MLMII', 'MLMIII', 'MLMIV', 'MLMV', 'MLMVI', 'MLMVII', 'MLMVIII', 'MLMIX', 'MLMX', 'MLMXI', 'MLMXII', 'MLMXIII', 'MLMXIV', 'MLMXV', 'MLMXVI', 'MLMXVII', 'MLMXVIII', 'MLMXIX', 'MLMXX', 'MLMXXI', 'MLMXXII', 'MLMXXIII', 'MLMXXIV', 'MLMXXV', 'MLMXXVI', 'MLMXXVII', 'MLMXXVIII', 'MLMXXIX', 'MLMXXX', 'MLMXXXI', 'MLMXXXII', 'MLMXXXIII', 'MLMXXXIV', 'MLMXXXV', 'MLMXXXVI', 'MLMXXXVII', 'MLMXXXVIII', 'MLMXXXIX', 'MLMXL', 'MLMXLI', 'MLMXLII', 'MLMXLIII', 'MLMXLIV', 'MLMVL', 'MLMVLI', 'MLMVLII', 'MLMVLIII', 'MLMVLIV', 'MM', 'MMI', 'MMII', 'MMIII', 'MMIV', 'MMV', 'MMVI', 'MMVII', 'MMVIII', 'MMIX', 'MMX', 'MMXI', 'MMXII', 'MMXIII', 'MMXIV', 'MMXV', 'MMXVI', 'MMXVII', 'MMXVIII', 'MMXIX', 'MMXX', 'MMXXI', 'MMXXII', 'MMXXIII', 'MMXXIV', 'MMXXV', 'MMXXVI', 'MMXXVII', 'MMXXVIII', 'MMXXIX', 'MMXXX', 'MMXXXI', 'MMXXXII', 'MMXXXIII', 'MMXXXIV', 'MMXXXV', 'MMXXXVI', 'MMXXXVII', 'MMXXXVIII', 'MMXXXIX', 'MMXL', 'MMXLI', 'MMXLII', 'MMXLIII', 'MMXLIV', 'MMVL', 'MMVLI', 'MMVLII', 'MMVLIII', 'MMVLIV', 'MML', 'MMLI', 'MMLII', 'MMLIII', 'MMLIV', 'MMLV', 'MMLVI', 'MMLVII', 'MMLVIII', 'MMLIX', 'MMLX', 'MMLXI', 'MMLXII', 'MMLXIII', 'MMLXIV', 'MMLXV', 'MMLXVI', 'MMLXVII', 'MMLXVIII', 'MMLXIX', 'MMLXX', 'MMLXXI', 'MMLXXII', 'MMLXXIII', 'MMLXXIV', 'MMLXXV', 'MMLXXVI', 'MMLXXVII', 'MMLXXVIII', 'MMLXXIX', 'MMLXXX', 'MMLXXXI', 'MMLXXXII', 'MMLXXXIII', 'MMLXXXIV', 'MMLXXXV', 'MMLXXXVI', 'MMLXXXVII', 'MMLXXXVIII', 'MMLXXXIX', 'MMXC', 'MMXCI', 'MMXCII', 'MMXCIII', 'MMXCIV', 'MMVC', 'MMVCI', 'MMVCII', 'MMVCIII', 'MMVCIV', 'MMC', 'MMCI', 'MMCII', 'MMCIII', 'MMCIV', 'MMCV', 'MMCVI', 'MMCVII', 'MMCVIII', 'MMCIX', 'MMCX', 'MMCXI', 'MMCXII', 'MMCXIII', 'MMCXIV', 'MMCXV', 'MMCXVI', 'MMCXVII', 'MMCXVIII', 'MMCXIX', 'MMCXX', 'MMCXXI', 'MMCXXII', 'MMCXXIII', 'MMCXXIV', 'MMCXXV', 'MMCXXVI', 'MMCXXVII', 'MMCXXVIII', 'MMCXXIX', 'MMCXXX', 'MMCXXXI', 'MMCXXXII', 'MMCXXXIII', 'MMCXXXIV', 'MMCXXXV', 'MMCXXXVI', 'MMCXXXVII', 'MMCXXXVIII', 'MMCXXXIX', 'MMCXL', 'MMCXLI', 'MMCXLII', 'MMCXLIII', 'MMCXLIV', 'MMCVL', 'MMCVLI', 'MMCVLII', 'MMCVLIII', 'MMCVLIV', 'MMCL', 'MMCLI', 'MMCLII', 'MMCLIII', 'MMCLIV', 'MMCLV', 'MMCLVI', 'MMCLVII', 'MMCLVIII', 'MMCLIX', 'MMCLX', 'MMCLXI', 'MMCLXII', 'MMCLXIII', 'MMCLXIV', 'MMCLXV', 'MMCLXVI', 'MMCLXVII', 'MMCLXVIII', 'MMCLXIX', 'MMCLXX', 'MMCLXXI', 'MMCLXXII', 'MMCLXXIII', 'MMCLXXIV', 'MMCLXXV', 'MMCLXXVI', 'MMCLXXVII', 'MMCLXXVIII', 'MMCLXXIX', 'MMCLXXX', 'MMCLXXXI', 'MMCLXXXII', 'MMCLXXXIII', 'MMCLXXXIV', 'MMCLXXXV', 'MMCLXXXVI', 'MMCLXXXVII', 'MMCLXXXVIII', 'MMCLXXXIX', 'MMCXC', 'MMCXCI', 'MMCXCII', 'MMCXCIII', 'MMCXCIV', 'MMCVC', 'MMCVCI', 'MMCVCII', 'MMCVCIII', 'MMCVCIV', 'MMCC', 'MMCCI', 'MMCCII', 'MMCCIII', 'MMCCIV', 'MMCCV', 'MMCCVI', 'MMCCVII', 'MMCCVIII', 'MMCCIX', 'MMCCX', 'MMCCXI', 'MMCCXII', 'MMCCXIII', 'MMCCXIV', 'MMCCXV', 'MMCCXVI', 'MMCCXVII', 'MMCCXVIII', 'MMCCXIX', 'MMCCXX', 'MMCCXXI', 'MMCCXXII', 'MMCCXXIII', 'MMCCXXIV', 'MMCCXXV', 'MMCCXXVI', 'MMCCXXVII', 'MMCCXXVIII', 'MMCCXXIX', 'MMCCXXX', 'MMCCXXXI', 'MMCCXXXII', 'MMCCXXXIII', 'MMCCXXXIV', 'MMCCXXXV', 'MMCCXXXVI', 'MMCCXXXVII', 'MMCCXXXVIII', 'MMCCXXXIX', 'MMCCXL', 'MMCCXLI', 'MMCCXLII', 'MMCCXLIII', 'MMCCXLIV', 'MMCCVL', 'MMCCVLI', 'MMCCVLII', 'MMCCVLIII', 'MMCCVLIV', 'MMCCL', 'MMCCLI', 'MMCCLII', 'MMCCLIII', 'MMCCLIV', 'MMCCLV', 'MMCCLVI', 'MMCCLVII', 'MMCCLVIII', 'MMCCLIX', 'MMCCLX', 'MMCCLXI', 'MMCCLXII', 'MMCCLXIII', 'MMCCLXIV', 'MMCCLXV', 'MMCCLXVI', 'MMCCLXVII', 'MMCCLXVIII', 'MMCCLXIX', 'MMCCLXX', 'MMCCLXXI', 'MMCCLXXII', 'MMCCLXXIII', 'MMCCLXXIV', 'MMCCLXXV', 'MMCCLXXVI', 'MMCCLXXVII', 'MMCCLXXVIII', 'MMCCLXXIX', 'MMCCLXXX', 'MMCCLXXXI', 'MMCCLXXXII', 'MMCCLXXXIII', 'MMCCLXXXIV', 'MMCCLXXXV', 'MMCCLXXXVI', 'MMCCLXXXVII', 'MMCCLXXXVIII', 'MMCCLXXXIX', 'MMCCXC', 'MMCCXCI', 'MMCCXCII', 'MMCCXCIII', 'MMCCXCIV', 'MMCCVC', 'MMCCVCI', 'MMCCVCII', 'MMCCVCIII', 'MMCCVCIV', 'MMCCC', 'MMCCCI', 'MMCCCII', 'MMCCCIII', 'MMCCCIV', 'MMCCCV', 'MMCCCVI', 'MMCCCVII', 'MMCCCVIII', 'MMCCCIX', 'MMCCCX', 'MMCCCXI', 'MMCCCXII', 'MMCCCXIII', 'MMCCCXIV', 'MMCCCXV', 'MMCCCXVI', 'MMCCCXVII', 'MMCCCXVIII', 'MMCCCXIX', 'MMCCCXX', 'MMCCCXXI', 'MMCCCXXII', 'MMCCCXXIII', 'MMCCCXXIV', 'MMCCCXXV', 'MMCCCXXVI', 'MMCCCXXVII', 'MMCCCXXVIII', 'MMCCCXXIX', 'MMCCCXXX', 'MMCCCXXXI', 'MMCCCXXXII', 'MMCCCXXXIII', 'MMCCCXXXIV', 'MMCCCXXXV', 'MMCCCXXXVI', 'MMCCCXXXVII', 'MMCCCXXXVIII', 'MMCCCXXXIX', 'MMCCCXL', 'MMCCCXLI', 'MMCCCXLII', 'MMCCCXLIII', 'MMCCCXLIV', 'MMCCCVL', 'MMCCCVLI', 'MMCCCVLII', 'MMCCCVLIII', 'MMCCCVLIV', 'MMCCCL', 'MMCCCLI', 'MMCCCLII', 'MMCCCLIII', 'MMCCCLIV', 'MMCCCLV', 'MMCCCLVI', 'MMCCCLVII', 'MMCCCLVIII', 'MMCCCLIX', 'MMCCCLX', 'MMCCCLXI', 'MMCCCLXII', 'MMCCCLXIII', 'MMCCCLXIV', 'MMCCCLXV', 'MMCCCLXVI', 'MMCCCLXVII', 'MMCCCLXVIII', 'MMCCCLXIX', 'MMCCCLXX', 'MMCCCLXXI', 'MMCCCLXXII', 'MMCCCLXXIII', 'MMCCCLXXIV', 'MMCCCLXXV', 'MMCCCLXXVI', 'MMCCCLXXVII', 'MMCCCLXXVIII', 'MMCCCLXXIX', 'MMCCCLXXX', 'MMCCCLXXXI', 'MMCCCLXXXII', 'MMCCCLXXXIII', 'MMCCCLXXXIV', 'MMCCCLXXXV', 'MMCCCLXXXVI', 'MMCCCLXXXVII', 'MMCCCLXXXVIII', 'MMCCCLXXXIX', 'MMCCCXC', 'MMCCCXCI', 'MMCCCXCII', 'MMCCCXCIII', 'MMCCCXCIV', 'MMCCCVC', 'MMCCCVCI', 'MMCCCVCII', 'MMCCCVCIII', 'MMCCCVCIV', 'MMCD', 'MMCDI', 'MMCDII', 'MMCDIII', 'MMCDIV', 'MMCDV', 'MMCDVI', 'MMCDVII', 'MMCDVIII', 'MMCDIX', 'MMCDX', 'MMCDXI', 'MMCDXII', 'MMCDXIII', 'MMCDXIV', 'MMCDXV', 'MMCDXVI', 'MMCDXVII', 'MMCDXVIII', 'MMCDXIX', 'MMCDXX', 'MMCDXXI', 'MMCDXXII', 'MMCDXXIII', 'MMCDXXIV', 'MMCDXXV', 'MMCDXXVI', 'MMCDXXVII', 'MMCDXXVIII', 'MMCDXXIX', 'MMCDXXX', 'MMCDXXXI', 'MMCDXXXII', 'MMCDXXXIII', 'MMCDXXXIV', 'MMCDXXXV', 'MMCDXXXVI', 'MMCDXXXVII', 'MMCDXXXVIII', 'MMCDXXXIX', 'MMCDXL', 'MMCDXLI', 'MMCDXLII', 'MMCDXLIII', 'MMCDXLIV', 'MMCDVL', 'MMCDVLI', 'MMCDVLII', 'MMCDVLIII', 'MMCDVLIV', 'MMLD', 'MMLDI', 'MMLDII', 'MMLDIII', 'MMLDIV', 'MMLDV', 'MMLDVI', 'MMLDVII', 'MMLDVIII', 'MMLDIX', 'MMLDX', 'MMLDXI', 'MMLDXII', 'MMLDXIII', 'MMLDXIV', 'MMLDXV', 'MMLDXVI', 'MMLDXVII', 'MMLDXVIII', 'MMLDXIX', 'MMLDXX', 'MMLDXXI', 'MMLDXXII', 'MMLDXXIII', 'MMLDXXIV', 'MMLDXXV', 'MMLDXXVI', 'MMLDXXVII', 'MMLDXXVIII', 'MMLDXXIX', 'MMLDXXX', 'MMLDXXXI', 'MMLDXXXII', 'MMLDXXXIII', 'MMLDXXXIV', 'MMLDXXXV', 'MMLDXXXVI', 'MMLDXXXVII', 'MMLDXXXVIII', 'MMLDXXXIX', 'MMLDXL', 'MMLDXLI', 'MMLDXLII', 'MMLDXLIII', 'MMLDXLIV', 'MMLDVL', 'MMLDVLI', 'MMLDVLII', 'MMLDVLIII', 'MMLDVLIV', 'MMD', 'MMDI', 'MMDII', 'MMDIII', 'MMDIV', 'MMDV', 'MMDVI', 'MMDVII', 'MMDVIII', 'MMDIX', 'MMDX', 'MMDXI', 'MMDXII', 'MMDXIII', 'MMDXIV', 'MMDXV', 'MMDXVI', 'MMDXVII', 'MMDXVIII', 'MMDXIX', 'MMDXX', 'MMDXXI', 'MMDXXII', 'MMDXXIII', 'MMDXXIV', 'MMDXXV', 'MMDXXVI', 'MMDXXVII', 'MMDXXVIII', 'MMDXXIX', 'MMDXXX', 'MMDXXXI', 'MMDXXXII', 'MMDXXXIII', 'MMDXXXIV', 'MMDXXXV', 'MMDXXXVI', 'MMDXXXVII', 'MMDXXXVIII', 'MMDXXXIX', 'MMDXL', 'MMDXLI', 'MMDXLII', 'MMDXLIII', 'MMDXLIV', 'MMDVL', 'MMDVLI', 'MMDVLII', 'MMDVLIII', 'MMDVLIV', 'MMDL', 'MMDLI', 'MMDLII', 'MMDLIII', 'MMDLIV', 'MMDLV', 'MMDLVI', 'MMDLVII', 'MMDLVIII', 'MMDLIX', 'MMDLX', 'MMDLXI', 'MMDLXII', 'MMDLXIII', 'MMDLXIV', 'MMDLXV', 'MMDLXVI', 'MMDLXVII', 'MMDLXVIII', 'MMDLXIX', 'MMDLXX', 'MMDLXXI', 'MMDLXXII', 'MMDLXXIII', 'MMDLXXIV', 'MMDLXXV', 'MMDLXXVI', 'MMDLXXVII', 'MMDLXXVIII', 'MMDLXXIX', 'MMDLXXX', 'MMDLXXXI', 'MMDLXXXII', 'MMDLXXXIII', 'MMDLXXXIV', 'MMDLXXXV', 'MMDLXXXVI', 'MMDLXXXVII', 'MMDLXXXVIII', 'MMDLXXXIX', 'MMDXC', 'MMDXCI', 'MMDXCII', 'MMDXCIII', 'MMDXCIV', 'MMDVC', 'MMDVCI', 'MMDVCII', 'MMDVCIII', 'MMDVCIV', 'MMDC', 'MMDCI', 'MMDCII', 'MMDCIII', 'MMDCIV', 'MMDCV', 'MMDCVI', 'MMDCVII', 'MMDCVIII', 'MMDCIX', 'MMDCX', 'MMDCXI', 'MMDCXII', 'MMDCXIII', 'MMDCXIV', 'MMDCXV', 'MMDCXVI', 'MMDCXVII', 'MMDCXVIII', 'MMDCXIX', 'MMDCXX', 'MMDCXXI', 'MMDCXXII', 'MMDCXXIII', 'MMDCXXIV', 'MMDCXXV', 'MMDCXXVI', 'MMDCXXVII', 'MMDCXXVIII', 'MMDCXXIX', 'MMDCXXX', 'MMDCXXXI', 'MMDCXXXII', 'MMDCXXXIII', 'MMDCXXXIV', 'MMDCXXXV', 'MMDCXXXVI', 'MMDCXXXVII', 'MMDCXXXVIII', 'MMDCXXXIX', 'MMDCXL', 'MMDCXLI', 'MMDCXLII', 'MMDCXLIII', 'MMDCXLIV', 'MMDCVL', 'MMDCVLI', 'MMDCVLII', 'MMDCVLIII', 'MMDCVLIV', 'MMDCL', 'MMDCLI', 'MMDCLII', 'MMDCLIII', 'MMDCLIV', 'MMDCLV', 'MMDCLVI', 'MMDCLVII', 'MMDCLVIII', 'MMDCLIX', 'MMDCLX', 'MMDCLXI', 'MMDCLXII', 'MMDCLXIII', 'MMDCLXIV', 'MMDCLXV', 'MMDCLXVI', 'MMDCLXVII', 'MMDCLXVIII', 'MMDCLXIX', 'MMDCLXX', 'MMDCLXXI', 'MMDCLXXII', 'MMDCLXXIII', 'MMDCLXXIV', 'MMDCLXXV', 'MMDCLXXVI', 'MMDCLXXVII', 'MMDCLXXVIII', 'MMDCLXXIX', 'MMDCLXXX', 'MMDCLXXXI', 'MMDCLXXXII', 'MMDCLXXXIII', 'MMDCLXXXIV', 'MMDCLXXXV', 'MMDCLXXXVI', 'MMDCLXXXVII', 'MMDCLXXXVIII', 'MMDCLXXXIX', 'MMDCXC', 'MMDCXCI', 'MMDCXCII', 'MMDCXCIII', 'MMDCXCIV', 'MMDCVC', 'MMDCVCI', 'MMDCVCII', 'MMDCVCIII', 'MMDCVCIV', 'MMDCC', 'MMDCCI', 'MMDCCII', 'MMDCCIII', 'MMDCCIV', 'MMDCCV', 'MMDCCVI', 'MMDCCVII', 'MMDCCVIII', 'MMDCCIX', 'MMDCCX', 'MMDCCXI', 'MMDCCXII', 'MMDCCXIII', 'MMDCCXIV', 'MMDCCXV', 'MMDCCXVI', 'MMDCCXVII', 'MMDCCXVIII', 'MMDCCXIX', 'MMDCCXX', 'MMDCCXXI', 'MMDCCXXII', 'MMDCCXXIII', 'MMDCCXXIV', 'MMDCCXXV', 'MMDCCXXVI', 'MMDCCXXVII', 'MMDCCXXVIII', 'MMDCCXXIX', 'MMDCCXXX', 'MMDCCXXXI', 'MMDCCXXXII', 'MMDCCXXXIII', 'MMDCCXXXIV', 'MMDCCXXXV', 'MMDCCXXXVI', 'MMDCCXXXVII', 'MMDCCXXXVIII', 'MMDCCXXXIX', 'MMDCCXL', 'MMDCCXLI', 'MMDCCXLII', 'MMDCCXLIII', 'MMDCCXLIV', 'MMDCCVL', 'MMDCCVLI', 'MMDCCVLII', 'MMDCCVLIII', 'MMDCCVLIV', 'MMDCCL', 'MMDCCLI', 'MMDCCLII', 'MMDCCLIII', 'MMDCCLIV', 'MMDCCLV', 'MMDCCLVI', 'MMDCCLVII', 'MMDCCLVIII', 'MMDCCLIX', 'MMDCCLX', 'MMDCCLXI', 'MMDCCLXII', 'MMDCCLXIII', 'MMDCCLXIV', 'MMDCCLXV', 'MMDCCLXVI', 'MMDCCLXVII', 'MMDCCLXVIII', 'MMDCCLXIX', 'MMDCCLXX', 'MMDCCLXXI', 'MMDCCLXXII', 'MMDCCLXXIII', 'MMDCCLXXIV', 'MMDCCLXXV', 'MMDCCLXXVI', 'MMDCCLXXVII', 'MMDCCLXXVIII', 'MMDCCLXXIX', 'MMDCCLXXX', 'MMDCCLXXXI', 'MMDCCLXXXII', 'MMDCCLXXXIII', 'MMDCCLXXXIV', 'MMDCCLXXXV', 'MMDCCLXXXVI', 'MMDCCLXXXVII', 'MMDCCLXXXVIII', 'MMDCCLXXXIX', 'MMDCCXC', 'MMDCCXCI', 'MMDCCXCII', 'MMDCCXCIII', 'MMDCCXCIV', 'MMDCCVC', 'MMDCCVCI', 'MMDCCVCII', 'MMDCCVCIII', 'MMDCCVCIV', 'MMDCCC', 'MMDCCCI', 'MMDCCCII', 'MMDCCCIII', 'MMDCCCIV', 'MMDCCCV', 'MMDCCCVI', 'MMDCCCVII', 'MMDCCCVIII', 'MMDCCCIX', 'MMDCCCX', 'MMDCCCXI', 'MMDCCCXII', 'MMDCCCXIII', 'MMDCCCXIV', 'MMDCCCXV', 'MMDCCCXVI', 'MMDCCCXVII', 'MMDCCCXVIII', 'MMDCCCXIX', 'MMDCCCXX', 'MMDCCCXXI', 'MMDCCCXXII', 'MMDCCCXXIII', 'MMDCCCXXIV', 'MMDCCCXXV', 'MMDCCCXXVI', 'MMDCCCXXVII', 'MMDCCCXXVIII', 'MMDCCCXXIX', 'MMDCCCXXX', 'MMDCCCXXXI', 'MMDCCCXXXII', 'MMDCCCXXXIII', 'MMDCCCXXXIV', 'MMDCCCXXXV', 'MMDCCCXXXVI', 'MMDCCCXXXVII', 'MMDCCCXXXVIII', 'MMDCCCXXXIX', 'MMDCCCXL', 'MMDCCCXLI', 'MMDCCCXLII', 'MMDCCCXLIII', 'MMDCCCXLIV', 'MMDCCCVL', 'MMDCCCVLI', 'MMDCCCVLII', 'MMDCCCVLIII', 'MMDCCCVLIV', 'MMDCCCL', 'MMDCCCLI', 'MMDCCCLII', 'MMDCCCLIII', 'MMDCCCLIV', 'MMDCCCLV', 'MMDCCCLVI', 'MMDCCCLVII', 'MMDCCCLVIII', 'MMDCCCLIX', 'MMDCCCLX', 'MMDCCCLXI', 'MMDCCCLXII', 'MMDCCCLXIII', 'MMDCCCLXIV', 'MMDCCCLXV', 'MMDCCCLXVI', 'MMDCCCLXVII', 'MMDCCCLXVIII', 'MMDCCCLXIX', 'MMDCCCLXX', 'MMDCCCLXXI', 'MMDCCCLXXII', 'MMDCCCLXXIII', 'MMDCCCLXXIV', 'MMDCCCLXXV', 'MMDCCCLXXVI', 'MMDCCCLXXVII', 'MMDCCCLXXVIII', 'MMDCCCLXXIX', 'MMDCCCLXXX', 'MMDCCCLXXXI', 'MMDCCCLXXXII', 'MMDCCCLXXXIII', 'MMDCCCLXXXIV', 'MMDCCCLXXXV', 'MMDCCCLXXXVI', 'MMDCCCLXXXVII', 'MMDCCCLXXXVIII', 'MMDCCCLXXXIX', 'MMDCCCXC', 'MMDCCCXCI', 'MMDCCCXCII', 'MMDCCCXCIII', 'MMDCCCXCIV', 'MMDCCCVC', 'MMDCCCVCI', 'MMDCCCVCII', 'MMDCCCVCIII', 'MMDCCCVCIV', 'MMCM', 'MMCMI', 'MMCMII', 'MMCMIII', 'MMCMIV', 'MMCMV', 'MMCMVI', 'MMCMVII', 'MMCMVIII', 'MMCMIX', 'MMCMX', 'MMCMXI', 'MMCMXII', 'MMCMXIII', 'MMCMXIV', 'MMCMXV', 'MMCMXVI', 'MMCMXVII', 'MMCMXVIII', 'MMCMXIX', 'MMCMXX', 'MMCMXXI', 'MMCMXXII', 'MMCMXXIII', 'MMCMXXIV', 'MMCMXXV', 'MMCMXXVI', 'MMCMXXVII', 'MMCMXXVIII', 'MMCMXXIX', 'MMCMXXX', 'MMCMXXXI', 'MMCMXXXII', 'MMCMXXXIII', 'MMCMXXXIV', 'MMCMXXXV', 'MMCMXXXVI', 'MMCMXXXVII', 'MMCMXXXVIII', 'MMCMXXXIX', 'MMCMXL', 'MMCMXLI', 'MMCMXLII', 'MMCMXLIII', 'MMCMXLIV', 'MMCMVL', 'MMCMVLI', 'MMCMVLII', 'MMCMVLIII', 'MMCMVLIV', 'MMLM', 'MMLMI', 'MMLMII', 'MMLMIII', 'MMLMIV', 'MMLMV', 'MMLMVI', 'MMLMVII', 'MMLMVIII', 'MMLMIX', 'MMLMX', 'MMLMXI', 'MMLMXII', 'MMLMXIII', 'MMLMXIV', 'MMLMXV', 'MMLMXVI', 'MMLMXVII', 'MMLMXVIII', 'MMLMXIX', 'MMLMXX', 'MMLMXXI', 'MMLMXXII', 'MMLMXXIII', 'MMLMXXIV', 'MMLMXXV', 'MMLMXXVI', 'MMLMXXVII', 'MMLMXXVIII', 'MMLMXXIX', 'MMLMXXX', 'MMLMXXXI', 'MMLMXXXII', 'MMLMXXXIII', 'MMLMXXXIV', 'MMLMXXXV', 'MMLMXXXVI', 'MMLMXXXVII', 'MMLMXXXVIII', 'MMLMXXXIX', 'MMLMXL', 'MMLMXLI', 'MMLMXLII', 'MMLMXLIII', 'MMLMXLIV', 'MMLMVL', 'MMLMVLI', 'MMLMVLII', 'MMLMVLIII', 'MMLMVLIV', 'MMM', 'MMMI', 'MMMII', 'MMMIII', 'MMMIV', 'MMMV', 'MMMVI', 'MMMVII', 'MMMVIII', 'MMMIX', 'MMMX', 'MMMXI', 'MMMXII', 'MMMXIII', 'MMMXIV', 'MMMXV', 'MMMXVI', 'MMMXVII', 'MMMXVIII', 'MMMXIX', 'MMMXX', 'MMMXXI', 'MMMXXII', 'MMMXXIII', 'MMMXXIV', 'MMMXXV', 'MMMXXVI', 'MMMXXVII', 'MMMXXVIII', 'MMMXXIX', 'MMMXXX', 'MMMXXXI', 'MMMXXXII', 'MMMXXXIII', 'MMMXXXIV', 'MMMXXXV', 'MMMXXXVI', 'MMMXXXVII', 'MMMXXXVIII', 'MMMXXXIX', 'MMMXL', 'MMMXLI', 'MMMXLII', 'MMMXLIII', 'MMMXLIV', 'MMMVL', 'MMMVLI', 'MMMVLII', 'MMMVLIII', 'MMMVLIV', 'MMML', 'MMMLI', 'MMMLII', 'MMMLIII', 'MMMLIV', 'MMMLV', 'MMMLVI', 'MMMLVII', 'MMMLVIII', 'MMMLIX', 'MMMLX', 'MMMLXI', 'MMMLXII', 'MMMLXIII', 'MMMLXIV', 'MMMLXV', 'MMMLXVI', 'MMMLXVII', 'MMMLXVIII', 'MMMLXIX', 'MMMLXX', 'MMMLXXI', 'MMMLXXII', 'MMMLXXIII', 'MMMLXXIV', 'MMMLXXV', 'MMMLXXVI', 'MMMLXXVII', 'MMMLXXVIII', 'MMMLXXIX', 'MMMLXXX', 'MMMLXXXI', 'MMMLXXXII', 'MMMLXXXIII', 'MMMLXXXIV', 'MMMLXXXV', 'MMMLXXXVI', 'MMMLXXXVII', 'MMMLXXXVIII', 'MMMLXXXIX', 'MMMXC', 'MMMXCI', 'MMMXCII', 'MMMXCIII', 'MMMXCIV', 'MMMVC', 'MMMVCI', 'MMMVCII', 'MMMVCIII', 'MMMVCIV', 'MMMC', 'MMMCI', 'MMMCII', 'MMMCIII', 'MMMCIV', 'MMMCV', 'MMMCVI', 'MMMCVII', 'MMMCVIII', 'MMMCIX', 'MMMCX', 'MMMCXI', 'MMMCXII', 'MMMCXIII', 'MMMCXIV', 'MMMCXV', 'MMMCXVI', 'MMMCXVII', 'MMMCXVIII', 'MMMCXIX', 'MMMCXX', 'MMMCXXI', 'MMMCXXII', 'MMMCXXIII', 'MMMCXXIV', 'MMMCXXV', 'MMMCXXVI', 'MMMCXXVII', 'MMMCXXVIII', 'MMMCXXIX', 'MMMCXXX', 'MMMCXXXI', 'MMMCXXXII', 'MMMCXXXIII', 'MMMCXXXIV', 'MMMCXXXV', 'MMMCXXXVI', 'MMMCXXXVII', 'MMMCXXXVIII', 'MMMCXXXIX', 'MMMCXL', 'MMMCXLI', 'MMMCXLII', 'MMMCXLIII', 'MMMCXLIV', 'MMMCVL', 'MMMCVLI', 'MMMCVLII', 'MMMCVLIII', 'MMMCVLIV', 'MMMCL', 'MMMCLI', 'MMMCLII', 'MMMCLIII', 'MMMCLIV', 'MMMCLV', 'MMMCLVI', 'MMMCLVII', 'MMMCLVIII', 'MMMCLIX', 'MMMCLX', 'MMMCLXI', 'MMMCLXII', 'MMMCLXIII', 'MMMCLXIV', 'MMMCLXV', 'MMMCLXVI', 'MMMCLXVII', 'MMMCLXVIII', 'MMMCLXIX', 'MMMCLXX', 'MMMCLXXI', 'MMMCLXXII', 'MMMCLXXIII', 'MMMCLXXIV', 'MMMCLXXV', 'MMMCLXXVI', 'MMMCLXXVII', 'MMMCLXXVIII', 'MMMCLXXIX', 'MMMCLXXX', 'MMMCLXXXI', 'MMMCLXXXII', 'MMMCLXXXIII', 'MMMCLXXXIV', 'MMMCLXXXV', 'MMMCLXXXVI', 'MMMCLXXXVII', 'MMMCLXXXVIII', 'MMMCLXXXIX', 'MMMCXC', 'MMMCXCI', 'MMMCXCII', 'MMMCXCIII', 'MMMCXCIV', 'MMMCVC', 'MMMCVCI', 'MMMCVCII', 'MMMCVCIII', 'MMMCVCIV', 'MMMCC', 'MMMCCI', 'MMMCCII', 'MMMCCIII', 'MMMCCIV', 'MMMCCV', 'MMMCCVI', 'MMMCCVII', 'MMMCCVIII', 'MMMCCIX', 'MMMCCX', 'MMMCCXI', 'MMMCCXII', 'MMMCCXIII', 'MMMCCXIV', 'MMMCCXV', 'MMMCCXVI', 'MMMCCXVII', 'MMMCCXVIII', 'MMMCCXIX', 'MMMCCXX', 'MMMCCXXI', 'MMMCCXXII', 'MMMCCXXIII', 'MMMCCXXIV', 'MMMCCXXV', 'MMMCCXXVI', 'MMMCCXXVII', 'MMMCCXXVIII', 'MMMCCXXIX', 'MMMCCXXX', 'MMMCCXXXI', 'MMMCCXXXII', 'MMMCCXXXIII', 'MMMCCXXXIV', 'MMMCCXXXV', 'MMMCCXXXVI', 'MMMCCXXXVII', 'MMMCCXXXVIII', 'MMMCCXXXIX', 'MMMCCXL', 'MMMCCXLI', 'MMMCCXLII', 'MMMCCXLIII', 'MMMCCXLIV', 'MMMCCVL', 'MMMCCVLI', 'MMMCCVLII', 'MMMCCVLIII', 'MMMCCVLIV', 'MMMCCL', 'MMMCCLI', 'MMMCCLII', 'MMMCCLIII', 'MMMCCLIV', 'MMMCCLV', 'MMMCCLVI', 'MMMCCLVII', 'MMMCCLVIII', 'MMMCCLIX', 'MMMCCLX', 'MMMCCLXI', 'MMMCCLXII', 'MMMCCLXIII', 'MMMCCLXIV', 'MMMCCLXV', 'MMMCCLXVI', 'MMMCCLXVII', 'MMMCCLXVIII', 'MMMCCLXIX', 'MMMCCLXX', 'MMMCCLXXI', 'MMMCCLXXII', 'MMMCCLXXIII', 'MMMCCLXXIV', 'MMMCCLXXV', 'MMMCCLXXVI', 'MMMCCLXXVII', 'MMMCCLXXVIII', 'MMMCCLXXIX', 'MMMCCLXXX', 'MMMCCLXXXI', 'MMMCCLXXXII', 'MMMCCLXXXIII', 'MMMCCLXXXIV', 'MMMCCLXXXV', 'MMMCCLXXXVI', 'MMMCCLXXXVII', 'MMMCCLXXXVIII', 'MMMCCLXXXIX', 'MMMCCXC', 'MMMCCXCI', 'MMMCCXCII', 'MMMCCXCIII', 'MMMCCXCIV', 'MMMCCVC', 'MMMCCVCI', 'MMMCCVCII', 'MMMCCVCIII', 'MMMCCVCIV', 'MMMCCC', 'MMMCCCI', 'MMMCCCII', 'MMMCCCIII', 'MMMCCCIV', 'MMMCCCV', 'MMMCCCVI', 'MMMCCCVII', 'MMMCCCVIII', 'MMMCCCIX', 'MMMCCCX', 'MMMCCCXI', 'MMMCCCXII', 'MMMCCCXIII', 'MMMCCCXIV', 'MMMCCCXV', 'MMMCCCXVI', 'MMMCCCXVII', 'MMMCCCXVIII', 'MMMCCCXIX', 'MMMCCCXX', 'MMMCCCXXI', 'MMMCCCXXII', 'MMMCCCXXIII', 'MMMCCCXXIV', 'MMMCCCXXV', 'MMMCCCXXVI', 'MMMCCCXXVII', 'MMMCCCXXVIII', 'MMMCCCXXIX', 'MMMCCCXXX', 'MMMCCCXXXI', 'MMMCCCXXXII', 'MMMCCCXXXIII', 'MMMCCCXXXIV', 'MMMCCCXXXV', 'MMMCCCXXXVI', 'MMMCCCXXXVII', 'MMMCCCXXXVIII', 'MMMCCCXXXIX', 'MMMCCCXL', 'MMMCCCXLI', 'MMMCCCXLII', 'MMMCCCXLIII', 'MMMCCCXLIV', 'MMMCCCVL', 'MMMCCCVLI', 'MMMCCCVLII', 'MMMCCCVLIII', 'MMMCCCVLIV', 'MMMCCCL', 'MMMCCCLI', 'MMMCCCLII', 'MMMCCCLIII', 'MMMCCCLIV', 'MMMCCCLV', 'MMMCCCLVI', 'MMMCCCLVII', 'MMMCCCLVIII', 'MMMCCCLIX', 'MMMCCCLX', 'MMMCCCLXI', 'MMMCCCLXII', 'MMMCCCLXIII', 'MMMCCCLXIV', 'MMMCCCLXV', 'MMMCCCLXVI', 'MMMCCCLXVII', 'MMMCCCLXVIII', 'MMMCCCLXIX', 'MMMCCCLXX', 'MMMCCCLXXI', 'MMMCCCLXXII', 'MMMCCCLXXIII', 'MMMCCCLXXIV', 'MMMCCCLXXV', 'MMMCCCLXXVI', 'MMMCCCLXXVII', 'MMMCCCLXXVIII', 'MMMCCCLXXIX', 'MMMCCCLXXX', 'MMMCCCLXXXI', 'MMMCCCLXXXII', 'MMMCCCLXXXIII', 'MMMCCCLXXXIV', 'MMMCCCLXXXV', 'MMMCCCLXXXVI', 'MMMCCCLXXXVII', 'MMMCCCLXXXVIII', 'MMMCCCLXXXIX', 'MMMCCCXC', 'MMMCCCXCI', 'MMMCCCXCII', 'MMMCCCXCIII', 'MMMCCCXCIV', 'MMMCCCVC', 'MMMCCCVCI', 'MMMCCCVCII', 'MMMCCCVCIII', 'MMMCCCVCIV', 'MMMCD', 'MMMCDI', 'MMMCDII', 'MMMCDIII', 'MMMCDIV', 'MMMCDV', 'MMMCDVI', 'MMMCDVII', 'MMMCDVIII', 'MMMCDIX', 'MMMCDX', 'MMMCDXI', 'MMMCDXII', 'MMMCDXIII', 'MMMCDXIV', 'MMMCDXV', 'MMMCDXVI', 'MMMCDXVII', 'MMMCDXVIII', 'MMMCDXIX', 'MMMCDXX', 'MMMCDXXI', 'MMMCDXXII', 'MMMCDXXIII', 'MMMCDXXIV', 'MMMCDXXV', 'MMMCDXXVI', 'MMMCDXXVII', 'MMMCDXXVIII', 'MMMCDXXIX', 'MMMCDXXX', 'MMMCDXXXI', 'MMMCDXXXII', 'MMMCDXXXIII', 'MMMCDXXXIV', 'MMMCDXXXV', 'MMMCDXXXVI', 'MMMCDXXXVII', 'MMMCDXXXVIII', 'MMMCDXXXIX', 'MMMCDXL', 'MMMCDXLI', 'MMMCDXLII', 'MMMCDXLIII', 'MMMCDXLIV', 'MMMCDVL', 'MMMCDVLI', 'MMMCDVLII', 'MMMCDVLIII', 'MMMCDVLIV', 'MMMLD', 'MMMLDI', 'MMMLDII', 'MMMLDIII', 'MMMLDIV', 'MMMLDV', 'MMMLDVI', 'MMMLDVII', 'MMMLDVIII', 'MMMLDIX', 'MMMLDX', 'MMMLDXI', 'MMMLDXII', 'MMMLDXIII', 'MMMLDXIV', 'MMMLDXV', 'MMMLDXVI', 'MMMLDXVII', 'MMMLDXVIII', 'MMMLDXIX', 'MMMLDXX', 'MMMLDXXI', 'MMMLDXXII', 'MMMLDXXIII', 'MMMLDXXIV', 'MMMLDXXV', 'MMMLDXXVI', 'MMMLDXXVII', 'MMMLDXXVIII', 'MMMLDXXIX', 'MMMLDXXX', 'MMMLDXXXI', 'MMMLDXXXII', 'MMMLDXXXIII', 'MMMLDXXXIV', 'MMMLDXXXV', 'MMMLDXXXVI', 'MMMLDXXXVII', 'MMMLDXXXVIII', 'MMMLDXXXIX', 'MMMLDXL', 'MMMLDXLI', 'MMMLDXLII', 'MMMLDXLIII', 'MMMLDXLIV', 'MMMLDVL', 'MMMLDVLI', 'MMMLDVLII', 'MMMLDVLIII', 'MMMLDVLIV', 'MMMD', 'MMMDI', 'MMMDII', 'MMMDIII', 'MMMDIV', 'MMMDV', 'MMMDVI', 'MMMDVII', 'MMMDVIII', 'MMMDIX', 'MMMDX', 'MMMDXI', 'MMMDXII', 'MMMDXIII', 'MMMDXIV', 'MMMDXV', 'MMMDXVI', 'MMMDXVII', 'MMMDXVIII', 'MMMDXIX', 'MMMDXX', 'MMMDXXI', 'MMMDXXII', 'MMMDXXIII', 'MMMDXXIV', 'MMMDXXV', 'MMMDXXVI', 'MMMDXXVII', 'MMMDXXVIII', 'MMMDXXIX', 'MMMDXXX', 'MMMDXXXI', 'MMMDXXXII', 'MMMDXXXIII', 'MMMDXXXIV', 'MMMDXXXV', 'MMMDXXXVI', 'MMMDXXXVII', 'MMMDXXXVIII', 'MMMDXXXIX', 'MMMDXL', 'MMMDXLI', 'MMMDXLII', 'MMMDXLIII', 'MMMDXLIV', 'MMMDVL', 'MMMDVLI', 'MMMDVLII', 'MMMDVLIII', 'MMMDVLIV', 'MMMDL', 'MMMDLI', 'MMMDLII', 'MMMDLIII', 'MMMDLIV', 'MMMDLV', 'MMMDLVI', 'MMMDLVII', 'MMMDLVIII', 'MMMDLIX', 'MMMDLX', 'MMMDLXI', 'MMMDLXII', 'MMMDLXIII', 'MMMDLXIV', 'MMMDLXV', 'MMMDLXVI', 'MMMDLXVII', 'MMMDLXVIII', 'MMMDLXIX', 'MMMDLXX', 'MMMDLXXI', 'MMMDLXXII', 'MMMDLXXIII', 'MMMDLXXIV', 'MMMDLXXV', 'MMMDLXXVI', 'MMMDLXXVII', 'MMMDLXXVIII', 'MMMDLXXIX', 'MMMDLXXX', 'MMMDLXXXI', 'MMMDLXXXII', 'MMMDLXXXIII', 'MMMDLXXXIV', 'MMMDLXXXV', 'MMMDLXXXVI', 'MMMDLXXXVII', 'MMMDLXXXVIII', 'MMMDLXXXIX', 'MMMDXC', 'MMMDXCI', 'MMMDXCII', 'MMMDXCIII', 'MMMDXCIV', 'MMMDVC', 'MMMDVCI', 'MMMDVCII', 'MMMDVCIII', 'MMMDVCIV', 'MMMDC', 'MMMDCI', 'MMMDCII', 'MMMDCIII', 'MMMDCIV', 'MMMDCV', 'MMMDCVI', 'MMMDCVII', 'MMMDCVIII', 'MMMDCIX', 'MMMDCX', 'MMMDCXI', 'MMMDCXII', 'MMMDCXIII', 'MMMDCXIV', 'MMMDCXV', 'MMMDCXVI', 'MMMDCXVII', 'MMMDCXVIII', 'MMMDCXIX', 'MMMDCXX', 'MMMDCXXI', 'MMMDCXXII', 'MMMDCXXIII', 'MMMDCXXIV', 'MMMDCXXV', 'MMMDCXXVI', 'MMMDCXXVII', 'MMMDCXXVIII', 'MMMDCXXIX', 'MMMDCXXX', 'MMMDCXXXI', 'MMMDCXXXII', 'MMMDCXXXIII', 'MMMDCXXXIV', 'MMMDCXXXV', 'MMMDCXXXVI', 'MMMDCXXXVII', 'MMMDCXXXVIII', 'MMMDCXXXIX', 'MMMDCXL', 'MMMDCXLI', 'MMMDCXLII', 'MMMDCXLIII', 'MMMDCXLIV', 'MMMDCVL', 'MMMDCVLI', 'MMMDCVLII', 'MMMDCVLIII', 'MMMDCVLIV', 'MMMDCL', 'MMMDCLI', 'MMMDCLII', 'MMMDCLIII', 'MMMDCLIV', 'MMMDCLV', 'MMMDCLVI', 'MMMDCLVII', 'MMMDCLVIII', 'MMMDCLIX', 'MMMDCLX', 'MMMDCLXI', 'MMMDCLXII', 'MMMDCLXIII', 'MMMDCLXIV', 'MMMDCLXV', 'MMMDCLXVI', 'MMMDCLXVII', 'MMMDCLXVIII', 'MMMDCLXIX', 'MMMDCLXX', 'MMMDCLXXI', 'MMMDCLXXII', 'MMMDCLXXIII', 'MMMDCLXXIV', 'MMMDCLXXV', 'MMMDCLXXVI', 'MMMDCLXXVII', 'MMMDCLXXVIII', 'MMMDCLXXIX', 'MMMDCLXXX', 'MMMDCLXXXI', 'MMMDCLXXXII', 'MMMDCLXXXIII', 'MMMDCLXXXIV', 'MMMDCLXXXV', 'MMMDCLXXXVI', 'MMMDCLXXXVII', 'MMMDCLXXXVIII', 'MMMDCLXXXIX', 'MMMDCXC', 'MMMDCXCI', 'MMMDCXCII', 'MMMDCXCIII', 'MMMDCXCIV', 'MMMDCVC', 'MMMDCVCI', 'MMMDCVCII', 'MMMDCVCIII', 'MMMDCVCIV', 'MMMDCC', 'MMMDCCI', 'MMMDCCII', 'MMMDCCIII', 'MMMDCCIV', 'MMMDCCV', 'MMMDCCVI', 'MMMDCCVII', 'MMMDCCVIII', 'MMMDCCIX', 'MMMDCCX', 'MMMDCCXI', 'MMMDCCXII', 'MMMDCCXIII', 'MMMDCCXIV', 'MMMDCCXV', 'MMMDCCXVI', 'MMMDCCXVII', 'MMMDCCXVIII', 'MMMDCCXIX', 'MMMDCCXX', 'MMMDCCXXI', 'MMMDCCXXII', 'MMMDCCXXIII', 'MMMDCCXXIV', 'MMMDCCXXV', 'MMMDCCXXVI', 'MMMDCCXXVII', 'MMMDCCXXVIII', 'MMMDCCXXIX', 'MMMDCCXXX', 'MMMDCCXXXI', 'MMMDCCXXXII', 'MMMDCCXXXIII', 'MMMDCCXXXIV', 'MMMDCCXXXV', 'MMMDCCXXXVI', 'MMMDCCXXXVII', 'MMMDCCXXXVIII', 'MMMDCCXXXIX', 'MMMDCCXL', 'MMMDCCXLI', 'MMMDCCXLII', 'MMMDCCXLIII', 'MMMDCCXLIV', 'MMMDCCVL', 'MMMDCCVLI', 'MMMDCCVLII', 'MMMDCCVLIII', 'MMMDCCVLIV', 'MMMDCCL', 'MMMDCCLI', 'MMMDCCLII', 'MMMDCCLIII', 'MMMDCCLIV', 'MMMDCCLV', 'MMMDCCLVI', 'MMMDCCLVII', 'MMMDCCLVIII', 'MMMDCCLIX', 'MMMDCCLX', 'MMMDCCLXI', 'MMMDCCLXII', 'MMMDCCLXIII', 'MMMDCCLXIV', 'MMMDCCLXV', 'MMMDCCLXVI', 'MMMDCCLXVII', 'MMMDCCLXVIII', 'MMMDCCLXIX', 'MMMDCCLXX', 'MMMDCCLXXI', 'MMMDCCLXXII', 'MMMDCCLXXIII', 'MMMDCCLXXIV', 'MMMDCCLXXV', 'MMMDCCLXXVI', 'MMMDCCLXXVII', 'MMMDCCLXXVIII', 'MMMDCCLXXIX', 'MMMDCCLXXX', 'MMMDCCLXXXI', 'MMMDCCLXXXII', 'MMMDCCLXXXIII', 'MMMDCCLXXXIV', 'MMMDCCLXXXV', 'MMMDCCLXXXVI', 'MMMDCCLXXXVII', 'MMMDCCLXXXVIII', 'MMMDCCLXXXIX', 'MMMDCCXC', 'MMMDCCXCI', 'MMMDCCXCII', 'MMMDCCXCIII', 'MMMDCCXCIV', 'MMMDCCVC', 'MMMDCCVCI', 'MMMDCCVCII', 'MMMDCCVCIII', 'MMMDCCVCIV', 'MMMDCCC', 'MMMDCCCI', 'MMMDCCCII', 'MMMDCCCIII', 'MMMDCCCIV', 'MMMDCCCV', 'MMMDCCCVI', 'MMMDCCCVII', 'MMMDCCCVIII', 'MMMDCCCIX', 'MMMDCCCX', 'MMMDCCCXI', 'MMMDCCCXII', 'MMMDCCCXIII', 'MMMDCCCXIV', 'MMMDCCCXV', 'MMMDCCCXVI', 'MMMDCCCXVII', 'MMMDCCCXVIII', 'MMMDCCCXIX', 'MMMDCCCXX', 'MMMDCCCXXI', 'MMMDCCCXXII', 'MMMDCCCXXIII', 'MMMDCCCXXIV', 'MMMDCCCXXV', 'MMMDCCCXXVI', 'MMMDCCCXXVII', 'MMMDCCCXXVIII', 'MMMDCCCXXIX', 'MMMDCCCXXX', 'MMMDCCCXXXI', 'MMMDCCCXXXII', 'MMMDCCCXXXIII', 'MMMDCCCXXXIV', 'MMMDCCCXXXV', 'MMMDCCCXXXVI', 'MMMDCCCXXXVII', 'MMMDCCCXXXVIII', 'MMMDCCCXXXIX', 'MMMDCCCXL', 'MMMDCCCXLI', 'MMMDCCCXLII', 'MMMDCCCXLIII', 'MMMDCCCXLIV', 'MMMDCCCVL', 'MMMDCCCVLI', 'MMMDCCCVLII', 'MMMDCCCVLIII', 'MMMDCCCVLIV', 'MMMDCCCL', 'MMMDCCCLI', 'MMMDCCCLII', 'MMMDCCCLIII', 'MMMDCCCLIV', 'MMMDCCCLV', 'MMMDCCCLVI', 'MMMDCCCLVII', 'MMMDCCCLVIII', 'MMMDCCCLIX', 'MMMDCCCLX', 'MMMDCCCLXI', 'MMMDCCCLXII', 'MMMDCCCLXIII', 'MMMDCCCLXIV', 'MMMDCCCLXV', 'MMMDCCCLXVI', 'MMMDCCCLXVII', 'MMMDCCCLXVIII', 'MMMDCCCLXIX', 'MMMDCCCLXX', 'MMMDCCCLXXI', 'MMMDCCCLXXII', 'MMMDCCCLXXIII', 'MMMDCCCLXXIV', 'MMMDCCCLXXV', 'MMMDCCCLXXVI', 'MMMDCCCLXXVII', 'MMMDCCCLXXVIII', 'MMMDCCCLXXIX', 'MMMDCCCLXXX', 'MMMDCCCLXXXI', 'MMMDCCCLXXXII', 'MMMDCCCLXXXIII', 'MMMDCCCLXXXIV', 'MMMDCCCLXXXV', 'MMMDCCCLXXXVI', 'MMMDCCCLXXXVII', 'MMMDCCCLXXXVIII', 'MMMDCCCLXXXIX', 'MMMDCCCXC', 'MMMDCCCXCI', 'MMMDCCCXCII', 'MMMDCCCXCIII', 'MMMDCCCXCIV', 'MMMDCCCVC', 'MMMDCCCVCI', 'MMMDCCCVCII', 'MMMDCCCVCIII', 'MMMDCCCVCIV', 'MMMCM', 'MMMCMI', 'MMMCMII', 'MMMCMIII', 'MMMCMIV', 'MMMCMV', 'MMMCMVI', 'MMMCMVII', 'MMMCMVIII', 'MMMCMIX', 'MMMCMX', 'MMMCMXI', 'MMMCMXII', 'MMMCMXIII', 'MMMCMXIV', 'MMMCMXV', 'MMMCMXVI', 'MMMCMXVII', 'MMMCMXVIII', 'MMMCMXIX', 'MMMCMXX', 'MMMCMXXI', 'MMMCMXXII', 'MMMCMXXIII', 'MMMCMXXIV', 'MMMCMXXV', 'MMMCMXXVI', 'MMMCMXXVII', 'MMMCMXXVIII', 'MMMCMXXIX', 'MMMCMXXX', 'MMMCMXXXI', 'MMMCMXXXII', 'MMMCMXXXIII', 'MMMCMXXXIV', 'MMMCMXXXV', 'MMMCMXXXVI', 'MMMCMXXXVII', 'MMMCMXXXVIII', 'MMMCMXXXIX', 'MMMCMXL', 'MMMCMXLI', 'MMMCMXLII', 'MMMCMXLIII', 'MMMCMXLIV', 'MMMCMVL', 'MMMCMVLI', 'MMMCMVLII', 'MMMCMVLIII', 'MMMCMVLIV', 'MMMLM', 'MMMLMI', 'MMMLMII', 'MMMLMIII', 'MMMLMIV', 'MMMLMV', 'MMMLMVI', 'MMMLMVII', 'MMMLMVIII', 'MMMLMIX', 'MMMLMX', 'MMMLMXI', 'MMMLMXII', 'MMMLMXIII', 'MMMLMXIV', 'MMMLMXV', 'MMMLMXVI', 'MMMLMXVII', 'MMMLMXVIII', 'MMMLMXIX', 'MMMLMXX', 'MMMLMXXI', 'MMMLMXXII', 'MMMLMXXIII', 'MMMLMXXIV', 'MMMLMXXV', 'MMMLMXXVI', 'MMMLMXXVII', 'MMMLMXXVIII', 'MMMLMXXIX', 'MMMLMXXX', 'MMMLMXXXI', 'MMMLMXXXII', 'MMMLMXXXIII', 'MMMLMXXXIV', 'MMMLMXXXV', 'MMMLMXXXVI', 'MMMLMXXXVII', 'MMMLMXXXVIII', 'MMMLMXXXIX', 'MMMLMXL', 'MMMLMXLI', 'MMMLMXLII', 'MMMLMXLIII', 'MMMLMXLIV', 'MMMLMVL', 'MMMLMVLI', 'MMMLMVLII', 'MMMLMVLIII', 'MMMLMVLIV', ] +const mode2 = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI', 'XII', 'XIII', 'XIV', 'XV', 'XVI', 'XVII', 'XVIII', 'XIX', 'XX', 'XXI', 'XXII', 'XXIII', 'XXIV', 'XXV', 'XXVI', 'XXVII', 'XXVIII', 'XXIX', 'XXX', 'XXXI', 'XXXII', 'XXXIII', 'XXXIV', 'XXXV', 'XXXVI', 'XXXVII', 'XXXVIII', 'XXXIX', 'XL', 'XLI', 'XLII', 'XLIII', 'XLIV', 'VL', 'VLI', 'VLII', 'VLIII', 'IL', 'L', 'LI', 'LII', 'LIII', 'LIV', 'LV', 'LVI', 'LVII', 'LVIII', 'LIX', 'LX', 'LXI', 'LXII', 'LXIII', 'LXIV', 'LXV', 'LXVI', 'LXVII', 'LXVIII', 'LXIX', 'LXX', 'LXXI', 'LXXII', 'LXXIII', 'LXXIV', 'LXXV', 'LXXVI', 'LXXVII', 'LXXVIII', 'LXXIX', 'LXXX', 'LXXXI', 'LXXXII', 'LXXXIII', 'LXXXIV', 'LXXXV', 'LXXXVI', 'LXXXVII', 'LXXXVIII', 'LXXXIX', 'XC', 'XCI', 'XCII', 'XCIII', 'XCIV', 'VC', 'VCI', 'VCII', 'VCIII', 'IC', 'C', 'CI', 'CII', 'CIII', 'CIV', 'CV', 'CVI', 'CVII', 'CVIII', 'CIX', 'CX', 'CXI', 'CXII', 'CXIII', 'CXIV', 'CXV', 'CXVI', 'CXVII', 'CXVIII', 'CXIX', 'CXX', 'CXXI', 'CXXII', 'CXXIII', 'CXXIV', 'CXXV', 'CXXVI', 'CXXVII', 'CXXVIII', 'CXXIX', 'CXXX', 'CXXXI', 'CXXXII', 'CXXXIII', 'CXXXIV', 'CXXXV', 'CXXXVI', 'CXXXVII', 'CXXXVIII', 'CXXXIX', 'CXL', 'CXLI', 'CXLII', 'CXLIII', 'CXLIV', 'CVL', 'CVLI', 'CVLII', 'CVLIII', 'CIL', 'CL', 'CLI', 'CLII', 'CLIII', 'CLIV', 'CLV', 'CLVI', 'CLVII', 'CLVIII', 'CLIX', 'CLX', 'CLXI', 'CLXII', 'CLXIII', 'CLXIV', 'CLXV', 'CLXVI', 'CLXVII', 'CLXVIII', 'CLXIX', 'CLXX', 'CLXXI', 'CLXXII', 'CLXXIII', 'CLXXIV', 'CLXXV', 'CLXXVI', 'CLXXVII', 'CLXXVIII', 'CLXXIX', 'CLXXX', 'CLXXXI', 'CLXXXII', 'CLXXXIII', 'CLXXXIV', 'CLXXXV', 'CLXXXVI', 'CLXXXVII', 'CLXXXVIII', 'CLXXXIX', 'CXC', 'CXCI', 'CXCII', 'CXCIII', 'CXCIV', 'CVC', 'CVCI', 'CVCII', 'CVCIII', 'CIC', 'CC', 'CCI', 'CCII', 'CCIII', 'CCIV', 'CCV', 'CCVI', 'CCVII', 'CCVIII', 'CCIX', 'CCX', 'CCXI', 'CCXII', 'CCXIII', 'CCXIV', 'CCXV', 'CCXVI', 'CCXVII', 'CCXVIII', 'CCXIX', 'CCXX', 'CCXXI', 'CCXXII', 'CCXXIII', 'CCXXIV', 'CCXXV', 'CCXXVI', 'CCXXVII', 'CCXXVIII', 'CCXXIX', 'CCXXX', 'CCXXXI', 'CCXXXII', 'CCXXXIII', 'CCXXXIV', 'CCXXXV', 'CCXXXVI', 'CCXXXVII', 'CCXXXVIII', 'CCXXXIX', 'CCXL', 'CCXLI', 'CCXLII', 'CCXLIII', 'CCXLIV', 'CCVL', 'CCVLI', 'CCVLII', 'CCVLIII', 'CCIL', 'CCL', 'CCLI', 'CCLII', 'CCLIII', 'CCLIV', 'CCLV', 'CCLVI', 'CCLVII', 'CCLVIII', 'CCLIX', 'CCLX', 'CCLXI', 'CCLXII', 'CCLXIII', 'CCLXIV', 'CCLXV', 'CCLXVI', 'CCLXVII', 'CCLXVIII', 'CCLXIX', 'CCLXX', 'CCLXXI', 'CCLXXII', 'CCLXXIII', 'CCLXXIV', 'CCLXXV', 'CCLXXVI', 'CCLXXVII', 'CCLXXVIII', 'CCLXXIX', 'CCLXXX', 'CCLXXXI', 'CCLXXXII', 'CCLXXXIII', 'CCLXXXIV', 'CCLXXXV', 'CCLXXXVI', 'CCLXXXVII', 'CCLXXXVIII', 'CCLXXXIX', 'CCXC', 'CCXCI', 'CCXCII', 'CCXCIII', 'CCXCIV', 'CCVC', 'CCVCI', 'CCVCII', 'CCVCIII', 'CCIC', 'CCC', 'CCCI', 'CCCII', 'CCCIII', 'CCCIV', 'CCCV', 'CCCVI', 'CCCVII', 'CCCVIII', 'CCCIX', 'CCCX', 'CCCXI', 'CCCXII', 'CCCXIII', 'CCCXIV', 'CCCXV', 'CCCXVI', 'CCCXVII', 'CCCXVIII', 'CCCXIX', 'CCCXX', 'CCCXXI', 'CCCXXII', 'CCCXXIII', 'CCCXXIV', 'CCCXXV', 'CCCXXVI', 'CCCXXVII', 'CCCXXVIII', 'CCCXXIX', 'CCCXXX', 'CCCXXXI', 'CCCXXXII', 'CCCXXXIII', 'CCCXXXIV', 'CCCXXXV', 'CCCXXXVI', 'CCCXXXVII', 'CCCXXXVIII', 'CCCXXXIX', 'CCCXL', 'CCCXLI', 'CCCXLII', 'CCCXLIII', 'CCCXLIV', 'CCCVL', 'CCCVLI', 'CCCVLII', 'CCCVLIII', 'CCCIL', 'CCCL', 'CCCLI', 'CCCLII', 'CCCLIII', 'CCCLIV', 'CCCLV', 'CCCLVI', 'CCCLVII', 'CCCLVIII', 'CCCLIX', 'CCCLX', 'CCCLXI', 'CCCLXII', 'CCCLXIII', 'CCCLXIV', 'CCCLXV', 'CCCLXVI', 'CCCLXVII', 'CCCLXVIII', 'CCCLXIX', 'CCCLXX', 'CCCLXXI', 'CCCLXXII', 'CCCLXXIII', 'CCCLXXIV', 'CCCLXXV', 'CCCLXXVI', 'CCCLXXVII', 'CCCLXXVIII', 'CCCLXXIX', 'CCCLXXX', 'CCCLXXXI', 'CCCLXXXII', 'CCCLXXXIII', 'CCCLXXXIV', 'CCCLXXXV', 'CCCLXXXVI', 'CCCLXXXVII', 'CCCLXXXVIII', 'CCCLXXXIX', 'CCCXC', 'CCCXCI', 'CCCXCII', 'CCCXCIII', 'CCCXCIV', 'CCCVC', 'CCCVCI', 'CCCVCII', 'CCCVCIII', 'CCCIC', 'CD', 'CDI', 'CDII', 'CDIII', 'CDIV', 'CDV', 'CDVI', 'CDVII', 'CDVIII', 'CDIX', 'CDX', 'CDXI', 'CDXII', 'CDXIII', 'CDXIV', 'CDXV', 'CDXVI', 'CDXVII', 'CDXVIII', 'CDXIX', 'CDXX', 'CDXXI', 'CDXXII', 'CDXXIII', 'CDXXIV', 'CDXXV', 'CDXXVI', 'CDXXVII', 'CDXXVIII', 'CDXXIX', 'CDXXX', 'CDXXXI', 'CDXXXII', 'CDXXXIII', 'CDXXXIV', 'CDXXXV', 'CDXXXVI', 'CDXXXVII', 'CDXXXVIII', 'CDXXXIX', 'CDXL', 'CDXLI', 'CDXLII', 'CDXLIII', 'CDXLIV', 'CDVL', 'CDVLI', 'CDVLII', 'CDVLIII', 'CDIL', 'LD', 'LDI', 'LDII', 'LDIII', 'LDIV', 'LDV', 'LDVI', 'LDVII', 'LDVIII', 'LDIX', 'LDX', 'LDXI', 'LDXII', 'LDXIII', 'LDXIV', 'LDXV', 'LDXVI', 'LDXVII', 'LDXVIII', 'LDXIX', 'LDXX', 'LDXXI', 'LDXXII', 'LDXXIII', 'LDXXIV', 'LDXXV', 'LDXXVI', 'LDXXVII', 'LDXXVIII', 'LDXXIX', 'LDXXX', 'LDXXXI', 'LDXXXII', 'LDXXXIII', 'LDXXXIV', 'LDXXXV', 'LDXXXVI', 'LDXXXVII', 'LDXXXVIII', 'LDXXXIX', 'XD', 'XDI', 'XDII', 'XDIII', 'XDIV', 'XDV', 'XDVI', 'XDVII', 'XDVIII', 'XDIX', 'D', 'DI', 'DII', 'DIII', 'DIV', 'DV', 'DVI', 'DVII', 'DVIII', 'DIX', 'DX', 'DXI', 'DXII', 'DXIII', 'DXIV', 'DXV', 'DXVI', 'DXVII', 'DXVIII', 'DXIX', 'DXX', 'DXXI', 'DXXII', 'DXXIII', 'DXXIV', 'DXXV', 'DXXVI', 'DXXVII', 'DXXVIII', 'DXXIX', 'DXXX', 'DXXXI', 'DXXXII', 'DXXXIII', 'DXXXIV', 'DXXXV', 'DXXXVI', 'DXXXVII', 'DXXXVIII', 'DXXXIX', 'DXL', 'DXLI', 'DXLII', 'DXLIII', 'DXLIV', 'DVL', 'DVLI', 'DVLII', 'DVLIII', 'DIL', 'DL', 'DLI', 'DLII', 'DLIII', 'DLIV', 'DLV', 'DLVI', 'DLVII', 'DLVIII', 'DLIX', 'DLX', 'DLXI', 'DLXII', 'DLXIII', 'DLXIV', 'DLXV', 'DLXVI', 'DLXVII', 'DLXVIII', 'DLXIX', 'DLXX', 'DLXXI', 'DLXXII', 'DLXXIII', 'DLXXIV', 'DLXXV', 'DLXXVI', 'DLXXVII', 'DLXXVIII', 'DLXXIX', 'DLXXX', 'DLXXXI', 'DLXXXII', 'DLXXXIII', 'DLXXXIV', 'DLXXXV', 'DLXXXVI', 'DLXXXVII', 'DLXXXVIII', 'DLXXXIX', 'DXC', 'DXCI', 'DXCII', 'DXCIII', 'DXCIV', 'DVC', 'DVCI', 'DVCII', 'DVCIII', 'DIC', 'DC', 'DCI', 'DCII', 'DCIII', 'DCIV', 'DCV', 'DCVI', 'DCVII', 'DCVIII', 'DCIX', 'DCX', 'DCXI', 'DCXII', 'DCXIII', 'DCXIV', 'DCXV', 'DCXVI', 'DCXVII', 'DCXVIII', 'DCXIX', 'DCXX', 'DCXXI', 'DCXXII', 'DCXXIII', 'DCXXIV', 'DCXXV', 'DCXXVI', 'DCXXVII', 'DCXXVIII', 'DCXXIX', 'DCXXX', 'DCXXXI', 'DCXXXII', 'DCXXXIII', 'DCXXXIV', 'DCXXXV', 'DCXXXVI', 'DCXXXVII', 'DCXXXVIII', 'DCXXXIX', 'DCXL', 'DCXLI', 'DCXLII', 'DCXLIII', 'DCXLIV', 'DCVL', 'DCVLI', 'DCVLII', 'DCVLIII', 'DCIL', 'DCL', 'DCLI', 'DCLII', 'DCLIII', 'DCLIV', 'DCLV', 'DCLVI', 'DCLVII', 'DCLVIII', 'DCLIX', 'DCLX', 'DCLXI', 'DCLXII', 'DCLXIII', 'DCLXIV', 'DCLXV', 'DCLXVI', 'DCLXVII', 'DCLXVIII', 'DCLXIX', 'DCLXX', 'DCLXXI', 'DCLXXII', 'DCLXXIII', 'DCLXXIV', 'DCLXXV', 'DCLXXVI', 'DCLXXVII', 'DCLXXVIII', 'DCLXXIX', 'DCLXXX', 'DCLXXXI', 'DCLXXXII', 'DCLXXXIII', 'DCLXXXIV', 'DCLXXXV', 'DCLXXXVI', 'DCLXXXVII', 'DCLXXXVIII', 'DCLXXXIX', 'DCXC', 'DCXCI', 'DCXCII', 'DCXCIII', 'DCXCIV', 'DCVC', 'DCVCI', 'DCVCII', 'DCVCIII', 'DCIC', 'DCC', 'DCCI', 'DCCII', 'DCCIII', 'DCCIV', 'DCCV', 'DCCVI', 'DCCVII', 'DCCVIII', 'DCCIX', 'DCCX', 'DCCXI', 'DCCXII', 'DCCXIII', 'DCCXIV', 'DCCXV', 'DCCXVI', 'DCCXVII', 'DCCXVIII', 'DCCXIX', 'DCCXX', 'DCCXXI', 'DCCXXII', 'DCCXXIII', 'DCCXXIV', 'DCCXXV', 'DCCXXVI', 'DCCXXVII', 'DCCXXVIII', 'DCCXXIX', 'DCCXXX', 'DCCXXXI', 'DCCXXXII', 'DCCXXXIII', 'DCCXXXIV', 'DCCXXXV', 'DCCXXXVI', 'DCCXXXVII', 'DCCXXXVIII', 'DCCXXXIX', 'DCCXL', 'DCCXLI', 'DCCXLII', 'DCCXLIII', 'DCCXLIV', 'DCCVL', 'DCCVLI', 'DCCVLII', 'DCCVLIII', 'DCCIL', 'DCCL', 'DCCLI', 'DCCLII', 'DCCLIII', 'DCCLIV', 'DCCLV', 'DCCLVI', 'DCCLVII', 'DCCLVIII', 'DCCLIX', 'DCCLX', 'DCCLXI', 'DCCLXII', 'DCCLXIII', 'DCCLXIV', 'DCCLXV', 'DCCLXVI', 'DCCLXVII', 'DCCLXVIII', 'DCCLXIX', 'DCCLXX', 'DCCLXXI', 'DCCLXXII', 'DCCLXXIII', 'DCCLXXIV', 'DCCLXXV', 'DCCLXXVI', 'DCCLXXVII', 'DCCLXXVIII', 'DCCLXXIX', 'DCCLXXX', 'DCCLXXXI', 'DCCLXXXII', 'DCCLXXXIII', 'DCCLXXXIV', 'DCCLXXXV', 'DCCLXXXVI', 'DCCLXXXVII', 'DCCLXXXVIII', 'DCCLXXXIX', 'DCCXC', 'DCCXCI', 'DCCXCII', 'DCCXCIII', 'DCCXCIV', 'DCCVC', 'DCCVCI', 'DCCVCII', 'DCCVCIII', 'DCCIC', 'DCCC', 'DCCCI', 'DCCCII', 'DCCCIII', 'DCCCIV', 'DCCCV', 'DCCCVI', 'DCCCVII', 'DCCCVIII', 'DCCCIX', 'DCCCX', 'DCCCXI', 'DCCCXII', 'DCCCXIII', 'DCCCXIV', 'DCCCXV', 'DCCCXVI', 'DCCCXVII', 'DCCCXVIII', 'DCCCXIX', 'DCCCXX', 'DCCCXXI', 'DCCCXXII', 'DCCCXXIII', 'DCCCXXIV', 'DCCCXXV', 'DCCCXXVI', 'DCCCXXVII', 'DCCCXXVIII', 'DCCCXXIX', 'DCCCXXX', 'DCCCXXXI', 'DCCCXXXII', 'DCCCXXXIII', 'DCCCXXXIV', 'DCCCXXXV', 'DCCCXXXVI', 'DCCCXXXVII', 'DCCCXXXVIII', 'DCCCXXXIX', 'DCCCXL', 'DCCCXLI', 'DCCCXLII', 'DCCCXLIII', 'DCCCXLIV', 'DCCCVL', 'DCCCVLI', 'DCCCVLII', 'DCCCVLIII', 'DCCCIL', 'DCCCL', 'DCCCLI', 'DCCCLII', 'DCCCLIII', 'DCCCLIV', 'DCCCLV', 'DCCCLVI', 'DCCCLVII', 'DCCCLVIII', 'DCCCLIX', 'DCCCLX', 'DCCCLXI', 'DCCCLXII', 'DCCCLXIII', 'DCCCLXIV', 'DCCCLXV', 'DCCCLXVI', 'DCCCLXVII', 'DCCCLXVIII', 'DCCCLXIX', 'DCCCLXX', 'DCCCLXXI', 'DCCCLXXII', 'DCCCLXXIII', 'DCCCLXXIV', 'DCCCLXXV', 'DCCCLXXVI', 'DCCCLXXVII', 'DCCCLXXVIII', 'DCCCLXXIX', 'DCCCLXXX', 'DCCCLXXXI', 'DCCCLXXXII', 'DCCCLXXXIII', 'DCCCLXXXIV', 'DCCCLXXXV', 'DCCCLXXXVI', 'DCCCLXXXVII', 'DCCCLXXXVIII', 'DCCCLXXXIX', 'DCCCXC', 'DCCCXCI', 'DCCCXCII', 'DCCCXCIII', 'DCCCXCIV', 'DCCCVC', 'DCCCVCI', 'DCCCVCII', 'DCCCVCIII', 'DCCCIC', 'CM', 'CMI', 'CMII', 'CMIII', 'CMIV', 'CMV', 'CMVI', 'CMVII', 'CMVIII', 'CMIX', 'CMX', 'CMXI', 'CMXII', 'CMXIII', 'CMXIV', 'CMXV', 'CMXVI', 'CMXVII', 'CMXVIII', 'CMXIX', 'CMXX', 'CMXXI', 'CMXXII', 'CMXXIII', 'CMXXIV', 'CMXXV', 'CMXXVI', 'CMXXVII', 'CMXXVIII', 'CMXXIX', 'CMXXX', 'CMXXXI', 'CMXXXII', 'CMXXXIII', 'CMXXXIV', 'CMXXXV', 'CMXXXVI', 'CMXXXVII', 'CMXXXVIII', 'CMXXXIX', 'CMXL', 'CMXLI', 'CMXLII', 'CMXLIII', 'CMXLIV', 'CMVL', 'CMVLI', 'CMVLII', 'CMVLIII', 'CMIL', 'LM', 'LMI', 'LMII', 'LMIII', 'LMIV', 'LMV', 'LMVI', 'LMVII', 'LMVIII', 'LMIX', 'LMX', 'LMXI', 'LMXII', 'LMXIII', 'LMXIV', 'LMXV', 'LMXVI', 'LMXVII', 'LMXVIII', 'LMXIX', 'LMXX', 'LMXXI', 'LMXXII', 'LMXXIII', 'LMXXIV', 'LMXXV', 'LMXXVI', 'LMXXVII', 'LMXXVIII', 'LMXXIX', 'LMXXX', 'LMXXXI', 'LMXXXII', 'LMXXXIII', 'LMXXXIV', 'LMXXXV', 'LMXXXVI', 'LMXXXVII', 'LMXXXVIII', 'LMXXXIX', 'XM', 'XMI', 'XMII', 'XMIII', 'XMIV', 'XMV', 'XMVI', 'XMVII', 'XMVIII', 'XMIX', 'M', 'MI', 'MII', 'MIII', 'MIV', 'MV', 'MVI', 'MVII', 'MVIII', 'MIX', 'MX', 'MXI', 'MXII', 'MXIII', 'MXIV', 'MXV', 'MXVI', 'MXVII', 'MXVIII', 'MXIX', 'MXX', 'MXXI', 'MXXII', 'MXXIII', 'MXXIV', 'MXXV', 'MXXVI', 'MXXVII', 'MXXVIII', 'MXXIX', 'MXXX', 'MXXXI', 'MXXXII', 'MXXXIII', 'MXXXIV', 'MXXXV', 'MXXXVI', 'MXXXVII', 'MXXXVIII', 'MXXXIX', 'MXL', 'MXLI', 'MXLII', 'MXLIII', 'MXLIV', 'MVL', 'MVLI', 'MVLII', 'MVLIII', 'MIL', 'ML', 'MLI', 'MLII', 'MLIII', 'MLIV', 'MLV', 'MLVI', 'MLVII', 'MLVIII', 'MLIX', 'MLX', 'MLXI', 'MLXII', 'MLXIII', 'MLXIV', 'MLXV', 'MLXVI', 'MLXVII', 'MLXVIII', 'MLXIX', 'MLXX', 'MLXXI', 'MLXXII', 'MLXXIII', 'MLXXIV', 'MLXXV', 'MLXXVI', 'MLXXVII', 'MLXXVIII', 'MLXXIX', 'MLXXX', 'MLXXXI', 'MLXXXII', 'MLXXXIII', 'MLXXXIV', 'MLXXXV', 'MLXXXVI', 'MLXXXVII', 'MLXXXVIII', 'MLXXXIX', 'MXC', 'MXCI', 'MXCII', 'MXCIII', 'MXCIV', 'MVC', 'MVCI', 'MVCII', 'MVCIII', 'MIC', 'MC', 'MCI', 'MCII', 'MCIII', 'MCIV', 'MCV', 'MCVI', 'MCVII', 'MCVIII', 'MCIX', 'MCX', 'MCXI', 'MCXII', 'MCXIII', 'MCXIV', 'MCXV', 'MCXVI', 'MCXVII', 'MCXVIII', 'MCXIX', 'MCXX', 'MCXXI', 'MCXXII', 'MCXXIII', 'MCXXIV', 'MCXXV', 'MCXXVI', 'MCXXVII', 'MCXXVIII', 'MCXXIX', 'MCXXX', 'MCXXXI', 'MCXXXII', 'MCXXXIII', 'MCXXXIV', 'MCXXXV', 'MCXXXVI', 'MCXXXVII', 'MCXXXVIII', 'MCXXXIX', 'MCXL', 'MCXLI', 'MCXLII', 'MCXLIII', 'MCXLIV', 'MCVL', 'MCVLI', 'MCVLII', 'MCVLIII', 'MCIL', 'MCL', 'MCLI', 'MCLII', 'MCLIII', 'MCLIV', 'MCLV', 'MCLVI', 'MCLVII', 'MCLVIII', 'MCLIX', 'MCLX', 'MCLXI', 'MCLXII', 'MCLXIII', 'MCLXIV', 'MCLXV', 'MCLXVI', 'MCLXVII', 'MCLXVIII', 'MCLXIX', 'MCLXX', 'MCLXXI', 'MCLXXII', 'MCLXXIII', 'MCLXXIV', 'MCLXXV', 'MCLXXVI', 'MCLXXVII', 'MCLXXVIII', 'MCLXXIX', 'MCLXXX', 'MCLXXXI', 'MCLXXXII', 'MCLXXXIII', 'MCLXXXIV', 'MCLXXXV', 'MCLXXXVI', 'MCLXXXVII', 'MCLXXXVIII', 'MCLXXXIX', 'MCXC', 'MCXCI', 'MCXCII', 'MCXCIII', 'MCXCIV', 'MCVC', 'MCVCI', 'MCVCII', 'MCVCIII', 'MCIC', 'MCC', 'MCCI', 'MCCII', 'MCCIII', 'MCCIV', 'MCCV', 'MCCVI', 'MCCVII', 'MCCVIII', 'MCCIX', 'MCCX', 'MCCXI', 'MCCXII', 'MCCXIII', 'MCCXIV', 'MCCXV', 'MCCXVI', 'MCCXVII', 'MCCXVIII', 'MCCXIX', 'MCCXX', 'MCCXXI', 'MCCXXII', 'MCCXXIII', 'MCCXXIV', 'MCCXXV', 'MCCXXVI', 'MCCXXVII', 'MCCXXVIII', 'MCCXXIX', 'MCCXXX', 'MCCXXXI', 'MCCXXXII', 'MCCXXXIII', 'MCCXXXIV', 'MCCXXXV', 'MCCXXXVI', 'MCCXXXVII', 'MCCXXXVIII', 'MCCXXXIX', 'MCCXL', 'MCCXLI', 'MCCXLII', 'MCCXLIII', 'MCCXLIV', 'MCCVL', 'MCCVLI', 'MCCVLII', 'MCCVLIII', 'MCCIL', 'MCCL', 'MCCLI', 'MCCLII', 'MCCLIII', 'MCCLIV', 'MCCLV', 'MCCLVI', 'MCCLVII', 'MCCLVIII', 'MCCLIX', 'MCCLX', 'MCCLXI', 'MCCLXII', 'MCCLXIII', 'MCCLXIV', 'MCCLXV', 'MCCLXVI', 'MCCLXVII', 'MCCLXVIII', 'MCCLXIX', 'MCCLXX', 'MCCLXXI', 'MCCLXXII', 'MCCLXXIII', 'MCCLXXIV', 'MCCLXXV', 'MCCLXXVI', 'MCCLXXVII', 'MCCLXXVIII', 'MCCLXXIX', 'MCCLXXX', 'MCCLXXXI', 'MCCLXXXII', 'MCCLXXXIII', 'MCCLXXXIV', 'MCCLXXXV', 'MCCLXXXVI', 'MCCLXXXVII', 'MCCLXXXVIII', 'MCCLXXXIX', 'MCCXC', 'MCCXCI', 'MCCXCII', 'MCCXCIII', 'MCCXCIV', 'MCCVC', 'MCCVCI', 'MCCVCII', 'MCCVCIII', 'MCCIC', 'MCCC', 'MCCCI', 'MCCCII', 'MCCCIII', 'MCCCIV', 'MCCCV', 'MCCCVI', 'MCCCVII', 'MCCCVIII', 'MCCCIX', 'MCCCX', 'MCCCXI', 'MCCCXII', 'MCCCXIII', 'MCCCXIV', 'MCCCXV', 'MCCCXVI', 'MCCCXVII', 'MCCCXVIII', 'MCCCXIX', 'MCCCXX', 'MCCCXXI', 'MCCCXXII', 'MCCCXXIII', 'MCCCXXIV', 'MCCCXXV', 'MCCCXXVI', 'MCCCXXVII', 'MCCCXXVIII', 'MCCCXXIX', 'MCCCXXX', 'MCCCXXXI', 'MCCCXXXII', 'MCCCXXXIII', 'MCCCXXXIV', 'MCCCXXXV', 'MCCCXXXVI', 'MCCCXXXVII', 'MCCCXXXVIII', 'MCCCXXXIX', 'MCCCXL', 'MCCCXLI', 'MCCCXLII', 'MCCCXLIII', 'MCCCXLIV', 'MCCCVL', 'MCCCVLI', 'MCCCVLII', 'MCCCVLIII', 'MCCCIL', 'MCCCL', 'MCCCLI', 'MCCCLII', 'MCCCLIII', 'MCCCLIV', 'MCCCLV', 'MCCCLVI', 'MCCCLVII', 'MCCCLVIII', 'MCCCLIX', 'MCCCLX', 'MCCCLXI', 'MCCCLXII', 'MCCCLXIII', 'MCCCLXIV', 'MCCCLXV', 'MCCCLXVI', 'MCCCLXVII', 'MCCCLXVIII', 'MCCCLXIX', 'MCCCLXX', 'MCCCLXXI', 'MCCCLXXII', 'MCCCLXXIII', 'MCCCLXXIV', 'MCCCLXXV', 'MCCCLXXVI', 'MCCCLXXVII', 'MCCCLXXVIII', 'MCCCLXXIX', 'MCCCLXXX', 'MCCCLXXXI', 'MCCCLXXXII', 'MCCCLXXXIII', 'MCCCLXXXIV', 'MCCCLXXXV', 'MCCCLXXXVI', 'MCCCLXXXVII', 'MCCCLXXXVIII', 'MCCCLXXXIX', 'MCCCXC', 'MCCCXCI', 'MCCCXCII', 'MCCCXCIII', 'MCCCXCIV', 'MCCCVC', 'MCCCVCI', 'MCCCVCII', 'MCCCVCIII', 'MCCCIC', 'MCD', 'MCDI', 'MCDII', 'MCDIII', 'MCDIV', 'MCDV', 'MCDVI', 'MCDVII', 'MCDVIII', 'MCDIX', 'MCDX', 'MCDXI', 'MCDXII', 'MCDXIII', 'MCDXIV', 'MCDXV', 'MCDXVI', 'MCDXVII', 'MCDXVIII', 'MCDXIX', 'MCDXX', 'MCDXXI', 'MCDXXII', 'MCDXXIII', 'MCDXXIV', 'MCDXXV', 'MCDXXVI', 'MCDXXVII', 'MCDXXVIII', 'MCDXXIX', 'MCDXXX', 'MCDXXXI', 'MCDXXXII', 'MCDXXXIII', 'MCDXXXIV', 'MCDXXXV', 'MCDXXXVI', 'MCDXXXVII', 'MCDXXXVIII', 'MCDXXXIX', 'MCDXL', 'MCDXLI', 'MCDXLII', 'MCDXLIII', 'MCDXLIV', 'MCDVL', 'MCDVLI', 'MCDVLII', 'MCDVLIII', 'MCDIL', 'MLD', 'MLDI', 'MLDII', 'MLDIII', 'MLDIV', 'MLDV', 'MLDVI', 'MLDVII', 'MLDVIII', 'MLDIX', 'MLDX', 'MLDXI', 'MLDXII', 'MLDXIII', 'MLDXIV', 'MLDXV', 'MLDXVI', 'MLDXVII', 'MLDXVIII', 'MLDXIX', 'MLDXX', 'MLDXXI', 'MLDXXII', 'MLDXXIII', 'MLDXXIV', 'MLDXXV', 'MLDXXVI', 'MLDXXVII', 'MLDXXVIII', 'MLDXXIX', 'MLDXXX', 'MLDXXXI', 'MLDXXXII', 'MLDXXXIII', 'MLDXXXIV', 'MLDXXXV', 'MLDXXXVI', 'MLDXXXVII', 'MLDXXXVIII', 'MLDXXXIX', 'MXD', 'MXDI', 'MXDII', 'MXDIII', 'MXDIV', 'MXDV', 'MXDVI', 'MXDVII', 'MXDVIII', 'MXDIX', 'MD', 'MDI', 'MDII', 'MDIII', 'MDIV', 'MDV', 'MDVI', 'MDVII', 'MDVIII', 'MDIX', 'MDX', 'MDXI', 'MDXII', 'MDXIII', 'MDXIV', 'MDXV', 'MDXVI', 'MDXVII', 'MDXVIII', 'MDXIX', 'MDXX', 'MDXXI', 'MDXXII', 'MDXXIII', 'MDXXIV', 'MDXXV', 'MDXXVI', 'MDXXVII', 'MDXXVIII', 'MDXXIX', 'MDXXX', 'MDXXXI', 'MDXXXII', 'MDXXXIII', 'MDXXXIV', 'MDXXXV', 'MDXXXVI', 'MDXXXVII', 'MDXXXVIII', 'MDXXXIX', 'MDXL', 'MDXLI', 'MDXLII', 'MDXLIII', 'MDXLIV', 'MDVL', 'MDVLI', 'MDVLII', 'MDVLIII', 'MDIL', 'MDL', 'MDLI', 'MDLII', 'MDLIII', 'MDLIV', 'MDLV', 'MDLVI', 'MDLVII', 'MDLVIII', 'MDLIX', 'MDLX', 'MDLXI', 'MDLXII', 'MDLXIII', 'MDLXIV', 'MDLXV', 'MDLXVI', 'MDLXVII', 'MDLXVIII', 'MDLXIX', 'MDLXX', 'MDLXXI', 'MDLXXII', 'MDLXXIII', 'MDLXXIV', 'MDLXXV', 'MDLXXVI', 'MDLXXVII', 'MDLXXVIII', 'MDLXXIX', 'MDLXXX', 'MDLXXXI', 'MDLXXXII', 'MDLXXXIII', 'MDLXXXIV', 'MDLXXXV', 'MDLXXXVI', 'MDLXXXVII', 'MDLXXXVIII', 'MDLXXXIX', 'MDXC', 'MDXCI', 'MDXCII', 'MDXCIII', 'MDXCIV', 'MDVC', 'MDVCI', 'MDVCII', 'MDVCIII', 'MDIC', 'MDC', 'MDCI', 'MDCII', 'MDCIII', 'MDCIV', 'MDCV', 'MDCVI', 'MDCVII', 'MDCVIII', 'MDCIX', 'MDCX', 'MDCXI', 'MDCXII', 'MDCXIII', 'MDCXIV', 'MDCXV', 'MDCXVI', 'MDCXVII', 'MDCXVIII', 'MDCXIX', 'MDCXX', 'MDCXXI', 'MDCXXII', 'MDCXXIII', 'MDCXXIV', 'MDCXXV', 'MDCXXVI', 'MDCXXVII', 'MDCXXVIII', 'MDCXXIX', 'MDCXXX', 'MDCXXXI', 'MDCXXXII', 'MDCXXXIII', 'MDCXXXIV', 'MDCXXXV', 'MDCXXXVI', 'MDCXXXVII', 'MDCXXXVIII', 'MDCXXXIX', 'MDCXL', 'MDCXLI', 'MDCXLII', 'MDCXLIII', 'MDCXLIV', 'MDCVL', 'MDCVLI', 'MDCVLII', 'MDCVLIII', 'MDCIL', 'MDCL', 'MDCLI', 'MDCLII', 'MDCLIII', 'MDCLIV', 'MDCLV', 'MDCLVI', 'MDCLVII', 'MDCLVIII', 'MDCLIX', 'MDCLX', 'MDCLXI', 'MDCLXII', 'MDCLXIII', 'MDCLXIV', 'MDCLXV', 'MDCLXVI', 'MDCLXVII', 'MDCLXVIII', 'MDCLXIX', 'MDCLXX', 'MDCLXXI', 'MDCLXXII', 'MDCLXXIII', 'MDCLXXIV', 'MDCLXXV', 'MDCLXXVI', 'MDCLXXVII', 'MDCLXXVIII', 'MDCLXXIX', 'MDCLXXX', 'MDCLXXXI', 'MDCLXXXII', 'MDCLXXXIII', 'MDCLXXXIV', 'MDCLXXXV', 'MDCLXXXVI', 'MDCLXXXVII', 'MDCLXXXVIII', 'MDCLXXXIX', 'MDCXC', 'MDCXCI', 'MDCXCII', 'MDCXCIII', 'MDCXCIV', 'MDCVC', 'MDCVCI', 'MDCVCII', 'MDCVCIII', 'MDCIC', 'MDCC', 'MDCCI', 'MDCCII', 'MDCCIII', 'MDCCIV', 'MDCCV', 'MDCCVI', 'MDCCVII', 'MDCCVIII', 'MDCCIX', 'MDCCX', 'MDCCXI', 'MDCCXII', 'MDCCXIII', 'MDCCXIV', 'MDCCXV', 'MDCCXVI', 'MDCCXVII', 'MDCCXVIII', 'MDCCXIX', 'MDCCXX', 'MDCCXXI', 'MDCCXXII', 'MDCCXXIII', 'MDCCXXIV', 'MDCCXXV', 'MDCCXXVI', 'MDCCXXVII', 'MDCCXXVIII', 'MDCCXXIX', 'MDCCXXX', 'MDCCXXXI', 'MDCCXXXII', 'MDCCXXXIII', 'MDCCXXXIV', 'MDCCXXXV', 'MDCCXXXVI', 'MDCCXXXVII', 'MDCCXXXVIII', 'MDCCXXXIX', 'MDCCXL', 'MDCCXLI', 'MDCCXLII', 'MDCCXLIII', 'MDCCXLIV', 'MDCCVL', 'MDCCVLI', 'MDCCVLII', 'MDCCVLIII', 'MDCCIL', 'MDCCL', 'MDCCLI', 'MDCCLII', 'MDCCLIII', 'MDCCLIV', 'MDCCLV', 'MDCCLVI', 'MDCCLVII', 'MDCCLVIII', 'MDCCLIX', 'MDCCLX', 'MDCCLXI', 'MDCCLXII', 'MDCCLXIII', 'MDCCLXIV', 'MDCCLXV', 'MDCCLXVI', 'MDCCLXVII', 'MDCCLXVIII', 'MDCCLXIX', 'MDCCLXX', 'MDCCLXXI', 'MDCCLXXII', 'MDCCLXXIII', 'MDCCLXXIV', 'MDCCLXXV', 'MDCCLXXVI', 'MDCCLXXVII', 'MDCCLXXVIII', 'MDCCLXXIX', 'MDCCLXXX', 'MDCCLXXXI', 'MDCCLXXXII', 'MDCCLXXXIII', 'MDCCLXXXIV', 'MDCCLXXXV', 'MDCCLXXXVI', 'MDCCLXXXVII', 'MDCCLXXXVIII', 'MDCCLXXXIX', 'MDCCXC', 'MDCCXCI', 'MDCCXCII', 'MDCCXCIII', 'MDCCXCIV', 'MDCCVC', 'MDCCVCI', 'MDCCVCII', 'MDCCVCIII', 'MDCCIC', 'MDCCC', 'MDCCCI', 'MDCCCII', 'MDCCCIII', 'MDCCCIV', 'MDCCCV', 'MDCCCVI', 'MDCCCVII', 'MDCCCVIII', 'MDCCCIX', 'MDCCCX', 'MDCCCXI', 'MDCCCXII', 'MDCCCXIII', 'MDCCCXIV', 'MDCCCXV', 'MDCCCXVI', 'MDCCCXVII', 'MDCCCXVIII', 'MDCCCXIX', 'MDCCCXX', 'MDCCCXXI', 'MDCCCXXII', 'MDCCCXXIII', 'MDCCCXXIV', 'MDCCCXXV', 'MDCCCXXVI', 'MDCCCXXVII', 'MDCCCXXVIII', 'MDCCCXXIX', 'MDCCCXXX', 'MDCCCXXXI', 'MDCCCXXXII', 'MDCCCXXXIII', 'MDCCCXXXIV', 'MDCCCXXXV', 'MDCCCXXXVI', 'MDCCCXXXVII', 'MDCCCXXXVIII', 'MDCCCXXXIX', 'MDCCCXL', 'MDCCCXLI', 'MDCCCXLII', 'MDCCCXLIII', 'MDCCCXLIV', 'MDCCCVL', 'MDCCCVLI', 'MDCCCVLII', 'MDCCCVLIII', 'MDCCCIL', 'MDCCCL', 'MDCCCLI', 'MDCCCLII', 'MDCCCLIII', 'MDCCCLIV', 'MDCCCLV', 'MDCCCLVI', 'MDCCCLVII', 'MDCCCLVIII', 'MDCCCLIX', 'MDCCCLX', 'MDCCCLXI', 'MDCCCLXII', 'MDCCCLXIII', 'MDCCCLXIV', 'MDCCCLXV', 'MDCCCLXVI', 'MDCCCLXVII', 'MDCCCLXVIII', 'MDCCCLXIX', 'MDCCCLXX', 'MDCCCLXXI', 'MDCCCLXXII', 'MDCCCLXXIII', 'MDCCCLXXIV', 'MDCCCLXXV', 'MDCCCLXXVI', 'MDCCCLXXVII', 'MDCCCLXXVIII', 'MDCCCLXXIX', 'MDCCCLXXX', 'MDCCCLXXXI', 'MDCCCLXXXII', 'MDCCCLXXXIII', 'MDCCCLXXXIV', 'MDCCCLXXXV', 'MDCCCLXXXVI', 'MDCCCLXXXVII', 'MDCCCLXXXVIII', 'MDCCCLXXXIX', 'MDCCCXC', 'MDCCCXCI', 'MDCCCXCII', 'MDCCCXCIII', 'MDCCCXCIV', 'MDCCCVC', 'MDCCCVCI', 'MDCCCVCII', 'MDCCCVCIII', 'MDCCCIC', 'MCM', 'MCMI', 'MCMII', 'MCMIII', 'MCMIV', 'MCMV', 'MCMVI', 'MCMVII', 'MCMVIII', 'MCMIX', 'MCMX', 'MCMXI', 'MCMXII', 'MCMXIII', 'MCMXIV', 'MCMXV', 'MCMXVI', 'MCMXVII', 'MCMXVIII', 'MCMXIX', 'MCMXX', 'MCMXXI', 'MCMXXII', 'MCMXXIII', 'MCMXXIV', 'MCMXXV', 'MCMXXVI', 'MCMXXVII', 'MCMXXVIII', 'MCMXXIX', 'MCMXXX', 'MCMXXXI', 'MCMXXXII', 'MCMXXXIII', 'MCMXXXIV', 'MCMXXXV', 'MCMXXXVI', 'MCMXXXVII', 'MCMXXXVIII', 'MCMXXXIX', 'MCMXL', 'MCMXLI', 'MCMXLII', 'MCMXLIII', 'MCMXLIV', 'MCMVL', 'MCMVLI', 'MCMVLII', 'MCMVLIII', 'MCMIL', 'MLM', 'MLMI', 'MLMII', 'MLMIII', 'MLMIV', 'MLMV', 'MLMVI', 'MLMVII', 'MLMVIII', 'MLMIX', 'MLMX', 'MLMXI', 'MLMXII', 'MLMXIII', 'MLMXIV', 'MLMXV', 'MLMXVI', 'MLMXVII', 'MLMXVIII', 'MLMXIX', 'MLMXX', 'MLMXXI', 'MLMXXII', 'MLMXXIII', 'MLMXXIV', 'MLMXXV', 'MLMXXVI', 'MLMXXVII', 'MLMXXVIII', 'MLMXXIX', 'MLMXXX', 'MLMXXXI', 'MLMXXXII', 'MLMXXXIII', 'MLMXXXIV', 'MLMXXXV', 'MLMXXXVI', 'MLMXXXVII', 'MLMXXXVIII', 'MLMXXXIX', 'MXM', 'MXMI', 'MXMII', 'MXMIII', 'MXMIV', 'MXMV', 'MXMVI', 'MXMVII', 'MXMVIII', 'MXMIX', 'MM', 'MMI', 'MMII', 'MMIII', 'MMIV', 'MMV', 'MMVI', 'MMVII', 'MMVIII', 'MMIX', 'MMX', 'MMXI', 'MMXII', 'MMXIII', 'MMXIV', 'MMXV', 'MMXVI', 'MMXVII', 'MMXVIII', 'MMXIX', 'MMXX', 'MMXXI', 'MMXXII', 'MMXXIII', 'MMXXIV', 'MMXXV', 'MMXXVI', 'MMXXVII', 'MMXXVIII', 'MMXXIX', 'MMXXX', 'MMXXXI', 'MMXXXII', 'MMXXXIII', 'MMXXXIV', 'MMXXXV', 'MMXXXVI', 'MMXXXVII', 'MMXXXVIII', 'MMXXXIX', 'MMXL', 'MMXLI', 'MMXLII', 'MMXLIII', 'MMXLIV', 'MMVL', 'MMVLI', 'MMVLII', 'MMVLIII', 'MMIL', 'MML', 'MMLI', 'MMLII', 'MMLIII', 'MMLIV', 'MMLV', 'MMLVI', 'MMLVII', 'MMLVIII', 'MMLIX', 'MMLX', 'MMLXI', 'MMLXII', 'MMLXIII', 'MMLXIV', 'MMLXV', 'MMLXVI', 'MMLXVII', 'MMLXVIII', 'MMLXIX', 'MMLXX', 'MMLXXI', 'MMLXXII', 'MMLXXIII', 'MMLXXIV', 'MMLXXV', 'MMLXXVI', 'MMLXXVII', 'MMLXXVIII', 'MMLXXIX', 'MMLXXX', 'MMLXXXI', 'MMLXXXII', 'MMLXXXIII', 'MMLXXXIV', 'MMLXXXV', 'MMLXXXVI', 'MMLXXXVII', 'MMLXXXVIII', 'MMLXXXIX', 'MMXC', 'MMXCI', 'MMXCII', 'MMXCIII', 'MMXCIV', 'MMVC', 'MMVCI', 'MMVCII', 'MMVCIII', 'MMIC', 'MMC', 'MMCI', 'MMCII', 'MMCIII', 'MMCIV', 'MMCV', 'MMCVI', 'MMCVII', 'MMCVIII', 'MMCIX', 'MMCX', 'MMCXI', 'MMCXII', 'MMCXIII', 'MMCXIV', 'MMCXV', 'MMCXVI', 'MMCXVII', 'MMCXVIII', 'MMCXIX', 'MMCXX', 'MMCXXI', 'MMCXXII', 'MMCXXIII', 'MMCXXIV', 'MMCXXV', 'MMCXXVI', 'MMCXXVII', 'MMCXXVIII', 'MMCXXIX', 'MMCXXX', 'MMCXXXI', 'MMCXXXII', 'MMCXXXIII', 'MMCXXXIV', 'MMCXXXV', 'MMCXXXVI', 'MMCXXXVII', 'MMCXXXVIII', 'MMCXXXIX', 'MMCXL', 'MMCXLI', 'MMCXLII', 'MMCXLIII', 'MMCXLIV', 'MMCVL', 'MMCVLI', 'MMCVLII', 'MMCVLIII', 'MMCIL', 'MMCL', 'MMCLI', 'MMCLII', 'MMCLIII', 'MMCLIV', 'MMCLV', 'MMCLVI', 'MMCLVII', 'MMCLVIII', 'MMCLIX', 'MMCLX', 'MMCLXI', 'MMCLXII', 'MMCLXIII', 'MMCLXIV', 'MMCLXV', 'MMCLXVI', 'MMCLXVII', 'MMCLXVIII', 'MMCLXIX', 'MMCLXX', 'MMCLXXI', 'MMCLXXII', 'MMCLXXIII', 'MMCLXXIV', 'MMCLXXV', 'MMCLXXVI', 'MMCLXXVII', 'MMCLXXVIII', 'MMCLXXIX', 'MMCLXXX', 'MMCLXXXI', 'MMCLXXXII', 'MMCLXXXIII', 'MMCLXXXIV', 'MMCLXXXV', 'MMCLXXXVI', 'MMCLXXXVII', 'MMCLXXXVIII', 'MMCLXXXIX', 'MMCXC', 'MMCXCI', 'MMCXCII', 'MMCXCIII', 'MMCXCIV', 'MMCVC', 'MMCVCI', 'MMCVCII', 'MMCVCIII', 'MMCIC', 'MMCC', 'MMCCI', 'MMCCII', 'MMCCIII', 'MMCCIV', 'MMCCV', 'MMCCVI', 'MMCCVII', 'MMCCVIII', 'MMCCIX', 'MMCCX', 'MMCCXI', 'MMCCXII', 'MMCCXIII', 'MMCCXIV', 'MMCCXV', 'MMCCXVI', 'MMCCXVII', 'MMCCXVIII', 'MMCCXIX', 'MMCCXX', 'MMCCXXI', 'MMCCXXII', 'MMCCXXIII', 'MMCCXXIV', 'MMCCXXV', 'MMCCXXVI', 'MMCCXXVII', 'MMCCXXVIII', 'MMCCXXIX', 'MMCCXXX', 'MMCCXXXI', 'MMCCXXXII', 'MMCCXXXIII', 'MMCCXXXIV', 'MMCCXXXV', 'MMCCXXXVI', 'MMCCXXXVII', 'MMCCXXXVIII', 'MMCCXXXIX', 'MMCCXL', 'MMCCXLI', 'MMCCXLII', 'MMCCXLIII', 'MMCCXLIV', 'MMCCVL', 'MMCCVLI', 'MMCCVLII', 'MMCCVLIII', 'MMCCIL', 'MMCCL', 'MMCCLI', 'MMCCLII', 'MMCCLIII', 'MMCCLIV', 'MMCCLV', 'MMCCLVI', 'MMCCLVII', 'MMCCLVIII', 'MMCCLIX', 'MMCCLX', 'MMCCLXI', 'MMCCLXII', 'MMCCLXIII', 'MMCCLXIV', 'MMCCLXV', 'MMCCLXVI', 'MMCCLXVII', 'MMCCLXVIII', 'MMCCLXIX', 'MMCCLXX', 'MMCCLXXI', 'MMCCLXXII', 'MMCCLXXIII', 'MMCCLXXIV', 'MMCCLXXV', 'MMCCLXXVI', 'MMCCLXXVII', 'MMCCLXXVIII', 'MMCCLXXIX', 'MMCCLXXX', 'MMCCLXXXI', 'MMCCLXXXII', 'MMCCLXXXIII', 'MMCCLXXXIV', 'MMCCLXXXV', 'MMCCLXXXVI', 'MMCCLXXXVII', 'MMCCLXXXVIII', 'MMCCLXXXIX', 'MMCCXC', 'MMCCXCI', 'MMCCXCII', 'MMCCXCIII', 'MMCCXCIV', 'MMCCVC', 'MMCCVCI', 'MMCCVCII', 'MMCCVCIII', 'MMCCIC', 'MMCCC', 'MMCCCI', 'MMCCCII', 'MMCCCIII', 'MMCCCIV', 'MMCCCV', 'MMCCCVI', 'MMCCCVII', 'MMCCCVIII', 'MMCCCIX', 'MMCCCX', 'MMCCCXI', 'MMCCCXII', 'MMCCCXIII', 'MMCCCXIV', 'MMCCCXV', 'MMCCCXVI', 'MMCCCXVII', 'MMCCCXVIII', 'MMCCCXIX', 'MMCCCXX', 'MMCCCXXI', 'MMCCCXXII', 'MMCCCXXIII', 'MMCCCXXIV', 'MMCCCXXV', 'MMCCCXXVI', 'MMCCCXXVII', 'MMCCCXXVIII', 'MMCCCXXIX', 'MMCCCXXX', 'MMCCCXXXI', 'MMCCCXXXII', 'MMCCCXXXIII', 'MMCCCXXXIV', 'MMCCCXXXV', 'MMCCCXXXVI', 'MMCCCXXXVII', 'MMCCCXXXVIII', 'MMCCCXXXIX', 'MMCCCXL', 'MMCCCXLI', 'MMCCCXLII', 'MMCCCXLIII', 'MMCCCXLIV', 'MMCCCVL', 'MMCCCVLI', 'MMCCCVLII', 'MMCCCVLIII', 'MMCCCIL', 'MMCCCL', 'MMCCCLI', 'MMCCCLII', 'MMCCCLIII', 'MMCCCLIV', 'MMCCCLV', 'MMCCCLVI', 'MMCCCLVII', 'MMCCCLVIII', 'MMCCCLIX', 'MMCCCLX', 'MMCCCLXI', 'MMCCCLXII', 'MMCCCLXIII', 'MMCCCLXIV', 'MMCCCLXV', 'MMCCCLXVI', 'MMCCCLXVII', 'MMCCCLXVIII', 'MMCCCLXIX', 'MMCCCLXX', 'MMCCCLXXI', 'MMCCCLXXII', 'MMCCCLXXIII', 'MMCCCLXXIV', 'MMCCCLXXV', 'MMCCCLXXVI', 'MMCCCLXXVII', 'MMCCCLXXVIII', 'MMCCCLXXIX', 'MMCCCLXXX', 'MMCCCLXXXI', 'MMCCCLXXXII', 'MMCCCLXXXIII', 'MMCCCLXXXIV', 'MMCCCLXXXV', 'MMCCCLXXXVI', 'MMCCCLXXXVII', 'MMCCCLXXXVIII', 'MMCCCLXXXIX', 'MMCCCXC', 'MMCCCXCI', 'MMCCCXCII', 'MMCCCXCIII', 'MMCCCXCIV', 'MMCCCVC', 'MMCCCVCI', 'MMCCCVCII', 'MMCCCVCIII', 'MMCCCIC', 'MMCD', 'MMCDI', 'MMCDII', 'MMCDIII', 'MMCDIV', 'MMCDV', 'MMCDVI', 'MMCDVII', 'MMCDVIII', 'MMCDIX', 'MMCDX', 'MMCDXI', 'MMCDXII', 'MMCDXIII', 'MMCDXIV', 'MMCDXV', 'MMCDXVI', 'MMCDXVII', 'MMCDXVIII', 'MMCDXIX', 'MMCDXX', 'MMCDXXI', 'MMCDXXII', 'MMCDXXIII', 'MMCDXXIV', 'MMCDXXV', 'MMCDXXVI', 'MMCDXXVII', 'MMCDXXVIII', 'MMCDXXIX', 'MMCDXXX', 'MMCDXXXI', 'MMCDXXXII', 'MMCDXXXIII', 'MMCDXXXIV', 'MMCDXXXV', 'MMCDXXXVI', 'MMCDXXXVII', 'MMCDXXXVIII', 'MMCDXXXIX', 'MMCDXL', 'MMCDXLI', 'MMCDXLII', 'MMCDXLIII', 'MMCDXLIV', 'MMCDVL', 'MMCDVLI', 'MMCDVLII', 'MMCDVLIII', 'MMCDIL', 'MMLD', 'MMLDI', 'MMLDII', 'MMLDIII', 'MMLDIV', 'MMLDV', 'MMLDVI', 'MMLDVII', 'MMLDVIII', 'MMLDIX', 'MMLDX', 'MMLDXI', 'MMLDXII', 'MMLDXIII', 'MMLDXIV', 'MMLDXV', 'MMLDXVI', 'MMLDXVII', 'MMLDXVIII', 'MMLDXIX', 'MMLDXX', 'MMLDXXI', 'MMLDXXII', 'MMLDXXIII', 'MMLDXXIV', 'MMLDXXV', 'MMLDXXVI', 'MMLDXXVII', 'MMLDXXVIII', 'MMLDXXIX', 'MMLDXXX', 'MMLDXXXI', 'MMLDXXXII', 'MMLDXXXIII', 'MMLDXXXIV', 'MMLDXXXV', 'MMLDXXXVI', 'MMLDXXXVII', 'MMLDXXXVIII', 'MMLDXXXIX', 'MMXD', 'MMXDI', 'MMXDII', 'MMXDIII', 'MMXDIV', 'MMXDV', 'MMXDVI', 'MMXDVII', 'MMXDVIII', 'MMXDIX', 'MMD', 'MMDI', 'MMDII', 'MMDIII', 'MMDIV', 'MMDV', 'MMDVI', 'MMDVII', 'MMDVIII', 'MMDIX', 'MMDX', 'MMDXI', 'MMDXII', 'MMDXIII', 'MMDXIV', 'MMDXV', 'MMDXVI', 'MMDXVII', 'MMDXVIII', 'MMDXIX', 'MMDXX', 'MMDXXI', 'MMDXXII', 'MMDXXIII', 'MMDXXIV', 'MMDXXV', 'MMDXXVI', 'MMDXXVII', 'MMDXXVIII', 'MMDXXIX', 'MMDXXX', 'MMDXXXI', 'MMDXXXII', 'MMDXXXIII', 'MMDXXXIV', 'MMDXXXV', 'MMDXXXVI', 'MMDXXXVII', 'MMDXXXVIII', 'MMDXXXIX', 'MMDXL', 'MMDXLI', 'MMDXLII', 'MMDXLIII', 'MMDXLIV', 'MMDVL', 'MMDVLI', 'MMDVLII', 'MMDVLIII', 'MMDIL', 'MMDL', 'MMDLI', 'MMDLII', 'MMDLIII', 'MMDLIV', 'MMDLV', 'MMDLVI', 'MMDLVII', 'MMDLVIII', 'MMDLIX', 'MMDLX', 'MMDLXI', 'MMDLXII', 'MMDLXIII', 'MMDLXIV', 'MMDLXV', 'MMDLXVI', 'MMDLXVII', 'MMDLXVIII', 'MMDLXIX', 'MMDLXX', 'MMDLXXI', 'MMDLXXII', 'MMDLXXIII', 'MMDLXXIV', 'MMDLXXV', 'MMDLXXVI', 'MMDLXXVII', 'MMDLXXVIII', 'MMDLXXIX', 'MMDLXXX', 'MMDLXXXI', 'MMDLXXXII', 'MMDLXXXIII', 'MMDLXXXIV', 'MMDLXXXV', 'MMDLXXXVI', 'MMDLXXXVII', 'MMDLXXXVIII', 'MMDLXXXIX', 'MMDXC', 'MMDXCI', 'MMDXCII', 'MMDXCIII', 'MMDXCIV', 'MMDVC', 'MMDVCI', 'MMDVCII', 'MMDVCIII', 'MMDIC', 'MMDC', 'MMDCI', 'MMDCII', 'MMDCIII', 'MMDCIV', 'MMDCV', 'MMDCVI', 'MMDCVII', 'MMDCVIII', 'MMDCIX', 'MMDCX', 'MMDCXI', 'MMDCXII', 'MMDCXIII', 'MMDCXIV', 'MMDCXV', 'MMDCXVI', 'MMDCXVII', 'MMDCXVIII', 'MMDCXIX', 'MMDCXX', 'MMDCXXI', 'MMDCXXII', 'MMDCXXIII', 'MMDCXXIV', 'MMDCXXV', 'MMDCXXVI', 'MMDCXXVII', 'MMDCXXVIII', 'MMDCXXIX', 'MMDCXXX', 'MMDCXXXI', 'MMDCXXXII', 'MMDCXXXIII', 'MMDCXXXIV', 'MMDCXXXV', 'MMDCXXXVI', 'MMDCXXXVII', 'MMDCXXXVIII', 'MMDCXXXIX', 'MMDCXL', 'MMDCXLI', 'MMDCXLII', 'MMDCXLIII', 'MMDCXLIV', 'MMDCVL', 'MMDCVLI', 'MMDCVLII', 'MMDCVLIII', 'MMDCIL', 'MMDCL', 'MMDCLI', 'MMDCLII', 'MMDCLIII', 'MMDCLIV', 'MMDCLV', 'MMDCLVI', 'MMDCLVII', 'MMDCLVIII', 'MMDCLIX', 'MMDCLX', 'MMDCLXI', 'MMDCLXII', 'MMDCLXIII', 'MMDCLXIV', 'MMDCLXV', 'MMDCLXVI', 'MMDCLXVII', 'MMDCLXVIII', 'MMDCLXIX', 'MMDCLXX', 'MMDCLXXI', 'MMDCLXXII', 'MMDCLXXIII', 'MMDCLXXIV', 'MMDCLXXV', 'MMDCLXXVI', 'MMDCLXXVII', 'MMDCLXXVIII', 'MMDCLXXIX', 'MMDCLXXX', 'MMDCLXXXI', 'MMDCLXXXII', 'MMDCLXXXIII', 'MMDCLXXXIV', 'MMDCLXXXV', 'MMDCLXXXVI', 'MMDCLXXXVII', 'MMDCLXXXVIII', 'MMDCLXXXIX', 'MMDCXC', 'MMDCXCI', 'MMDCXCII', 'MMDCXCIII', 'MMDCXCIV', 'MMDCVC', 'MMDCVCI', 'MMDCVCII', 'MMDCVCIII', 'MMDCIC', 'MMDCC', 'MMDCCI', 'MMDCCII', 'MMDCCIII', 'MMDCCIV', 'MMDCCV', 'MMDCCVI', 'MMDCCVII', 'MMDCCVIII', 'MMDCCIX', 'MMDCCX', 'MMDCCXI', 'MMDCCXII', 'MMDCCXIII', 'MMDCCXIV', 'MMDCCXV', 'MMDCCXVI', 'MMDCCXVII', 'MMDCCXVIII', 'MMDCCXIX', 'MMDCCXX', 'MMDCCXXI', 'MMDCCXXII', 'MMDCCXXIII', 'MMDCCXXIV', 'MMDCCXXV', 'MMDCCXXVI', 'MMDCCXXVII', 'MMDCCXXVIII', 'MMDCCXXIX', 'MMDCCXXX', 'MMDCCXXXI', 'MMDCCXXXII', 'MMDCCXXXIII', 'MMDCCXXXIV', 'MMDCCXXXV', 'MMDCCXXXVI', 'MMDCCXXXVII', 'MMDCCXXXVIII', 'MMDCCXXXIX', 'MMDCCXL', 'MMDCCXLI', 'MMDCCXLII', 'MMDCCXLIII', 'MMDCCXLIV', 'MMDCCVL', 'MMDCCVLI', 'MMDCCVLII', 'MMDCCVLIII', 'MMDCCIL', 'MMDCCL', 'MMDCCLI', 'MMDCCLII', 'MMDCCLIII', 'MMDCCLIV', 'MMDCCLV', 'MMDCCLVI', 'MMDCCLVII', 'MMDCCLVIII', 'MMDCCLIX', 'MMDCCLX', 'MMDCCLXI', 'MMDCCLXII', 'MMDCCLXIII', 'MMDCCLXIV', 'MMDCCLXV', 'MMDCCLXVI', 'MMDCCLXVII', 'MMDCCLXVIII', 'MMDCCLXIX', 'MMDCCLXX', 'MMDCCLXXI', 'MMDCCLXXII', 'MMDCCLXXIII', 'MMDCCLXXIV', 'MMDCCLXXV', 'MMDCCLXXVI', 'MMDCCLXXVII', 'MMDCCLXXVIII', 'MMDCCLXXIX', 'MMDCCLXXX', 'MMDCCLXXXI', 'MMDCCLXXXII', 'MMDCCLXXXIII', 'MMDCCLXXXIV', 'MMDCCLXXXV', 'MMDCCLXXXVI', 'MMDCCLXXXVII', 'MMDCCLXXXVIII', 'MMDCCLXXXIX', 'MMDCCXC', 'MMDCCXCI', 'MMDCCXCII', 'MMDCCXCIII', 'MMDCCXCIV', 'MMDCCVC', 'MMDCCVCI', 'MMDCCVCII', 'MMDCCVCIII', 'MMDCCIC', 'MMDCCC', 'MMDCCCI', 'MMDCCCII', 'MMDCCCIII', 'MMDCCCIV', 'MMDCCCV', 'MMDCCCVI', 'MMDCCCVII', 'MMDCCCVIII', 'MMDCCCIX', 'MMDCCCX', 'MMDCCCXI', 'MMDCCCXII', 'MMDCCCXIII', 'MMDCCCXIV', 'MMDCCCXV', 'MMDCCCXVI', 'MMDCCCXVII', 'MMDCCCXVIII', 'MMDCCCXIX', 'MMDCCCXX', 'MMDCCCXXI', 'MMDCCCXXII', 'MMDCCCXXIII', 'MMDCCCXXIV', 'MMDCCCXXV', 'MMDCCCXXVI', 'MMDCCCXXVII', 'MMDCCCXXVIII', 'MMDCCCXXIX', 'MMDCCCXXX', 'MMDCCCXXXI', 'MMDCCCXXXII', 'MMDCCCXXXIII', 'MMDCCCXXXIV', 'MMDCCCXXXV', 'MMDCCCXXXVI', 'MMDCCCXXXVII', 'MMDCCCXXXVIII', 'MMDCCCXXXIX', 'MMDCCCXL', 'MMDCCCXLI', 'MMDCCCXLII', 'MMDCCCXLIII', 'MMDCCCXLIV', 'MMDCCCVL', 'MMDCCCVLI', 'MMDCCCVLII', 'MMDCCCVLIII', 'MMDCCCIL', 'MMDCCCL', 'MMDCCCLI', 'MMDCCCLII', 'MMDCCCLIII', 'MMDCCCLIV', 'MMDCCCLV', 'MMDCCCLVI', 'MMDCCCLVII', 'MMDCCCLVIII', 'MMDCCCLIX', 'MMDCCCLX', 'MMDCCCLXI', 'MMDCCCLXII', 'MMDCCCLXIII', 'MMDCCCLXIV', 'MMDCCCLXV', 'MMDCCCLXVI', 'MMDCCCLXVII', 'MMDCCCLXVIII', 'MMDCCCLXIX', 'MMDCCCLXX', 'MMDCCCLXXI', 'MMDCCCLXXII', 'MMDCCCLXXIII', 'MMDCCCLXXIV', 'MMDCCCLXXV', 'MMDCCCLXXVI', 'MMDCCCLXXVII', 'MMDCCCLXXVIII', 'MMDCCCLXXIX', 'MMDCCCLXXX', 'MMDCCCLXXXI', 'MMDCCCLXXXII', 'MMDCCCLXXXIII', 'MMDCCCLXXXIV', 'MMDCCCLXXXV', 'MMDCCCLXXXVI', 'MMDCCCLXXXVII', 'MMDCCCLXXXVIII', 'MMDCCCLXXXIX', 'MMDCCCXC', 'MMDCCCXCI', 'MMDCCCXCII', 'MMDCCCXCIII', 'MMDCCCXCIV', 'MMDCCCVC', 'MMDCCCVCI', 'MMDCCCVCII', 'MMDCCCVCIII', 'MMDCCCIC', 'MMCM', 'MMCMI', 'MMCMII', 'MMCMIII', 'MMCMIV', 'MMCMV', 'MMCMVI', 'MMCMVII', 'MMCMVIII', 'MMCMIX', 'MMCMX', 'MMCMXI', 'MMCMXII', 'MMCMXIII', 'MMCMXIV', 'MMCMXV', 'MMCMXVI', 'MMCMXVII', 'MMCMXVIII', 'MMCMXIX', 'MMCMXX', 'MMCMXXI', 'MMCMXXII', 'MMCMXXIII', 'MMCMXXIV', 'MMCMXXV', 'MMCMXXVI', 'MMCMXXVII', 'MMCMXXVIII', 'MMCMXXIX', 'MMCMXXX', 'MMCMXXXI', 'MMCMXXXII', 'MMCMXXXIII', 'MMCMXXXIV', 'MMCMXXXV', 'MMCMXXXVI', 'MMCMXXXVII', 'MMCMXXXVIII', 'MMCMXXXIX', 'MMCMXL', 'MMCMXLI', 'MMCMXLII', 'MMCMXLIII', 'MMCMXLIV', 'MMCMVL', 'MMCMVLI', 'MMCMVLII', 'MMCMVLIII', 'MMCMIL', 'MMLM', 'MMLMI', 'MMLMII', 'MMLMIII', 'MMLMIV', 'MMLMV', 'MMLMVI', 'MMLMVII', 'MMLMVIII', 'MMLMIX', 'MMLMX', 'MMLMXI', 'MMLMXII', 'MMLMXIII', 'MMLMXIV', 'MMLMXV', 'MMLMXVI', 'MMLMXVII', 'MMLMXVIII', 'MMLMXIX', 'MMLMXX', 'MMLMXXI', 'MMLMXXII', 'MMLMXXIII', 'MMLMXXIV', 'MMLMXXV', 'MMLMXXVI', 'MMLMXXVII', 'MMLMXXVIII', 'MMLMXXIX', 'MMLMXXX', 'MMLMXXXI', 'MMLMXXXII', 'MMLMXXXIII', 'MMLMXXXIV', 'MMLMXXXV', 'MMLMXXXVI', 'MMLMXXXVII', 'MMLMXXXVIII', 'MMLMXXXIX', 'MMXM', 'MMXMI', 'MMXMII', 'MMXMIII', 'MMXMIV', 'MMXMV', 'MMXMVI', 'MMXMVII', 'MMXMVIII', 'MMXMIX', 'MMM', 'MMMI', 'MMMII', 'MMMIII', 'MMMIV', 'MMMV', 'MMMVI', 'MMMVII', 'MMMVIII', 'MMMIX', 'MMMX', 'MMMXI', 'MMMXII', 'MMMXIII', 'MMMXIV', 'MMMXV', 'MMMXVI', 'MMMXVII', 'MMMXVIII', 'MMMXIX', 'MMMXX', 'MMMXXI', 'MMMXXII', 'MMMXXIII', 'MMMXXIV', 'MMMXXV', 'MMMXXVI', 'MMMXXVII', 'MMMXXVIII', 'MMMXXIX', 'MMMXXX', 'MMMXXXI', 'MMMXXXII', 'MMMXXXIII', 'MMMXXXIV', 'MMMXXXV', 'MMMXXXVI', 'MMMXXXVII', 'MMMXXXVIII', 'MMMXXXIX', 'MMMXL', 'MMMXLI', 'MMMXLII', 'MMMXLIII', 'MMMXLIV', 'MMMVL', 'MMMVLI', 'MMMVLII', 'MMMVLIII', 'MMMIL', 'MMML', 'MMMLI', 'MMMLII', 'MMMLIII', 'MMMLIV', 'MMMLV', 'MMMLVI', 'MMMLVII', 'MMMLVIII', 'MMMLIX', 'MMMLX', 'MMMLXI', 'MMMLXII', 'MMMLXIII', 'MMMLXIV', 'MMMLXV', 'MMMLXVI', 'MMMLXVII', 'MMMLXVIII', 'MMMLXIX', 'MMMLXX', 'MMMLXXI', 'MMMLXXII', 'MMMLXXIII', 'MMMLXXIV', 'MMMLXXV', 'MMMLXXVI', 'MMMLXXVII', 'MMMLXXVIII', 'MMMLXXIX', 'MMMLXXX', 'MMMLXXXI', 'MMMLXXXII', 'MMMLXXXIII', 'MMMLXXXIV', 'MMMLXXXV', 'MMMLXXXVI', 'MMMLXXXVII', 'MMMLXXXVIII', 'MMMLXXXIX', 'MMMXC', 'MMMXCI', 'MMMXCII', 'MMMXCIII', 'MMMXCIV', 'MMMVC', 'MMMVCI', 'MMMVCII', 'MMMVCIII', 'MMMIC', 'MMMC', 'MMMCI', 'MMMCII', 'MMMCIII', 'MMMCIV', 'MMMCV', 'MMMCVI', 'MMMCVII', 'MMMCVIII', 'MMMCIX', 'MMMCX', 'MMMCXI', 'MMMCXII', 'MMMCXIII', 'MMMCXIV', 'MMMCXV', 'MMMCXVI', 'MMMCXVII', 'MMMCXVIII', 'MMMCXIX', 'MMMCXX', 'MMMCXXI', 'MMMCXXII', 'MMMCXXIII', 'MMMCXXIV', 'MMMCXXV', 'MMMCXXVI', 'MMMCXXVII', 'MMMCXXVIII', 'MMMCXXIX', 'MMMCXXX', 'MMMCXXXI', 'MMMCXXXII', 'MMMCXXXIII', 'MMMCXXXIV', 'MMMCXXXV', 'MMMCXXXVI', 'MMMCXXXVII', 'MMMCXXXVIII', 'MMMCXXXIX', 'MMMCXL', 'MMMCXLI', 'MMMCXLII', 'MMMCXLIII', 'MMMCXLIV', 'MMMCVL', 'MMMCVLI', 'MMMCVLII', 'MMMCVLIII', 'MMMCIL', 'MMMCL', 'MMMCLI', 'MMMCLII', 'MMMCLIII', 'MMMCLIV', 'MMMCLV', 'MMMCLVI', 'MMMCLVII', 'MMMCLVIII', 'MMMCLIX', 'MMMCLX', 'MMMCLXI', 'MMMCLXII', 'MMMCLXIII', 'MMMCLXIV', 'MMMCLXV', 'MMMCLXVI', 'MMMCLXVII', 'MMMCLXVIII', 'MMMCLXIX', 'MMMCLXX', 'MMMCLXXI', 'MMMCLXXII', 'MMMCLXXIII', 'MMMCLXXIV', 'MMMCLXXV', 'MMMCLXXVI', 'MMMCLXXVII', 'MMMCLXXVIII', 'MMMCLXXIX', 'MMMCLXXX', 'MMMCLXXXI', 'MMMCLXXXII', 'MMMCLXXXIII', 'MMMCLXXXIV', 'MMMCLXXXV', 'MMMCLXXXVI', 'MMMCLXXXVII', 'MMMCLXXXVIII', 'MMMCLXXXIX', 'MMMCXC', 'MMMCXCI', 'MMMCXCII', 'MMMCXCIII', 'MMMCXCIV', 'MMMCVC', 'MMMCVCI', 'MMMCVCII', 'MMMCVCIII', 'MMMCIC', 'MMMCC', 'MMMCCI', 'MMMCCII', 'MMMCCIII', 'MMMCCIV', 'MMMCCV', 'MMMCCVI', 'MMMCCVII', 'MMMCCVIII', 'MMMCCIX', 'MMMCCX', 'MMMCCXI', 'MMMCCXII', 'MMMCCXIII', 'MMMCCXIV', 'MMMCCXV', 'MMMCCXVI', 'MMMCCXVII', 'MMMCCXVIII', 'MMMCCXIX', 'MMMCCXX', 'MMMCCXXI', 'MMMCCXXII', 'MMMCCXXIII', 'MMMCCXXIV', 'MMMCCXXV', 'MMMCCXXVI', 'MMMCCXXVII', 'MMMCCXXVIII', 'MMMCCXXIX', 'MMMCCXXX', 'MMMCCXXXI', 'MMMCCXXXII', 'MMMCCXXXIII', 'MMMCCXXXIV', 'MMMCCXXXV', 'MMMCCXXXVI', 'MMMCCXXXVII', 'MMMCCXXXVIII', 'MMMCCXXXIX', 'MMMCCXL', 'MMMCCXLI', 'MMMCCXLII', 'MMMCCXLIII', 'MMMCCXLIV', 'MMMCCVL', 'MMMCCVLI', 'MMMCCVLII', 'MMMCCVLIII', 'MMMCCIL', 'MMMCCL', 'MMMCCLI', 'MMMCCLII', 'MMMCCLIII', 'MMMCCLIV', 'MMMCCLV', 'MMMCCLVI', 'MMMCCLVII', 'MMMCCLVIII', 'MMMCCLIX', 'MMMCCLX', 'MMMCCLXI', 'MMMCCLXII', 'MMMCCLXIII', 'MMMCCLXIV', 'MMMCCLXV', 'MMMCCLXVI', 'MMMCCLXVII', 'MMMCCLXVIII', 'MMMCCLXIX', 'MMMCCLXX', 'MMMCCLXXI', 'MMMCCLXXII', 'MMMCCLXXIII', 'MMMCCLXXIV', 'MMMCCLXXV', 'MMMCCLXXVI', 'MMMCCLXXVII', 'MMMCCLXXVIII', 'MMMCCLXXIX', 'MMMCCLXXX', 'MMMCCLXXXI', 'MMMCCLXXXII', 'MMMCCLXXXIII', 'MMMCCLXXXIV', 'MMMCCLXXXV', 'MMMCCLXXXVI', 'MMMCCLXXXVII', 'MMMCCLXXXVIII', 'MMMCCLXXXIX', 'MMMCCXC', 'MMMCCXCI', 'MMMCCXCII', 'MMMCCXCIII', 'MMMCCXCIV', 'MMMCCVC', 'MMMCCVCI', 'MMMCCVCII', 'MMMCCVCIII', 'MMMCCIC', 'MMMCCC', 'MMMCCCI', 'MMMCCCII', 'MMMCCCIII', 'MMMCCCIV', 'MMMCCCV', 'MMMCCCVI', 'MMMCCCVII', 'MMMCCCVIII', 'MMMCCCIX', 'MMMCCCX', 'MMMCCCXI', 'MMMCCCXII', 'MMMCCCXIII', 'MMMCCCXIV', 'MMMCCCXV', 'MMMCCCXVI', 'MMMCCCXVII', 'MMMCCCXVIII', 'MMMCCCXIX', 'MMMCCCXX', 'MMMCCCXXI', 'MMMCCCXXII', 'MMMCCCXXIII', 'MMMCCCXXIV', 'MMMCCCXXV', 'MMMCCCXXVI', 'MMMCCCXXVII', 'MMMCCCXXVIII', 'MMMCCCXXIX', 'MMMCCCXXX', 'MMMCCCXXXI', 'MMMCCCXXXII', 'MMMCCCXXXIII', 'MMMCCCXXXIV', 'MMMCCCXXXV', 'MMMCCCXXXVI', 'MMMCCCXXXVII', 'MMMCCCXXXVIII', 'MMMCCCXXXIX', 'MMMCCCXL', 'MMMCCCXLI', 'MMMCCCXLII', 'MMMCCCXLIII', 'MMMCCCXLIV', 'MMMCCCVL', 'MMMCCCVLI', 'MMMCCCVLII', 'MMMCCCVLIII', 'MMMCCCIL', 'MMMCCCL', 'MMMCCCLI', 'MMMCCCLII', 'MMMCCCLIII', 'MMMCCCLIV', 'MMMCCCLV', 'MMMCCCLVI', 'MMMCCCLVII', 'MMMCCCLVIII', 'MMMCCCLIX', 'MMMCCCLX', 'MMMCCCLXI', 'MMMCCCLXII', 'MMMCCCLXIII', 'MMMCCCLXIV', 'MMMCCCLXV', 'MMMCCCLXVI', 'MMMCCCLXVII', 'MMMCCCLXVIII', 'MMMCCCLXIX', 'MMMCCCLXX', 'MMMCCCLXXI', 'MMMCCCLXXII', 'MMMCCCLXXIII', 'MMMCCCLXXIV', 'MMMCCCLXXV', 'MMMCCCLXXVI', 'MMMCCCLXXVII', 'MMMCCCLXXVIII', 'MMMCCCLXXIX', 'MMMCCCLXXX', 'MMMCCCLXXXI', 'MMMCCCLXXXII', 'MMMCCCLXXXIII', 'MMMCCCLXXXIV', 'MMMCCCLXXXV', 'MMMCCCLXXXVI', 'MMMCCCLXXXVII', 'MMMCCCLXXXVIII', 'MMMCCCLXXXIX', 'MMMCCCXC', 'MMMCCCXCI', 'MMMCCCXCII', 'MMMCCCXCIII', 'MMMCCCXCIV', 'MMMCCCVC', 'MMMCCCVCI', 'MMMCCCVCII', 'MMMCCCVCIII', 'MMMCCCIC', 'MMMCD', 'MMMCDI', 'MMMCDII', 'MMMCDIII', 'MMMCDIV', 'MMMCDV', 'MMMCDVI', 'MMMCDVII', 'MMMCDVIII', 'MMMCDIX', 'MMMCDX', 'MMMCDXI', 'MMMCDXII', 'MMMCDXIII', 'MMMCDXIV', 'MMMCDXV', 'MMMCDXVI', 'MMMCDXVII', 'MMMCDXVIII', 'MMMCDXIX', 'MMMCDXX', 'MMMCDXXI', 'MMMCDXXII', 'MMMCDXXIII', 'MMMCDXXIV', 'MMMCDXXV', 'MMMCDXXVI', 'MMMCDXXVII', 'MMMCDXXVIII', 'MMMCDXXIX', 'MMMCDXXX', 'MMMCDXXXI', 'MMMCDXXXII', 'MMMCDXXXIII', 'MMMCDXXXIV', 'MMMCDXXXV', 'MMMCDXXXVI', 'MMMCDXXXVII', 'MMMCDXXXVIII', 'MMMCDXXXIX', 'MMMCDXL', 'MMMCDXLI', 'MMMCDXLII', 'MMMCDXLIII', 'MMMCDXLIV', 'MMMCDVL', 'MMMCDVLI', 'MMMCDVLII', 'MMMCDVLIII', 'MMMCDIL', 'MMMLD', 'MMMLDI', 'MMMLDII', 'MMMLDIII', 'MMMLDIV', 'MMMLDV', 'MMMLDVI', 'MMMLDVII', 'MMMLDVIII', 'MMMLDIX', 'MMMLDX', 'MMMLDXI', 'MMMLDXII', 'MMMLDXIII', 'MMMLDXIV', 'MMMLDXV', 'MMMLDXVI', 'MMMLDXVII', 'MMMLDXVIII', 'MMMLDXIX', 'MMMLDXX', 'MMMLDXXI', 'MMMLDXXII', 'MMMLDXXIII', 'MMMLDXXIV', 'MMMLDXXV', 'MMMLDXXVI', 'MMMLDXXVII', 'MMMLDXXVIII', 'MMMLDXXIX', 'MMMLDXXX', 'MMMLDXXXI', 'MMMLDXXXII', 'MMMLDXXXIII', 'MMMLDXXXIV', 'MMMLDXXXV', 'MMMLDXXXVI', 'MMMLDXXXVII', 'MMMLDXXXVIII', 'MMMLDXXXIX', 'MMMXD', 'MMMXDI', 'MMMXDII', 'MMMXDIII', 'MMMXDIV', 'MMMXDV', 'MMMXDVI', 'MMMXDVII', 'MMMXDVIII', 'MMMXDIX', 'MMMD', 'MMMDI', 'MMMDII', 'MMMDIII', 'MMMDIV', 'MMMDV', 'MMMDVI', 'MMMDVII', 'MMMDVIII', 'MMMDIX', 'MMMDX', 'MMMDXI', 'MMMDXII', 'MMMDXIII', 'MMMDXIV', 'MMMDXV', 'MMMDXVI', 'MMMDXVII', 'MMMDXVIII', 'MMMDXIX', 'MMMDXX', 'MMMDXXI', 'MMMDXXII', 'MMMDXXIII', 'MMMDXXIV', 'MMMDXXV', 'MMMDXXVI', 'MMMDXXVII', 'MMMDXXVIII', 'MMMDXXIX', 'MMMDXXX', 'MMMDXXXI', 'MMMDXXXII', 'MMMDXXXIII', 'MMMDXXXIV', 'MMMDXXXV', 'MMMDXXXVI', 'MMMDXXXVII', 'MMMDXXXVIII', 'MMMDXXXIX', 'MMMDXL', 'MMMDXLI', 'MMMDXLII', 'MMMDXLIII', 'MMMDXLIV', 'MMMDVL', 'MMMDVLI', 'MMMDVLII', 'MMMDVLIII', 'MMMDIL', 'MMMDL', 'MMMDLI', 'MMMDLII', 'MMMDLIII', 'MMMDLIV', 'MMMDLV', 'MMMDLVI', 'MMMDLVII', 'MMMDLVIII', 'MMMDLIX', 'MMMDLX', 'MMMDLXI', 'MMMDLXII', 'MMMDLXIII', 'MMMDLXIV', 'MMMDLXV', 'MMMDLXVI', 'MMMDLXVII', 'MMMDLXVIII', 'MMMDLXIX', 'MMMDLXX', 'MMMDLXXI', 'MMMDLXXII', 'MMMDLXXIII', 'MMMDLXXIV', 'MMMDLXXV', 'MMMDLXXVI', 'MMMDLXXVII', 'MMMDLXXVIII', 'MMMDLXXIX', 'MMMDLXXX', 'MMMDLXXXI', 'MMMDLXXXII', 'MMMDLXXXIII', 'MMMDLXXXIV', 'MMMDLXXXV', 'MMMDLXXXVI', 'MMMDLXXXVII', 'MMMDLXXXVIII', 'MMMDLXXXIX', 'MMMDXC', 'MMMDXCI', 'MMMDXCII', 'MMMDXCIII', 'MMMDXCIV', 'MMMDVC', 'MMMDVCI', 'MMMDVCII', 'MMMDVCIII', 'MMMDIC', 'MMMDC', 'MMMDCI', 'MMMDCII', 'MMMDCIII', 'MMMDCIV', 'MMMDCV', 'MMMDCVI', 'MMMDCVII', 'MMMDCVIII', 'MMMDCIX', 'MMMDCX', 'MMMDCXI', 'MMMDCXII', 'MMMDCXIII', 'MMMDCXIV', 'MMMDCXV', 'MMMDCXVI', 'MMMDCXVII', 'MMMDCXVIII', 'MMMDCXIX', 'MMMDCXX', 'MMMDCXXI', 'MMMDCXXII', 'MMMDCXXIII', 'MMMDCXXIV', 'MMMDCXXV', 'MMMDCXXVI', 'MMMDCXXVII', 'MMMDCXXVIII', 'MMMDCXXIX', 'MMMDCXXX', 'MMMDCXXXI', 'MMMDCXXXII', 'MMMDCXXXIII', 'MMMDCXXXIV', 'MMMDCXXXV', 'MMMDCXXXVI', 'MMMDCXXXVII', 'MMMDCXXXVIII', 'MMMDCXXXIX', 'MMMDCXL', 'MMMDCXLI', 'MMMDCXLII', 'MMMDCXLIII', 'MMMDCXLIV', 'MMMDCVL', 'MMMDCVLI', 'MMMDCVLII', 'MMMDCVLIII', 'MMMDCIL', 'MMMDCL', 'MMMDCLI', 'MMMDCLII', 'MMMDCLIII', 'MMMDCLIV', 'MMMDCLV', 'MMMDCLVI', 'MMMDCLVII', 'MMMDCLVIII', 'MMMDCLIX', 'MMMDCLX', 'MMMDCLXI', 'MMMDCLXII', 'MMMDCLXIII', 'MMMDCLXIV', 'MMMDCLXV', 'MMMDCLXVI', 'MMMDCLXVII', 'MMMDCLXVIII', 'MMMDCLXIX', 'MMMDCLXX', 'MMMDCLXXI', 'MMMDCLXXII', 'MMMDCLXXIII', 'MMMDCLXXIV', 'MMMDCLXXV', 'MMMDCLXXVI', 'MMMDCLXXVII', 'MMMDCLXXVIII', 'MMMDCLXXIX', 'MMMDCLXXX', 'MMMDCLXXXI', 'MMMDCLXXXII', 'MMMDCLXXXIII', 'MMMDCLXXXIV', 'MMMDCLXXXV', 'MMMDCLXXXVI', 'MMMDCLXXXVII', 'MMMDCLXXXVIII', 'MMMDCLXXXIX', 'MMMDCXC', 'MMMDCXCI', 'MMMDCXCII', 'MMMDCXCIII', 'MMMDCXCIV', 'MMMDCVC', 'MMMDCVCI', 'MMMDCVCII', 'MMMDCVCIII', 'MMMDCIC', 'MMMDCC', 'MMMDCCI', 'MMMDCCII', 'MMMDCCIII', 'MMMDCCIV', 'MMMDCCV', 'MMMDCCVI', 'MMMDCCVII', 'MMMDCCVIII', 'MMMDCCIX', 'MMMDCCX', 'MMMDCCXI', 'MMMDCCXII', 'MMMDCCXIII', 'MMMDCCXIV', 'MMMDCCXV', 'MMMDCCXVI', 'MMMDCCXVII', 'MMMDCCXVIII', 'MMMDCCXIX', 'MMMDCCXX', 'MMMDCCXXI', 'MMMDCCXXII', 'MMMDCCXXIII', 'MMMDCCXXIV', 'MMMDCCXXV', 'MMMDCCXXVI', 'MMMDCCXXVII', 'MMMDCCXXVIII', 'MMMDCCXXIX', 'MMMDCCXXX', 'MMMDCCXXXI', 'MMMDCCXXXII', 'MMMDCCXXXIII', 'MMMDCCXXXIV', 'MMMDCCXXXV', 'MMMDCCXXXVI', 'MMMDCCXXXVII', 'MMMDCCXXXVIII', 'MMMDCCXXXIX', 'MMMDCCXL', 'MMMDCCXLI', 'MMMDCCXLII', 'MMMDCCXLIII', 'MMMDCCXLIV', 'MMMDCCVL', 'MMMDCCVLI', 'MMMDCCVLII', 'MMMDCCVLIII', 'MMMDCCIL', 'MMMDCCL', 'MMMDCCLI', 'MMMDCCLII', 'MMMDCCLIII', 'MMMDCCLIV', 'MMMDCCLV', 'MMMDCCLVI', 'MMMDCCLVII', 'MMMDCCLVIII', 'MMMDCCLIX', 'MMMDCCLX', 'MMMDCCLXI', 'MMMDCCLXII', 'MMMDCCLXIII', 'MMMDCCLXIV', 'MMMDCCLXV', 'MMMDCCLXVI', 'MMMDCCLXVII', 'MMMDCCLXVIII', 'MMMDCCLXIX', 'MMMDCCLXX', 'MMMDCCLXXI', 'MMMDCCLXXII', 'MMMDCCLXXIII', 'MMMDCCLXXIV', 'MMMDCCLXXV', 'MMMDCCLXXVI', 'MMMDCCLXXVII', 'MMMDCCLXXVIII', 'MMMDCCLXXIX', 'MMMDCCLXXX', 'MMMDCCLXXXI', 'MMMDCCLXXXII', 'MMMDCCLXXXIII', 'MMMDCCLXXXIV', 'MMMDCCLXXXV', 'MMMDCCLXXXVI', 'MMMDCCLXXXVII', 'MMMDCCLXXXVIII', 'MMMDCCLXXXIX', 'MMMDCCXC', 'MMMDCCXCI', 'MMMDCCXCII', 'MMMDCCXCIII', 'MMMDCCXCIV', 'MMMDCCVC', 'MMMDCCVCI', 'MMMDCCVCII', 'MMMDCCVCIII', 'MMMDCCIC', 'MMMDCCC', 'MMMDCCCI', 'MMMDCCCII', 'MMMDCCCIII', 'MMMDCCCIV', 'MMMDCCCV', 'MMMDCCCVI', 'MMMDCCCVII', 'MMMDCCCVIII', 'MMMDCCCIX', 'MMMDCCCX', 'MMMDCCCXI', 'MMMDCCCXII', 'MMMDCCCXIII', 'MMMDCCCXIV', 'MMMDCCCXV', 'MMMDCCCXVI', 'MMMDCCCXVII', 'MMMDCCCXVIII', 'MMMDCCCXIX', 'MMMDCCCXX', 'MMMDCCCXXI', 'MMMDCCCXXII', 'MMMDCCCXXIII', 'MMMDCCCXXIV', 'MMMDCCCXXV', 'MMMDCCCXXVI', 'MMMDCCCXXVII', 'MMMDCCCXXVIII', 'MMMDCCCXXIX', 'MMMDCCCXXX', 'MMMDCCCXXXI', 'MMMDCCCXXXII', 'MMMDCCCXXXIII', 'MMMDCCCXXXIV', 'MMMDCCCXXXV', 'MMMDCCCXXXVI', 'MMMDCCCXXXVII', 'MMMDCCCXXXVIII', 'MMMDCCCXXXIX', 'MMMDCCCXL', 'MMMDCCCXLI', 'MMMDCCCXLII', 'MMMDCCCXLIII', 'MMMDCCCXLIV', 'MMMDCCCVL', 'MMMDCCCVLI', 'MMMDCCCVLII', 'MMMDCCCVLIII', 'MMMDCCCIL', 'MMMDCCCL', 'MMMDCCCLI', 'MMMDCCCLII', 'MMMDCCCLIII', 'MMMDCCCLIV', 'MMMDCCCLV', 'MMMDCCCLVI', 'MMMDCCCLVII', 'MMMDCCCLVIII', 'MMMDCCCLIX', 'MMMDCCCLX', 'MMMDCCCLXI', 'MMMDCCCLXII', 'MMMDCCCLXIII', 'MMMDCCCLXIV', 'MMMDCCCLXV', 'MMMDCCCLXVI', 'MMMDCCCLXVII', 'MMMDCCCLXVIII', 'MMMDCCCLXIX', 'MMMDCCCLXX', 'MMMDCCCLXXI', 'MMMDCCCLXXII', 'MMMDCCCLXXIII', 'MMMDCCCLXXIV', 'MMMDCCCLXXV', 'MMMDCCCLXXVI', 'MMMDCCCLXXVII', 'MMMDCCCLXXVIII', 'MMMDCCCLXXIX', 'MMMDCCCLXXX', 'MMMDCCCLXXXI', 'MMMDCCCLXXXII', 'MMMDCCCLXXXIII', 'MMMDCCCLXXXIV', 'MMMDCCCLXXXV', 'MMMDCCCLXXXVI', 'MMMDCCCLXXXVII', 'MMMDCCCLXXXVIII', 'MMMDCCCLXXXIX', 'MMMDCCCXC', 'MMMDCCCXCI', 'MMMDCCCXCII', 'MMMDCCCXCIII', 'MMMDCCCXCIV', 'MMMDCCCVC', 'MMMDCCCVCI', 'MMMDCCCVCII', 'MMMDCCCVCIII', 'MMMDCCCIC', 'MMMCM', 'MMMCMI', 'MMMCMII', 'MMMCMIII', 'MMMCMIV', 'MMMCMV', 'MMMCMVI', 'MMMCMVII', 'MMMCMVIII', 'MMMCMIX', 'MMMCMX', 'MMMCMXI', 'MMMCMXII', 'MMMCMXIII', 'MMMCMXIV', 'MMMCMXV', 'MMMCMXVI', 'MMMCMXVII', 'MMMCMXVIII', 'MMMCMXIX', 'MMMCMXX', 'MMMCMXXI', 'MMMCMXXII', 'MMMCMXXIII', 'MMMCMXXIV', 'MMMCMXXV', 'MMMCMXXVI', 'MMMCMXXVII', 'MMMCMXXVIII', 'MMMCMXXIX', 'MMMCMXXX', 'MMMCMXXXI', 'MMMCMXXXII', 'MMMCMXXXIII', 'MMMCMXXXIV', 'MMMCMXXXV', 'MMMCMXXXVI', 'MMMCMXXXVII', 'MMMCMXXXVIII', 'MMMCMXXXIX', 'MMMCMXL', 'MMMCMXLI', 'MMMCMXLII', 'MMMCMXLIII', 'MMMCMXLIV', 'MMMCMVL', 'MMMCMVLI', 'MMMCMVLII', 'MMMCMVLIII', 'MMMCMIL', 'MMMLM', 'MMMLMI', 'MMMLMII', 'MMMLMIII', 'MMMLMIV', 'MMMLMV', 'MMMLMVI', 'MMMLMVII', 'MMMLMVIII', 'MMMLMIX', 'MMMLMX', 'MMMLMXI', 'MMMLMXII', 'MMMLMXIII', 'MMMLMXIV', 'MMMLMXV', 'MMMLMXVI', 'MMMLMXVII', 'MMMLMXVIII', 'MMMLMXIX', 'MMMLMXX', 'MMMLMXXI', 'MMMLMXXII', 'MMMLMXXIII', 'MMMLMXXIV', 'MMMLMXXV', 'MMMLMXXVI', 'MMMLMXXVII', 'MMMLMXXVIII', 'MMMLMXXIX', 'MMMLMXXX', 'MMMLMXXXI', 'MMMLMXXXII', 'MMMLMXXXIII', 'MMMLMXXXIV', 'MMMLMXXXV', 'MMMLMXXXVI', 'MMMLMXXXVII', 'MMMLMXXXVIII', 'MMMLMXXXIX', 'MMMXM', 'MMMXMI', 'MMMXMII', 'MMMXMIII', 'MMMXMIV', 'MMMXMV', 'MMMXMVI', 'MMMXMVII', 'MMMXMVIII', 'MMMXMIX', ] +const mode3 = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI', 'XII', 'XIII', 'XIV', 'XV', 'XVI', 'XVII', 'XVIII', 'XIX', 'XX', 'XXI', 'XXII', 'XXIII', 'XXIV', 'XXV', 'XXVI', 'XXVII', 'XXVIII', 'XXIX', 'XXX', 'XXXI', 'XXXII', 'XXXIII', 'XXXIV', 'XXXV', 'XXXVI', 'XXXVII', 'XXXVIII', 'XXXIX', 'XL', 'XLI', 'XLII', 'XLIII', 'XLIV', 'VL', 'VLI', 'VLII', 'VLIII', 'IL', 'L', 'LI', 'LII', 'LIII', 'LIV', 'LV', 'LVI', 'LVII', 'LVIII', 'LIX', 'LX', 'LXI', 'LXII', 'LXIII', 'LXIV', 'LXV', 'LXVI', 'LXVII', 'LXVIII', 'LXIX', 'LXX', 'LXXI', 'LXXII', 'LXXIII', 'LXXIV', 'LXXV', 'LXXVI', 'LXXVII', 'LXXVIII', 'LXXIX', 'LXXX', 'LXXXI', 'LXXXII', 'LXXXIII', 'LXXXIV', 'LXXXV', 'LXXXVI', 'LXXXVII', 'LXXXVIII', 'LXXXIX', 'XC', 'XCI', 'XCII', 'XCIII', 'XCIV', 'VC', 'VCI', 'VCII', 'VCIII', 'IC', 'C', 'CI', 'CII', 'CIII', 'CIV', 'CV', 'CVI', 'CVII', 'CVIII', 'CIX', 'CX', 'CXI', 'CXII', 'CXIII', 'CXIV', 'CXV', 'CXVI', 'CXVII', 'CXVIII', 'CXIX', 'CXX', 'CXXI', 'CXXII', 'CXXIII', 'CXXIV', 'CXXV', 'CXXVI', 'CXXVII', 'CXXVIII', 'CXXIX', 'CXXX', 'CXXXI', 'CXXXII', 'CXXXIII', 'CXXXIV', 'CXXXV', 'CXXXVI', 'CXXXVII', 'CXXXVIII', 'CXXXIX', 'CXL', 'CXLI', 'CXLII', 'CXLIII', 'CXLIV', 'CVL', 'CVLI', 'CVLII', 'CVLIII', 'CIL', 'CL', 'CLI', 'CLII', 'CLIII', 'CLIV', 'CLV', 'CLVI', 'CLVII', 'CLVIII', 'CLIX', 'CLX', 'CLXI', 'CLXII', 'CLXIII', 'CLXIV', 'CLXV', 'CLXVI', 'CLXVII', 'CLXVIII', 'CLXIX', 'CLXX', 'CLXXI', 'CLXXII', 'CLXXIII', 'CLXXIV', 'CLXXV', 'CLXXVI', 'CLXXVII', 'CLXXVIII', 'CLXXIX', 'CLXXX', 'CLXXXI', 'CLXXXII', 'CLXXXIII', 'CLXXXIV', 'CLXXXV', 'CLXXXVI', 'CLXXXVII', 'CLXXXVIII', 'CLXXXIX', 'CXC', 'CXCI', 'CXCII', 'CXCIII', 'CXCIV', 'CVC', 'CVCI', 'CVCII', 'CVCIII', 'CIC', 'CC', 'CCI', 'CCII', 'CCIII', 'CCIV', 'CCV', 'CCVI', 'CCVII', 'CCVIII', 'CCIX', 'CCX', 'CCXI', 'CCXII', 'CCXIII', 'CCXIV', 'CCXV', 'CCXVI', 'CCXVII', 'CCXVIII', 'CCXIX', 'CCXX', 'CCXXI', 'CCXXII', 'CCXXIII', 'CCXXIV', 'CCXXV', 'CCXXVI', 'CCXXVII', 'CCXXVIII', 'CCXXIX', 'CCXXX', 'CCXXXI', 'CCXXXII', 'CCXXXIII', 'CCXXXIV', 'CCXXXV', 'CCXXXVI', 'CCXXXVII', 'CCXXXVIII', 'CCXXXIX', 'CCXL', 'CCXLI', 'CCXLII', 'CCXLIII', 'CCXLIV', 'CCVL', 'CCVLI', 'CCVLII', 'CCVLIII', 'CCIL', 'CCL', 'CCLI', 'CCLII', 'CCLIII', 'CCLIV', 'CCLV', 'CCLVI', 'CCLVII', 'CCLVIII', 'CCLIX', 'CCLX', 'CCLXI', 'CCLXII', 'CCLXIII', 'CCLXIV', 'CCLXV', 'CCLXVI', 'CCLXVII', 'CCLXVIII', 'CCLXIX', 'CCLXX', 'CCLXXI', 'CCLXXII', 'CCLXXIII', 'CCLXXIV', 'CCLXXV', 'CCLXXVI', 'CCLXXVII', 'CCLXXVIII', 'CCLXXIX', 'CCLXXX', 'CCLXXXI', 'CCLXXXII', 'CCLXXXIII', 'CCLXXXIV', 'CCLXXXV', 'CCLXXXVI', 'CCLXXXVII', 'CCLXXXVIII', 'CCLXXXIX', 'CCXC', 'CCXCI', 'CCXCII', 'CCXCIII', 'CCXCIV', 'CCVC', 'CCVCI', 'CCVCII', 'CCVCIII', 'CCIC', 'CCC', 'CCCI', 'CCCII', 'CCCIII', 'CCCIV', 'CCCV', 'CCCVI', 'CCCVII', 'CCCVIII', 'CCCIX', 'CCCX', 'CCCXI', 'CCCXII', 'CCCXIII', 'CCCXIV', 'CCCXV', 'CCCXVI', 'CCCXVII', 'CCCXVIII', 'CCCXIX', 'CCCXX', 'CCCXXI', 'CCCXXII', 'CCCXXIII', 'CCCXXIV', 'CCCXXV', 'CCCXXVI', 'CCCXXVII', 'CCCXXVIII', 'CCCXXIX', 'CCCXXX', 'CCCXXXI', 'CCCXXXII', 'CCCXXXIII', 'CCCXXXIV', 'CCCXXXV', 'CCCXXXVI', 'CCCXXXVII', 'CCCXXXVIII', 'CCCXXXIX', 'CCCXL', 'CCCXLI', 'CCCXLII', 'CCCXLIII', 'CCCXLIV', 'CCCVL', 'CCCVLI', 'CCCVLII', 'CCCVLIII', 'CCCIL', 'CCCL', 'CCCLI', 'CCCLII', 'CCCLIII', 'CCCLIV', 'CCCLV', 'CCCLVI', 'CCCLVII', 'CCCLVIII', 'CCCLIX', 'CCCLX', 'CCCLXI', 'CCCLXII', 'CCCLXIII', 'CCCLXIV', 'CCCLXV', 'CCCLXVI', 'CCCLXVII', 'CCCLXVIII', 'CCCLXIX', 'CCCLXX', 'CCCLXXI', 'CCCLXXII', 'CCCLXXIII', 'CCCLXXIV', 'CCCLXXV', 'CCCLXXVI', 'CCCLXXVII', 'CCCLXXVIII', 'CCCLXXIX', 'CCCLXXX', 'CCCLXXXI', 'CCCLXXXII', 'CCCLXXXIII', 'CCCLXXXIV', 'CCCLXXXV', 'CCCLXXXVI', 'CCCLXXXVII', 'CCCLXXXVIII', 'CCCLXXXIX', 'CCCXC', 'CCCXCI', 'CCCXCII', 'CCCXCIII', 'CCCXCIV', 'CCCVC', 'CCCVCI', 'CCCVCII', 'CCCVCIII', 'CCCIC', 'CD', 'CDI', 'CDII', 'CDIII', 'CDIV', 'CDV', 'CDVI', 'CDVII', 'CDVIII', 'CDIX', 'CDX', 'CDXI', 'CDXII', 'CDXIII', 'CDXIV', 'CDXV', 'CDXVI', 'CDXVII', 'CDXVIII', 'CDXIX', 'CDXX', 'CDXXI', 'CDXXII', 'CDXXIII', 'CDXXIV', 'CDXXV', 'CDXXVI', 'CDXXVII', 'CDXXVIII', 'CDXXIX', 'CDXXX', 'CDXXXI', 'CDXXXII', 'CDXXXIII', 'CDXXXIV', 'CDXXXV', 'CDXXXVI', 'CDXXXVII', 'CDXXXVIII', 'CDXXXIX', 'CDXL', 'CDXLI', 'CDXLII', 'CDXLIII', 'CDXLIV', 'CDVL', 'CDVLI', 'CDVLII', 'CDVLIII', 'CDIL', 'LD', 'LDI', 'LDII', 'LDIII', 'LDIV', 'LDV', 'LDVI', 'LDVII', 'LDVIII', 'LDIX', 'LDX', 'LDXI', 'LDXII', 'LDXIII', 'LDXIV', 'LDXV', 'LDXVI', 'LDXVII', 'LDXVIII', 'LDXIX', 'LDXX', 'LDXXI', 'LDXXII', 'LDXXIII', 'LDXXIV', 'LDXXV', 'LDXXVI', 'LDXXVII', 'LDXXVIII', 'LDXXIX', 'LDXXX', 'LDXXXI', 'LDXXXII', 'LDXXXIII', 'LDXXXIV', 'LDXXXV', 'LDXXXVI', 'LDXXXVII', 'LDXXXVIII', 'LDXXXIX', 'XD', 'XDI', 'XDII', 'XDIII', 'XDIV', 'VD', 'VDI', 'VDII', 'VDIII', 'VDIV', 'D', 'DI', 'DII', 'DIII', 'DIV', 'DV', 'DVI', 'DVII', 'DVIII', 'DIX', 'DX', 'DXI', 'DXII', 'DXIII', 'DXIV', 'DXV', 'DXVI', 'DXVII', 'DXVIII', 'DXIX', 'DXX', 'DXXI', 'DXXII', 'DXXIII', 'DXXIV', 'DXXV', 'DXXVI', 'DXXVII', 'DXXVIII', 'DXXIX', 'DXXX', 'DXXXI', 'DXXXII', 'DXXXIII', 'DXXXIV', 'DXXXV', 'DXXXVI', 'DXXXVII', 'DXXXVIII', 'DXXXIX', 'DXL', 'DXLI', 'DXLII', 'DXLIII', 'DXLIV', 'DVL', 'DVLI', 'DVLII', 'DVLIII', 'DIL', 'DL', 'DLI', 'DLII', 'DLIII', 'DLIV', 'DLV', 'DLVI', 'DLVII', 'DLVIII', 'DLIX', 'DLX', 'DLXI', 'DLXII', 'DLXIII', 'DLXIV', 'DLXV', 'DLXVI', 'DLXVII', 'DLXVIII', 'DLXIX', 'DLXX', 'DLXXI', 'DLXXII', 'DLXXIII', 'DLXXIV', 'DLXXV', 'DLXXVI', 'DLXXVII', 'DLXXVIII', 'DLXXIX', 'DLXXX', 'DLXXXI', 'DLXXXII', 'DLXXXIII', 'DLXXXIV', 'DLXXXV', 'DLXXXVI', 'DLXXXVII', 'DLXXXVIII', 'DLXXXIX', 'DXC', 'DXCI', 'DXCII', 'DXCIII', 'DXCIV', 'DVC', 'DVCI', 'DVCII', 'DVCIII', 'DIC', 'DC', 'DCI', 'DCII', 'DCIII', 'DCIV', 'DCV', 'DCVI', 'DCVII', 'DCVIII', 'DCIX', 'DCX', 'DCXI', 'DCXII', 'DCXIII', 'DCXIV', 'DCXV', 'DCXVI', 'DCXVII', 'DCXVIII', 'DCXIX', 'DCXX', 'DCXXI', 'DCXXII', 'DCXXIII', 'DCXXIV', 'DCXXV', 'DCXXVI', 'DCXXVII', 'DCXXVIII', 'DCXXIX', 'DCXXX', 'DCXXXI', 'DCXXXII', 'DCXXXIII', 'DCXXXIV', 'DCXXXV', 'DCXXXVI', 'DCXXXVII', 'DCXXXVIII', 'DCXXXIX', 'DCXL', 'DCXLI', 'DCXLII', 'DCXLIII', 'DCXLIV', 'DCVL', 'DCVLI', 'DCVLII', 'DCVLIII', 'DCIL', 'DCL', 'DCLI', 'DCLII', 'DCLIII', 'DCLIV', 'DCLV', 'DCLVI', 'DCLVII', 'DCLVIII', 'DCLIX', 'DCLX', 'DCLXI', 'DCLXII', 'DCLXIII', 'DCLXIV', 'DCLXV', 'DCLXVI', 'DCLXVII', 'DCLXVIII', 'DCLXIX', 'DCLXX', 'DCLXXI', 'DCLXXII', 'DCLXXIII', 'DCLXXIV', 'DCLXXV', 'DCLXXVI', 'DCLXXVII', 'DCLXXVIII', 'DCLXXIX', 'DCLXXX', 'DCLXXXI', 'DCLXXXII', 'DCLXXXIII', 'DCLXXXIV', 'DCLXXXV', 'DCLXXXVI', 'DCLXXXVII', 'DCLXXXVIII', 'DCLXXXIX', 'DCXC', 'DCXCI', 'DCXCII', 'DCXCIII', 'DCXCIV', 'DCVC', 'DCVCI', 'DCVCII', 'DCVCIII', 'DCIC', 'DCC', 'DCCI', 'DCCII', 'DCCIII', 'DCCIV', 'DCCV', 'DCCVI', 'DCCVII', 'DCCVIII', 'DCCIX', 'DCCX', 'DCCXI', 'DCCXII', 'DCCXIII', 'DCCXIV', 'DCCXV', 'DCCXVI', 'DCCXVII', 'DCCXVIII', 'DCCXIX', 'DCCXX', 'DCCXXI', 'DCCXXII', 'DCCXXIII', 'DCCXXIV', 'DCCXXV', 'DCCXXVI', 'DCCXXVII', 'DCCXXVIII', 'DCCXXIX', 'DCCXXX', 'DCCXXXI', 'DCCXXXII', 'DCCXXXIII', 'DCCXXXIV', 'DCCXXXV', 'DCCXXXVI', 'DCCXXXVII', 'DCCXXXVIII', 'DCCXXXIX', 'DCCXL', 'DCCXLI', 'DCCXLII', 'DCCXLIII', 'DCCXLIV', 'DCCVL', 'DCCVLI', 'DCCVLII', 'DCCVLIII', 'DCCIL', 'DCCL', 'DCCLI', 'DCCLII', 'DCCLIII', 'DCCLIV', 'DCCLV', 'DCCLVI', 'DCCLVII', 'DCCLVIII', 'DCCLIX', 'DCCLX', 'DCCLXI', 'DCCLXII', 'DCCLXIII', 'DCCLXIV', 'DCCLXV', 'DCCLXVI', 'DCCLXVII', 'DCCLXVIII', 'DCCLXIX', 'DCCLXX', 'DCCLXXI', 'DCCLXXII', 'DCCLXXIII', 'DCCLXXIV', 'DCCLXXV', 'DCCLXXVI', 'DCCLXXVII', 'DCCLXXVIII', 'DCCLXXIX', 'DCCLXXX', 'DCCLXXXI', 'DCCLXXXII', 'DCCLXXXIII', 'DCCLXXXIV', 'DCCLXXXV', 'DCCLXXXVI', 'DCCLXXXVII', 'DCCLXXXVIII', 'DCCLXXXIX', 'DCCXC', 'DCCXCI', 'DCCXCII', 'DCCXCIII', 'DCCXCIV', 'DCCVC', 'DCCVCI', 'DCCVCII', 'DCCVCIII', 'DCCIC', 'DCCC', 'DCCCI', 'DCCCII', 'DCCCIII', 'DCCCIV', 'DCCCV', 'DCCCVI', 'DCCCVII', 'DCCCVIII', 'DCCCIX', 'DCCCX', 'DCCCXI', 'DCCCXII', 'DCCCXIII', 'DCCCXIV', 'DCCCXV', 'DCCCXVI', 'DCCCXVII', 'DCCCXVIII', 'DCCCXIX', 'DCCCXX', 'DCCCXXI', 'DCCCXXII', 'DCCCXXIII', 'DCCCXXIV', 'DCCCXXV', 'DCCCXXVI', 'DCCCXXVII', 'DCCCXXVIII', 'DCCCXXIX', 'DCCCXXX', 'DCCCXXXI', 'DCCCXXXII', 'DCCCXXXIII', 'DCCCXXXIV', 'DCCCXXXV', 'DCCCXXXVI', 'DCCCXXXVII', 'DCCCXXXVIII', 'DCCCXXXIX', 'DCCCXL', 'DCCCXLI', 'DCCCXLII', 'DCCCXLIII', 'DCCCXLIV', 'DCCCVL', 'DCCCVLI', 'DCCCVLII', 'DCCCVLIII', 'DCCCIL', 'DCCCL', 'DCCCLI', 'DCCCLII', 'DCCCLIII', 'DCCCLIV', 'DCCCLV', 'DCCCLVI', 'DCCCLVII', 'DCCCLVIII', 'DCCCLIX', 'DCCCLX', 'DCCCLXI', 'DCCCLXII', 'DCCCLXIII', 'DCCCLXIV', 'DCCCLXV', 'DCCCLXVI', 'DCCCLXVII', 'DCCCLXVIII', 'DCCCLXIX', 'DCCCLXX', 'DCCCLXXI', 'DCCCLXXII', 'DCCCLXXIII', 'DCCCLXXIV', 'DCCCLXXV', 'DCCCLXXVI', 'DCCCLXXVII', 'DCCCLXXVIII', 'DCCCLXXIX', 'DCCCLXXX', 'DCCCLXXXI', 'DCCCLXXXII', 'DCCCLXXXIII', 'DCCCLXXXIV', 'DCCCLXXXV', 'DCCCLXXXVI', 'DCCCLXXXVII', 'DCCCLXXXVIII', 'DCCCLXXXIX', 'DCCCXC', 'DCCCXCI', 'DCCCXCII', 'DCCCXCIII', 'DCCCXCIV', 'DCCCVC', 'DCCCVCI', 'DCCCVCII', 'DCCCVCIII', 'DCCCIC', 'CM', 'CMI', 'CMII', 'CMIII', 'CMIV', 'CMV', 'CMVI', 'CMVII', 'CMVIII', 'CMIX', 'CMX', 'CMXI', 'CMXII', 'CMXIII', 'CMXIV', 'CMXV', 'CMXVI', 'CMXVII', 'CMXVIII', 'CMXIX', 'CMXX', 'CMXXI', 'CMXXII', 'CMXXIII', 'CMXXIV', 'CMXXV', 'CMXXVI', 'CMXXVII', 'CMXXVIII', 'CMXXIX', 'CMXXX', 'CMXXXI', 'CMXXXII', 'CMXXXIII', 'CMXXXIV', 'CMXXXV', 'CMXXXVI', 'CMXXXVII', 'CMXXXVIII', 'CMXXXIX', 'CMXL', 'CMXLI', 'CMXLII', 'CMXLIII', 'CMXLIV', 'CMVL', 'CMVLI', 'CMVLII', 'CMVLIII', 'CMIL', 'LM', 'LMI', 'LMII', 'LMIII', 'LMIV', 'LMV', 'LMVI', 'LMVII', 'LMVIII', 'LMIX', 'LMX', 'LMXI', 'LMXII', 'LMXIII', 'LMXIV', 'LMXV', 'LMXVI', 'LMXVII', 'LMXVIII', 'LMXIX', 'LMXX', 'LMXXI', 'LMXXII', 'LMXXIII', 'LMXXIV', 'LMXXV', 'LMXXVI', 'LMXXVII', 'LMXXVIII', 'LMXXIX', 'LMXXX', 'LMXXXI', 'LMXXXII', 'LMXXXIII', 'LMXXXIV', 'LMXXXV', 'LMXXXVI', 'LMXXXVII', 'LMXXXVIII', 'LMXXXIX', 'XM', 'XMI', 'XMII', 'XMIII', 'XMIV', 'VM', 'VMI', 'VMII', 'VMIII', 'VMIV', 'M', 'MI', 'MII', 'MIII', 'MIV', 'MV', 'MVI', 'MVII', 'MVIII', 'MIX', 'MX', 'MXI', 'MXII', 'MXIII', 'MXIV', 'MXV', 'MXVI', 'MXVII', 'MXVIII', 'MXIX', 'MXX', 'MXXI', 'MXXII', 'MXXIII', 'MXXIV', 'MXXV', 'MXXVI', 'MXXVII', 'MXXVIII', 'MXXIX', 'MXXX', 'MXXXI', 'MXXXII', 'MXXXIII', 'MXXXIV', 'MXXXV', 'MXXXVI', 'MXXXVII', 'MXXXVIII', 'MXXXIX', 'MXL', 'MXLI', 'MXLII', 'MXLIII', 'MXLIV', 'MVL', 'MVLI', 'MVLII', 'MVLIII', 'MIL', 'ML', 'MLI', 'MLII', 'MLIII', 'MLIV', 'MLV', 'MLVI', 'MLVII', 'MLVIII', 'MLIX', 'MLX', 'MLXI', 'MLXII', 'MLXIII', 'MLXIV', 'MLXV', 'MLXVI', 'MLXVII', 'MLXVIII', 'MLXIX', 'MLXX', 'MLXXI', 'MLXXII', 'MLXXIII', 'MLXXIV', 'MLXXV', 'MLXXVI', 'MLXXVII', 'MLXXVIII', 'MLXXIX', 'MLXXX', 'MLXXXI', 'MLXXXII', 'MLXXXIII', 'MLXXXIV', 'MLXXXV', 'MLXXXVI', 'MLXXXVII', 'MLXXXVIII', 'MLXXXIX', 'MXC', 'MXCI', 'MXCII', 'MXCIII', 'MXCIV', 'MVC', 'MVCI', 'MVCII', 'MVCIII', 'MIC', 'MC', 'MCI', 'MCII', 'MCIII', 'MCIV', 'MCV', 'MCVI', 'MCVII', 'MCVIII', 'MCIX', 'MCX', 'MCXI', 'MCXII', 'MCXIII', 'MCXIV', 'MCXV', 'MCXVI', 'MCXVII', 'MCXVIII', 'MCXIX', 'MCXX', 'MCXXI', 'MCXXII', 'MCXXIII', 'MCXXIV', 'MCXXV', 'MCXXVI', 'MCXXVII', 'MCXXVIII', 'MCXXIX', 'MCXXX', 'MCXXXI', 'MCXXXII', 'MCXXXIII', 'MCXXXIV', 'MCXXXV', 'MCXXXVI', 'MCXXXVII', 'MCXXXVIII', 'MCXXXIX', 'MCXL', 'MCXLI', 'MCXLII', 'MCXLIII', 'MCXLIV', 'MCVL', 'MCVLI', 'MCVLII', 'MCVLIII', 'MCIL', 'MCL', 'MCLI', 'MCLII', 'MCLIII', 'MCLIV', 'MCLV', 'MCLVI', 'MCLVII', 'MCLVIII', 'MCLIX', 'MCLX', 'MCLXI', 'MCLXII', 'MCLXIII', 'MCLXIV', 'MCLXV', 'MCLXVI', 'MCLXVII', 'MCLXVIII', 'MCLXIX', 'MCLXX', 'MCLXXI', 'MCLXXII', 'MCLXXIII', 'MCLXXIV', 'MCLXXV', 'MCLXXVI', 'MCLXXVII', 'MCLXXVIII', 'MCLXXIX', 'MCLXXX', 'MCLXXXI', 'MCLXXXII', 'MCLXXXIII', 'MCLXXXIV', 'MCLXXXV', 'MCLXXXVI', 'MCLXXXVII', 'MCLXXXVIII', 'MCLXXXIX', 'MCXC', 'MCXCI', 'MCXCII', 'MCXCIII', 'MCXCIV', 'MCVC', 'MCVCI', 'MCVCII', 'MCVCIII', 'MCIC', 'MCC', 'MCCI', 'MCCII', 'MCCIII', 'MCCIV', 'MCCV', 'MCCVI', 'MCCVII', 'MCCVIII', 'MCCIX', 'MCCX', 'MCCXI', 'MCCXII', 'MCCXIII', 'MCCXIV', 'MCCXV', 'MCCXVI', 'MCCXVII', 'MCCXVIII', 'MCCXIX', 'MCCXX', 'MCCXXI', 'MCCXXII', 'MCCXXIII', 'MCCXXIV', 'MCCXXV', 'MCCXXVI', 'MCCXXVII', 'MCCXXVIII', 'MCCXXIX', 'MCCXXX', 'MCCXXXI', 'MCCXXXII', 'MCCXXXIII', 'MCCXXXIV', 'MCCXXXV', 'MCCXXXVI', 'MCCXXXVII', 'MCCXXXVIII', 'MCCXXXIX', 'MCCXL', 'MCCXLI', 'MCCXLII', 'MCCXLIII', 'MCCXLIV', 'MCCVL', 'MCCVLI', 'MCCVLII', 'MCCVLIII', 'MCCIL', 'MCCL', 'MCCLI', 'MCCLII', 'MCCLIII', 'MCCLIV', 'MCCLV', 'MCCLVI', 'MCCLVII', 'MCCLVIII', 'MCCLIX', 'MCCLX', 'MCCLXI', 'MCCLXII', 'MCCLXIII', 'MCCLXIV', 'MCCLXV', 'MCCLXVI', 'MCCLXVII', 'MCCLXVIII', 'MCCLXIX', 'MCCLXX', 'MCCLXXI', 'MCCLXXII', 'MCCLXXIII', 'MCCLXXIV', 'MCCLXXV', 'MCCLXXVI', 'MCCLXXVII', 'MCCLXXVIII', 'MCCLXXIX', 'MCCLXXX', 'MCCLXXXI', 'MCCLXXXII', 'MCCLXXXIII', 'MCCLXXXIV', 'MCCLXXXV', 'MCCLXXXVI', 'MCCLXXXVII', 'MCCLXXXVIII', 'MCCLXXXIX', 'MCCXC', 'MCCXCI', 'MCCXCII', 'MCCXCIII', 'MCCXCIV', 'MCCVC', 'MCCVCI', 'MCCVCII', 'MCCVCIII', 'MCCIC', 'MCCC', 'MCCCI', 'MCCCII', 'MCCCIII', 'MCCCIV', 'MCCCV', 'MCCCVI', 'MCCCVII', 'MCCCVIII', 'MCCCIX', 'MCCCX', 'MCCCXI', 'MCCCXII', 'MCCCXIII', 'MCCCXIV', 'MCCCXV', 'MCCCXVI', 'MCCCXVII', 'MCCCXVIII', 'MCCCXIX', 'MCCCXX', 'MCCCXXI', 'MCCCXXII', 'MCCCXXIII', 'MCCCXXIV', 'MCCCXXV', 'MCCCXXVI', 'MCCCXXVII', 'MCCCXXVIII', 'MCCCXXIX', 'MCCCXXX', 'MCCCXXXI', 'MCCCXXXII', 'MCCCXXXIII', 'MCCCXXXIV', 'MCCCXXXV', 'MCCCXXXVI', 'MCCCXXXVII', 'MCCCXXXVIII', 'MCCCXXXIX', 'MCCCXL', 'MCCCXLI', 'MCCCXLII', 'MCCCXLIII', 'MCCCXLIV', 'MCCCVL', 'MCCCVLI', 'MCCCVLII', 'MCCCVLIII', 'MCCCIL', 'MCCCL', 'MCCCLI', 'MCCCLII', 'MCCCLIII', 'MCCCLIV', 'MCCCLV', 'MCCCLVI', 'MCCCLVII', 'MCCCLVIII', 'MCCCLIX', 'MCCCLX', 'MCCCLXI', 'MCCCLXII', 'MCCCLXIII', 'MCCCLXIV', 'MCCCLXV', 'MCCCLXVI', 'MCCCLXVII', 'MCCCLXVIII', 'MCCCLXIX', 'MCCCLXX', 'MCCCLXXI', 'MCCCLXXII', 'MCCCLXXIII', 'MCCCLXXIV', 'MCCCLXXV', 'MCCCLXXVI', 'MCCCLXXVII', 'MCCCLXXVIII', 'MCCCLXXIX', 'MCCCLXXX', 'MCCCLXXXI', 'MCCCLXXXII', 'MCCCLXXXIII', 'MCCCLXXXIV', 'MCCCLXXXV', 'MCCCLXXXVI', 'MCCCLXXXVII', 'MCCCLXXXVIII', 'MCCCLXXXIX', 'MCCCXC', 'MCCCXCI', 'MCCCXCII', 'MCCCXCIII', 'MCCCXCIV', 'MCCCVC', 'MCCCVCI', 'MCCCVCII', 'MCCCVCIII', 'MCCCIC', 'MCD', 'MCDI', 'MCDII', 'MCDIII', 'MCDIV', 'MCDV', 'MCDVI', 'MCDVII', 'MCDVIII', 'MCDIX', 'MCDX', 'MCDXI', 'MCDXII', 'MCDXIII', 'MCDXIV', 'MCDXV', 'MCDXVI', 'MCDXVII', 'MCDXVIII', 'MCDXIX', 'MCDXX', 'MCDXXI', 'MCDXXII', 'MCDXXIII', 'MCDXXIV', 'MCDXXV', 'MCDXXVI', 'MCDXXVII', 'MCDXXVIII', 'MCDXXIX', 'MCDXXX', 'MCDXXXI', 'MCDXXXII', 'MCDXXXIII', 'MCDXXXIV', 'MCDXXXV', 'MCDXXXVI', 'MCDXXXVII', 'MCDXXXVIII', 'MCDXXXIX', 'MCDXL', 'MCDXLI', 'MCDXLII', 'MCDXLIII', 'MCDXLIV', 'MCDVL', 'MCDVLI', 'MCDVLII', 'MCDVLIII', 'MCDIL', 'MLD', 'MLDI', 'MLDII', 'MLDIII', 'MLDIV', 'MLDV', 'MLDVI', 'MLDVII', 'MLDVIII', 'MLDIX', 'MLDX', 'MLDXI', 'MLDXII', 'MLDXIII', 'MLDXIV', 'MLDXV', 'MLDXVI', 'MLDXVII', 'MLDXVIII', 'MLDXIX', 'MLDXX', 'MLDXXI', 'MLDXXII', 'MLDXXIII', 'MLDXXIV', 'MLDXXV', 'MLDXXVI', 'MLDXXVII', 'MLDXXVIII', 'MLDXXIX', 'MLDXXX', 'MLDXXXI', 'MLDXXXII', 'MLDXXXIII', 'MLDXXXIV', 'MLDXXXV', 'MLDXXXVI', 'MLDXXXVII', 'MLDXXXVIII', 'MLDXXXIX', 'MXD', 'MXDI', 'MXDII', 'MXDIII', 'MXDIV', 'MVD', 'MVDI', 'MVDII', 'MVDIII', 'MVDIV', 'MD', 'MDI', 'MDII', 'MDIII', 'MDIV', 'MDV', 'MDVI', 'MDVII', 'MDVIII', 'MDIX', 'MDX', 'MDXI', 'MDXII', 'MDXIII', 'MDXIV', 'MDXV', 'MDXVI', 'MDXVII', 'MDXVIII', 'MDXIX', 'MDXX', 'MDXXI', 'MDXXII', 'MDXXIII', 'MDXXIV', 'MDXXV', 'MDXXVI', 'MDXXVII', 'MDXXVIII', 'MDXXIX', 'MDXXX', 'MDXXXI', 'MDXXXII', 'MDXXXIII', 'MDXXXIV', 'MDXXXV', 'MDXXXVI', 'MDXXXVII', 'MDXXXVIII', 'MDXXXIX', 'MDXL', 'MDXLI', 'MDXLII', 'MDXLIII', 'MDXLIV', 'MDVL', 'MDVLI', 'MDVLII', 'MDVLIII', 'MDIL', 'MDL', 'MDLI', 'MDLII', 'MDLIII', 'MDLIV', 'MDLV', 'MDLVI', 'MDLVII', 'MDLVIII', 'MDLIX', 'MDLX', 'MDLXI', 'MDLXII', 'MDLXIII', 'MDLXIV', 'MDLXV', 'MDLXVI', 'MDLXVII', 'MDLXVIII', 'MDLXIX', 'MDLXX', 'MDLXXI', 'MDLXXII', 'MDLXXIII', 'MDLXXIV', 'MDLXXV', 'MDLXXVI', 'MDLXXVII', 'MDLXXVIII', 'MDLXXIX', 'MDLXXX', 'MDLXXXI', 'MDLXXXII', 'MDLXXXIII', 'MDLXXXIV', 'MDLXXXV', 'MDLXXXVI', 'MDLXXXVII', 'MDLXXXVIII', 'MDLXXXIX', 'MDXC', 'MDXCI', 'MDXCII', 'MDXCIII', 'MDXCIV', 'MDVC', 'MDVCI', 'MDVCII', 'MDVCIII', 'MDIC', 'MDC', 'MDCI', 'MDCII', 'MDCIII', 'MDCIV', 'MDCV', 'MDCVI', 'MDCVII', 'MDCVIII', 'MDCIX', 'MDCX', 'MDCXI', 'MDCXII', 'MDCXIII', 'MDCXIV', 'MDCXV', 'MDCXVI', 'MDCXVII', 'MDCXVIII', 'MDCXIX', 'MDCXX', 'MDCXXI', 'MDCXXII', 'MDCXXIII', 'MDCXXIV', 'MDCXXV', 'MDCXXVI', 'MDCXXVII', 'MDCXXVIII', 'MDCXXIX', 'MDCXXX', 'MDCXXXI', 'MDCXXXII', 'MDCXXXIII', 'MDCXXXIV', 'MDCXXXV', 'MDCXXXVI', 'MDCXXXVII', 'MDCXXXVIII', 'MDCXXXIX', 'MDCXL', 'MDCXLI', 'MDCXLII', 'MDCXLIII', 'MDCXLIV', 'MDCVL', 'MDCVLI', 'MDCVLII', 'MDCVLIII', 'MDCIL', 'MDCL', 'MDCLI', 'MDCLII', 'MDCLIII', 'MDCLIV', 'MDCLV', 'MDCLVI', 'MDCLVII', 'MDCLVIII', 'MDCLIX', 'MDCLX', 'MDCLXI', 'MDCLXII', 'MDCLXIII', 'MDCLXIV', 'MDCLXV', 'MDCLXVI', 'MDCLXVII', 'MDCLXVIII', 'MDCLXIX', 'MDCLXX', 'MDCLXXI', 'MDCLXXII', 'MDCLXXIII', 'MDCLXXIV', 'MDCLXXV', 'MDCLXXVI', 'MDCLXXVII', 'MDCLXXVIII', 'MDCLXXIX', 'MDCLXXX', 'MDCLXXXI', 'MDCLXXXII', 'MDCLXXXIII', 'MDCLXXXIV', 'MDCLXXXV', 'MDCLXXXVI', 'MDCLXXXVII', 'MDCLXXXVIII', 'MDCLXXXIX', 'MDCXC', 'MDCXCI', 'MDCXCII', 'MDCXCIII', 'MDCXCIV', 'MDCVC', 'MDCVCI', 'MDCVCII', 'MDCVCIII', 'MDCIC', 'MDCC', 'MDCCI', 'MDCCII', 'MDCCIII', 'MDCCIV', 'MDCCV', 'MDCCVI', 'MDCCVII', 'MDCCVIII', 'MDCCIX', 'MDCCX', 'MDCCXI', 'MDCCXII', 'MDCCXIII', 'MDCCXIV', 'MDCCXV', 'MDCCXVI', 'MDCCXVII', 'MDCCXVIII', 'MDCCXIX', 'MDCCXX', 'MDCCXXI', 'MDCCXXII', 'MDCCXXIII', 'MDCCXXIV', 'MDCCXXV', 'MDCCXXVI', 'MDCCXXVII', 'MDCCXXVIII', 'MDCCXXIX', 'MDCCXXX', 'MDCCXXXI', 'MDCCXXXII', 'MDCCXXXIII', 'MDCCXXXIV', 'MDCCXXXV', 'MDCCXXXVI', 'MDCCXXXVII', 'MDCCXXXVIII', 'MDCCXXXIX', 'MDCCXL', 'MDCCXLI', 'MDCCXLII', 'MDCCXLIII', 'MDCCXLIV', 'MDCCVL', 'MDCCVLI', 'MDCCVLII', 'MDCCVLIII', 'MDCCIL', 'MDCCL', 'MDCCLI', 'MDCCLII', 'MDCCLIII', 'MDCCLIV', 'MDCCLV', 'MDCCLVI', 'MDCCLVII', 'MDCCLVIII', 'MDCCLIX', 'MDCCLX', 'MDCCLXI', 'MDCCLXII', 'MDCCLXIII', 'MDCCLXIV', 'MDCCLXV', 'MDCCLXVI', 'MDCCLXVII', 'MDCCLXVIII', 'MDCCLXIX', 'MDCCLXX', 'MDCCLXXI', 'MDCCLXXII', 'MDCCLXXIII', 'MDCCLXXIV', 'MDCCLXXV', 'MDCCLXXVI', 'MDCCLXXVII', 'MDCCLXXVIII', 'MDCCLXXIX', 'MDCCLXXX', 'MDCCLXXXI', 'MDCCLXXXII', 'MDCCLXXXIII', 'MDCCLXXXIV', 'MDCCLXXXV', 'MDCCLXXXVI', 'MDCCLXXXVII', 'MDCCLXXXVIII', 'MDCCLXXXIX', 'MDCCXC', 'MDCCXCI', 'MDCCXCII', 'MDCCXCIII', 'MDCCXCIV', 'MDCCVC', 'MDCCVCI', 'MDCCVCII', 'MDCCVCIII', 'MDCCIC', 'MDCCC', 'MDCCCI', 'MDCCCII', 'MDCCCIII', 'MDCCCIV', 'MDCCCV', 'MDCCCVI', 'MDCCCVII', 'MDCCCVIII', 'MDCCCIX', 'MDCCCX', 'MDCCCXI', 'MDCCCXII', 'MDCCCXIII', 'MDCCCXIV', 'MDCCCXV', 'MDCCCXVI', 'MDCCCXVII', 'MDCCCXVIII', 'MDCCCXIX', 'MDCCCXX', 'MDCCCXXI', 'MDCCCXXII', 'MDCCCXXIII', 'MDCCCXXIV', 'MDCCCXXV', 'MDCCCXXVI', 'MDCCCXXVII', 'MDCCCXXVIII', 'MDCCCXXIX', 'MDCCCXXX', 'MDCCCXXXI', 'MDCCCXXXII', 'MDCCCXXXIII', 'MDCCCXXXIV', 'MDCCCXXXV', 'MDCCCXXXVI', 'MDCCCXXXVII', 'MDCCCXXXVIII', 'MDCCCXXXIX', 'MDCCCXL', 'MDCCCXLI', 'MDCCCXLII', 'MDCCCXLIII', 'MDCCCXLIV', 'MDCCCVL', 'MDCCCVLI', 'MDCCCVLII', 'MDCCCVLIII', 'MDCCCIL', 'MDCCCL', 'MDCCCLI', 'MDCCCLII', 'MDCCCLIII', 'MDCCCLIV', 'MDCCCLV', 'MDCCCLVI', 'MDCCCLVII', 'MDCCCLVIII', 'MDCCCLIX', 'MDCCCLX', 'MDCCCLXI', 'MDCCCLXII', 'MDCCCLXIII', 'MDCCCLXIV', 'MDCCCLXV', 'MDCCCLXVI', 'MDCCCLXVII', 'MDCCCLXVIII', 'MDCCCLXIX', 'MDCCCLXX', 'MDCCCLXXI', 'MDCCCLXXII', 'MDCCCLXXIII', 'MDCCCLXXIV', 'MDCCCLXXV', 'MDCCCLXXVI', 'MDCCCLXXVII', 'MDCCCLXXVIII', 'MDCCCLXXIX', 'MDCCCLXXX', 'MDCCCLXXXI', 'MDCCCLXXXII', 'MDCCCLXXXIII', 'MDCCCLXXXIV', 'MDCCCLXXXV', 'MDCCCLXXXVI', 'MDCCCLXXXVII', 'MDCCCLXXXVIII', 'MDCCCLXXXIX', 'MDCCCXC', 'MDCCCXCI', 'MDCCCXCII', 'MDCCCXCIII', 'MDCCCXCIV', 'MDCCCVC', 'MDCCCVCI', 'MDCCCVCII', 'MDCCCVCIII', 'MDCCCIC', 'MCM', 'MCMI', 'MCMII', 'MCMIII', 'MCMIV', 'MCMV', 'MCMVI', 'MCMVII', 'MCMVIII', 'MCMIX', 'MCMX', 'MCMXI', 'MCMXII', 'MCMXIII', 'MCMXIV', 'MCMXV', 'MCMXVI', 'MCMXVII', 'MCMXVIII', 'MCMXIX', 'MCMXX', 'MCMXXI', 'MCMXXII', 'MCMXXIII', 'MCMXXIV', 'MCMXXV', 'MCMXXVI', 'MCMXXVII', 'MCMXXVIII', 'MCMXXIX', 'MCMXXX', 'MCMXXXI', 'MCMXXXII', 'MCMXXXIII', 'MCMXXXIV', 'MCMXXXV', 'MCMXXXVI', 'MCMXXXVII', 'MCMXXXVIII', 'MCMXXXIX', 'MCMXL', 'MCMXLI', 'MCMXLII', 'MCMXLIII', 'MCMXLIV', 'MCMVL', 'MCMVLI', 'MCMVLII', 'MCMVLIII', 'MCMIL', 'MLM', 'MLMI', 'MLMII', 'MLMIII', 'MLMIV', 'MLMV', 'MLMVI', 'MLMVII', 'MLMVIII', 'MLMIX', 'MLMX', 'MLMXI', 'MLMXII', 'MLMXIII', 'MLMXIV', 'MLMXV', 'MLMXVI', 'MLMXVII', 'MLMXVIII', 'MLMXIX', 'MLMXX', 'MLMXXI', 'MLMXXII', 'MLMXXIII', 'MLMXXIV', 'MLMXXV', 'MLMXXVI', 'MLMXXVII', 'MLMXXVIII', 'MLMXXIX', 'MLMXXX', 'MLMXXXI', 'MLMXXXII', 'MLMXXXIII', 'MLMXXXIV', 'MLMXXXV', 'MLMXXXVI', 'MLMXXXVII', 'MLMXXXVIII', 'MLMXXXIX', 'MXM', 'MXMI', 'MXMII', 'MXMIII', 'MXMIV', 'MVM', 'MVMI', 'MVMII', 'MVMIII', 'MVMIV', 'MM', 'MMI', 'MMII', 'MMIII', 'MMIV', 'MMV', 'MMVI', 'MMVII', 'MMVIII', 'MMIX', 'MMX', 'MMXI', 'MMXII', 'MMXIII', 'MMXIV', 'MMXV', 'MMXVI', 'MMXVII', 'MMXVIII', 'MMXIX', 'MMXX', 'MMXXI', 'MMXXII', 'MMXXIII', 'MMXXIV', 'MMXXV', 'MMXXVI', 'MMXXVII', 'MMXXVIII', 'MMXXIX', 'MMXXX', 'MMXXXI', 'MMXXXII', 'MMXXXIII', 'MMXXXIV', 'MMXXXV', 'MMXXXVI', 'MMXXXVII', 'MMXXXVIII', 'MMXXXIX', 'MMXL', 'MMXLI', 'MMXLII', 'MMXLIII', 'MMXLIV', 'MMVL', 'MMVLI', 'MMVLII', 'MMVLIII', 'MMIL', 'MML', 'MMLI', 'MMLII', 'MMLIII', 'MMLIV', 'MMLV', 'MMLVI', 'MMLVII', 'MMLVIII', 'MMLIX', 'MMLX', 'MMLXI', 'MMLXII', 'MMLXIII', 'MMLXIV', 'MMLXV', 'MMLXVI', 'MMLXVII', 'MMLXVIII', 'MMLXIX', 'MMLXX', 'MMLXXI', 'MMLXXII', 'MMLXXIII', 'MMLXXIV', 'MMLXXV', 'MMLXXVI', 'MMLXXVII', 'MMLXXVIII', 'MMLXXIX', 'MMLXXX', 'MMLXXXI', 'MMLXXXII', 'MMLXXXIII', 'MMLXXXIV', 'MMLXXXV', 'MMLXXXVI', 'MMLXXXVII', 'MMLXXXVIII', 'MMLXXXIX', 'MMXC', 'MMXCI', 'MMXCII', 'MMXCIII', 'MMXCIV', 'MMVC', 'MMVCI', 'MMVCII', 'MMVCIII', 'MMIC', 'MMC', 'MMCI', 'MMCII', 'MMCIII', 'MMCIV', 'MMCV', 'MMCVI', 'MMCVII', 'MMCVIII', 'MMCIX', 'MMCX', 'MMCXI', 'MMCXII', 'MMCXIII', 'MMCXIV', 'MMCXV', 'MMCXVI', 'MMCXVII', 'MMCXVIII', 'MMCXIX', 'MMCXX', 'MMCXXI', 'MMCXXII', 'MMCXXIII', 'MMCXXIV', 'MMCXXV', 'MMCXXVI', 'MMCXXVII', 'MMCXXVIII', 'MMCXXIX', 'MMCXXX', 'MMCXXXI', 'MMCXXXII', 'MMCXXXIII', 'MMCXXXIV', 'MMCXXXV', 'MMCXXXVI', 'MMCXXXVII', 'MMCXXXVIII', 'MMCXXXIX', 'MMCXL', 'MMCXLI', 'MMCXLII', 'MMCXLIII', 'MMCXLIV', 'MMCVL', 'MMCVLI', 'MMCVLII', 'MMCVLIII', 'MMCIL', 'MMCL', 'MMCLI', 'MMCLII', 'MMCLIII', 'MMCLIV', 'MMCLV', 'MMCLVI', 'MMCLVII', 'MMCLVIII', 'MMCLIX', 'MMCLX', 'MMCLXI', 'MMCLXII', 'MMCLXIII', 'MMCLXIV', 'MMCLXV', 'MMCLXVI', 'MMCLXVII', 'MMCLXVIII', 'MMCLXIX', 'MMCLXX', 'MMCLXXI', 'MMCLXXII', 'MMCLXXIII', 'MMCLXXIV', 'MMCLXXV', 'MMCLXXVI', 'MMCLXXVII', 'MMCLXXVIII', 'MMCLXXIX', 'MMCLXXX', 'MMCLXXXI', 'MMCLXXXII', 'MMCLXXXIII', 'MMCLXXXIV', 'MMCLXXXV', 'MMCLXXXVI', 'MMCLXXXVII', 'MMCLXXXVIII', 'MMCLXXXIX', 'MMCXC', 'MMCXCI', 'MMCXCII', 'MMCXCIII', 'MMCXCIV', 'MMCVC', 'MMCVCI', 'MMCVCII', 'MMCVCIII', 'MMCIC', 'MMCC', 'MMCCI', 'MMCCII', 'MMCCIII', 'MMCCIV', 'MMCCV', 'MMCCVI', 'MMCCVII', 'MMCCVIII', 'MMCCIX', 'MMCCX', 'MMCCXI', 'MMCCXII', 'MMCCXIII', 'MMCCXIV', 'MMCCXV', 'MMCCXVI', 'MMCCXVII', 'MMCCXVIII', 'MMCCXIX', 'MMCCXX', 'MMCCXXI', 'MMCCXXII', 'MMCCXXIII', 'MMCCXXIV', 'MMCCXXV', 'MMCCXXVI', 'MMCCXXVII', 'MMCCXXVIII', 'MMCCXXIX', 'MMCCXXX', 'MMCCXXXI', 'MMCCXXXII', 'MMCCXXXIII', 'MMCCXXXIV', 'MMCCXXXV', 'MMCCXXXVI', 'MMCCXXXVII', 'MMCCXXXVIII', 'MMCCXXXIX', 'MMCCXL', 'MMCCXLI', 'MMCCXLII', 'MMCCXLIII', 'MMCCXLIV', 'MMCCVL', 'MMCCVLI', 'MMCCVLII', 'MMCCVLIII', 'MMCCIL', 'MMCCL', 'MMCCLI', 'MMCCLII', 'MMCCLIII', 'MMCCLIV', 'MMCCLV', 'MMCCLVI', 'MMCCLVII', 'MMCCLVIII', 'MMCCLIX', 'MMCCLX', 'MMCCLXI', 'MMCCLXII', 'MMCCLXIII', 'MMCCLXIV', 'MMCCLXV', 'MMCCLXVI', 'MMCCLXVII', 'MMCCLXVIII', 'MMCCLXIX', 'MMCCLXX', 'MMCCLXXI', 'MMCCLXXII', 'MMCCLXXIII', 'MMCCLXXIV', 'MMCCLXXV', 'MMCCLXXVI', 'MMCCLXXVII', 'MMCCLXXVIII', 'MMCCLXXIX', 'MMCCLXXX', 'MMCCLXXXI', 'MMCCLXXXII', 'MMCCLXXXIII', 'MMCCLXXXIV', 'MMCCLXXXV', 'MMCCLXXXVI', 'MMCCLXXXVII', 'MMCCLXXXVIII', 'MMCCLXXXIX', 'MMCCXC', 'MMCCXCI', 'MMCCXCII', 'MMCCXCIII', 'MMCCXCIV', 'MMCCVC', 'MMCCVCI', 'MMCCVCII', 'MMCCVCIII', 'MMCCIC', 'MMCCC', 'MMCCCI', 'MMCCCII', 'MMCCCIII', 'MMCCCIV', 'MMCCCV', 'MMCCCVI', 'MMCCCVII', 'MMCCCVIII', 'MMCCCIX', 'MMCCCX', 'MMCCCXI', 'MMCCCXII', 'MMCCCXIII', 'MMCCCXIV', 'MMCCCXV', 'MMCCCXVI', 'MMCCCXVII', 'MMCCCXVIII', 'MMCCCXIX', 'MMCCCXX', 'MMCCCXXI', 'MMCCCXXII', 'MMCCCXXIII', 'MMCCCXXIV', 'MMCCCXXV', 'MMCCCXXVI', 'MMCCCXXVII', 'MMCCCXXVIII', 'MMCCCXXIX', 'MMCCCXXX', 'MMCCCXXXI', 'MMCCCXXXII', 'MMCCCXXXIII', 'MMCCCXXXIV', 'MMCCCXXXV', 'MMCCCXXXVI', 'MMCCCXXXVII', 'MMCCCXXXVIII', 'MMCCCXXXIX', 'MMCCCXL', 'MMCCCXLI', 'MMCCCXLII', 'MMCCCXLIII', 'MMCCCXLIV', 'MMCCCVL', 'MMCCCVLI', 'MMCCCVLII', 'MMCCCVLIII', 'MMCCCIL', 'MMCCCL', 'MMCCCLI', 'MMCCCLII', 'MMCCCLIII', 'MMCCCLIV', 'MMCCCLV', 'MMCCCLVI', 'MMCCCLVII', 'MMCCCLVIII', 'MMCCCLIX', 'MMCCCLX', 'MMCCCLXI', 'MMCCCLXII', 'MMCCCLXIII', 'MMCCCLXIV', 'MMCCCLXV', 'MMCCCLXVI', 'MMCCCLXVII', 'MMCCCLXVIII', 'MMCCCLXIX', 'MMCCCLXX', 'MMCCCLXXI', 'MMCCCLXXII', 'MMCCCLXXIII', 'MMCCCLXXIV', 'MMCCCLXXV', 'MMCCCLXXVI', 'MMCCCLXXVII', 'MMCCCLXXVIII', 'MMCCCLXXIX', 'MMCCCLXXX', 'MMCCCLXXXI', 'MMCCCLXXXII', 'MMCCCLXXXIII', 'MMCCCLXXXIV', 'MMCCCLXXXV', 'MMCCCLXXXVI', 'MMCCCLXXXVII', 'MMCCCLXXXVIII', 'MMCCCLXXXIX', 'MMCCCXC', 'MMCCCXCI', 'MMCCCXCII', 'MMCCCXCIII', 'MMCCCXCIV', 'MMCCCVC', 'MMCCCVCI', 'MMCCCVCII', 'MMCCCVCIII', 'MMCCCIC', 'MMCD', 'MMCDI', 'MMCDII', 'MMCDIII', 'MMCDIV', 'MMCDV', 'MMCDVI', 'MMCDVII', 'MMCDVIII', 'MMCDIX', 'MMCDX', 'MMCDXI', 'MMCDXII', 'MMCDXIII', 'MMCDXIV', 'MMCDXV', 'MMCDXVI', 'MMCDXVII', 'MMCDXVIII', 'MMCDXIX', 'MMCDXX', 'MMCDXXI', 'MMCDXXII', 'MMCDXXIII', 'MMCDXXIV', 'MMCDXXV', 'MMCDXXVI', 'MMCDXXVII', 'MMCDXXVIII', 'MMCDXXIX', 'MMCDXXX', 'MMCDXXXI', 'MMCDXXXII', 'MMCDXXXIII', 'MMCDXXXIV', 'MMCDXXXV', 'MMCDXXXVI', 'MMCDXXXVII', 'MMCDXXXVIII', 'MMCDXXXIX', 'MMCDXL', 'MMCDXLI', 'MMCDXLII', 'MMCDXLIII', 'MMCDXLIV', 'MMCDVL', 'MMCDVLI', 'MMCDVLII', 'MMCDVLIII', 'MMCDIL', 'MMLD', 'MMLDI', 'MMLDII', 'MMLDIII', 'MMLDIV', 'MMLDV', 'MMLDVI', 'MMLDVII', 'MMLDVIII', 'MMLDIX', 'MMLDX', 'MMLDXI', 'MMLDXII', 'MMLDXIII', 'MMLDXIV', 'MMLDXV', 'MMLDXVI', 'MMLDXVII', 'MMLDXVIII', 'MMLDXIX', 'MMLDXX', 'MMLDXXI', 'MMLDXXII', 'MMLDXXIII', 'MMLDXXIV', 'MMLDXXV', 'MMLDXXVI', 'MMLDXXVII', 'MMLDXXVIII', 'MMLDXXIX', 'MMLDXXX', 'MMLDXXXI', 'MMLDXXXII', 'MMLDXXXIII', 'MMLDXXXIV', 'MMLDXXXV', 'MMLDXXXVI', 'MMLDXXXVII', 'MMLDXXXVIII', 'MMLDXXXIX', 'MMXD', 'MMXDI', 'MMXDII', 'MMXDIII', 'MMXDIV', 'MMVD', 'MMVDI', 'MMVDII', 'MMVDIII', 'MMVDIV', 'MMD', 'MMDI', 'MMDII', 'MMDIII', 'MMDIV', 'MMDV', 'MMDVI', 'MMDVII', 'MMDVIII', 'MMDIX', 'MMDX', 'MMDXI', 'MMDXII', 'MMDXIII', 'MMDXIV', 'MMDXV', 'MMDXVI', 'MMDXVII', 'MMDXVIII', 'MMDXIX', 'MMDXX', 'MMDXXI', 'MMDXXII', 'MMDXXIII', 'MMDXXIV', 'MMDXXV', 'MMDXXVI', 'MMDXXVII', 'MMDXXVIII', 'MMDXXIX', 'MMDXXX', 'MMDXXXI', 'MMDXXXII', 'MMDXXXIII', 'MMDXXXIV', 'MMDXXXV', 'MMDXXXVI', 'MMDXXXVII', 'MMDXXXVIII', 'MMDXXXIX', 'MMDXL', 'MMDXLI', 'MMDXLII', 'MMDXLIII', 'MMDXLIV', 'MMDVL', 'MMDVLI', 'MMDVLII', 'MMDVLIII', 'MMDIL', 'MMDL', 'MMDLI', 'MMDLII', 'MMDLIII', 'MMDLIV', 'MMDLV', 'MMDLVI', 'MMDLVII', 'MMDLVIII', 'MMDLIX', 'MMDLX', 'MMDLXI', 'MMDLXII', 'MMDLXIII', 'MMDLXIV', 'MMDLXV', 'MMDLXVI', 'MMDLXVII', 'MMDLXVIII', 'MMDLXIX', 'MMDLXX', 'MMDLXXI', 'MMDLXXII', 'MMDLXXIII', 'MMDLXXIV', 'MMDLXXV', 'MMDLXXVI', 'MMDLXXVII', 'MMDLXXVIII', 'MMDLXXIX', 'MMDLXXX', 'MMDLXXXI', 'MMDLXXXII', 'MMDLXXXIII', 'MMDLXXXIV', 'MMDLXXXV', 'MMDLXXXVI', 'MMDLXXXVII', 'MMDLXXXVIII', 'MMDLXXXIX', 'MMDXC', 'MMDXCI', 'MMDXCII', 'MMDXCIII', 'MMDXCIV', 'MMDVC', 'MMDVCI', 'MMDVCII', 'MMDVCIII', 'MMDIC', 'MMDC', 'MMDCI', 'MMDCII', 'MMDCIII', 'MMDCIV', 'MMDCV', 'MMDCVI', 'MMDCVII', 'MMDCVIII', 'MMDCIX', 'MMDCX', 'MMDCXI', 'MMDCXII', 'MMDCXIII', 'MMDCXIV', 'MMDCXV', 'MMDCXVI', 'MMDCXVII', 'MMDCXVIII', 'MMDCXIX', 'MMDCXX', 'MMDCXXI', 'MMDCXXII', 'MMDCXXIII', 'MMDCXXIV', 'MMDCXXV', 'MMDCXXVI', 'MMDCXXVII', 'MMDCXXVIII', 'MMDCXXIX', 'MMDCXXX', 'MMDCXXXI', 'MMDCXXXII', 'MMDCXXXIII', 'MMDCXXXIV', 'MMDCXXXV', 'MMDCXXXVI', 'MMDCXXXVII', 'MMDCXXXVIII', 'MMDCXXXIX', 'MMDCXL', 'MMDCXLI', 'MMDCXLII', 'MMDCXLIII', 'MMDCXLIV', 'MMDCVL', 'MMDCVLI', 'MMDCVLII', 'MMDCVLIII', 'MMDCIL', 'MMDCL', 'MMDCLI', 'MMDCLII', 'MMDCLIII', 'MMDCLIV', 'MMDCLV', 'MMDCLVI', 'MMDCLVII', 'MMDCLVIII', 'MMDCLIX', 'MMDCLX', 'MMDCLXI', 'MMDCLXII', 'MMDCLXIII', 'MMDCLXIV', 'MMDCLXV', 'MMDCLXVI', 'MMDCLXVII', 'MMDCLXVIII', 'MMDCLXIX', 'MMDCLXX', 'MMDCLXXI', 'MMDCLXXII', 'MMDCLXXIII', 'MMDCLXXIV', 'MMDCLXXV', 'MMDCLXXVI', 'MMDCLXXVII', 'MMDCLXXVIII', 'MMDCLXXIX', 'MMDCLXXX', 'MMDCLXXXI', 'MMDCLXXXII', 'MMDCLXXXIII', 'MMDCLXXXIV', 'MMDCLXXXV', 'MMDCLXXXVI', 'MMDCLXXXVII', 'MMDCLXXXVIII', 'MMDCLXXXIX', 'MMDCXC', 'MMDCXCI', 'MMDCXCII', 'MMDCXCIII', 'MMDCXCIV', 'MMDCVC', 'MMDCVCI', 'MMDCVCII', 'MMDCVCIII', 'MMDCIC', 'MMDCC', 'MMDCCI', 'MMDCCII', 'MMDCCIII', 'MMDCCIV', 'MMDCCV', 'MMDCCVI', 'MMDCCVII', 'MMDCCVIII', 'MMDCCIX', 'MMDCCX', 'MMDCCXI', 'MMDCCXII', 'MMDCCXIII', 'MMDCCXIV', 'MMDCCXV', 'MMDCCXVI', 'MMDCCXVII', 'MMDCCXVIII', 'MMDCCXIX', 'MMDCCXX', 'MMDCCXXI', 'MMDCCXXII', 'MMDCCXXIII', 'MMDCCXXIV', 'MMDCCXXV', 'MMDCCXXVI', 'MMDCCXXVII', 'MMDCCXXVIII', 'MMDCCXXIX', 'MMDCCXXX', 'MMDCCXXXI', 'MMDCCXXXII', 'MMDCCXXXIII', 'MMDCCXXXIV', 'MMDCCXXXV', 'MMDCCXXXVI', 'MMDCCXXXVII', 'MMDCCXXXVIII', 'MMDCCXXXIX', 'MMDCCXL', 'MMDCCXLI', 'MMDCCXLII', 'MMDCCXLIII', 'MMDCCXLIV', 'MMDCCVL', 'MMDCCVLI', 'MMDCCVLII', 'MMDCCVLIII', 'MMDCCIL', 'MMDCCL', 'MMDCCLI', 'MMDCCLII', 'MMDCCLIII', 'MMDCCLIV', 'MMDCCLV', 'MMDCCLVI', 'MMDCCLVII', 'MMDCCLVIII', 'MMDCCLIX', 'MMDCCLX', 'MMDCCLXI', 'MMDCCLXII', 'MMDCCLXIII', 'MMDCCLXIV', 'MMDCCLXV', 'MMDCCLXVI', 'MMDCCLXVII', 'MMDCCLXVIII', 'MMDCCLXIX', 'MMDCCLXX', 'MMDCCLXXI', 'MMDCCLXXII', 'MMDCCLXXIII', 'MMDCCLXXIV', 'MMDCCLXXV', 'MMDCCLXXVI', 'MMDCCLXXVII', 'MMDCCLXXVIII', 'MMDCCLXXIX', 'MMDCCLXXX', 'MMDCCLXXXI', 'MMDCCLXXXII', 'MMDCCLXXXIII', 'MMDCCLXXXIV', 'MMDCCLXXXV', 'MMDCCLXXXVI', 'MMDCCLXXXVII', 'MMDCCLXXXVIII', 'MMDCCLXXXIX', 'MMDCCXC', 'MMDCCXCI', 'MMDCCXCII', 'MMDCCXCIII', 'MMDCCXCIV', 'MMDCCVC', 'MMDCCVCI', 'MMDCCVCII', 'MMDCCVCIII', 'MMDCCIC', 'MMDCCC', 'MMDCCCI', 'MMDCCCII', 'MMDCCCIII', 'MMDCCCIV', 'MMDCCCV', 'MMDCCCVI', 'MMDCCCVII', 'MMDCCCVIII', 'MMDCCCIX', 'MMDCCCX', 'MMDCCCXI', 'MMDCCCXII', 'MMDCCCXIII', 'MMDCCCXIV', 'MMDCCCXV', 'MMDCCCXVI', 'MMDCCCXVII', 'MMDCCCXVIII', 'MMDCCCXIX', 'MMDCCCXX', 'MMDCCCXXI', 'MMDCCCXXII', 'MMDCCCXXIII', 'MMDCCCXXIV', 'MMDCCCXXV', 'MMDCCCXXVI', 'MMDCCCXXVII', 'MMDCCCXXVIII', 'MMDCCCXXIX', 'MMDCCCXXX', 'MMDCCCXXXI', 'MMDCCCXXXII', 'MMDCCCXXXIII', 'MMDCCCXXXIV', 'MMDCCCXXXV', 'MMDCCCXXXVI', 'MMDCCCXXXVII', 'MMDCCCXXXVIII', 'MMDCCCXXXIX', 'MMDCCCXL', 'MMDCCCXLI', 'MMDCCCXLII', 'MMDCCCXLIII', 'MMDCCCXLIV', 'MMDCCCVL', 'MMDCCCVLI', 'MMDCCCVLII', 'MMDCCCVLIII', 'MMDCCCIL', 'MMDCCCL', 'MMDCCCLI', 'MMDCCCLII', 'MMDCCCLIII', 'MMDCCCLIV', 'MMDCCCLV', 'MMDCCCLVI', 'MMDCCCLVII', 'MMDCCCLVIII', 'MMDCCCLIX', 'MMDCCCLX', 'MMDCCCLXI', 'MMDCCCLXII', 'MMDCCCLXIII', 'MMDCCCLXIV', 'MMDCCCLXV', 'MMDCCCLXVI', 'MMDCCCLXVII', 'MMDCCCLXVIII', 'MMDCCCLXIX', 'MMDCCCLXX', 'MMDCCCLXXI', 'MMDCCCLXXII', 'MMDCCCLXXIII', 'MMDCCCLXXIV', 'MMDCCCLXXV', 'MMDCCCLXXVI', 'MMDCCCLXXVII', 'MMDCCCLXXVIII', 'MMDCCCLXXIX', 'MMDCCCLXXX', 'MMDCCCLXXXI', 'MMDCCCLXXXII', 'MMDCCCLXXXIII', 'MMDCCCLXXXIV', 'MMDCCCLXXXV', 'MMDCCCLXXXVI', 'MMDCCCLXXXVII', 'MMDCCCLXXXVIII', 'MMDCCCLXXXIX', 'MMDCCCXC', 'MMDCCCXCI', 'MMDCCCXCII', 'MMDCCCXCIII', 'MMDCCCXCIV', 'MMDCCCVC', 'MMDCCCVCI', 'MMDCCCVCII', 'MMDCCCVCIII', 'MMDCCCIC', 'MMCM', 'MMCMI', 'MMCMII', 'MMCMIII', 'MMCMIV', 'MMCMV', 'MMCMVI', 'MMCMVII', 'MMCMVIII', 'MMCMIX', 'MMCMX', 'MMCMXI', 'MMCMXII', 'MMCMXIII', 'MMCMXIV', 'MMCMXV', 'MMCMXVI', 'MMCMXVII', 'MMCMXVIII', 'MMCMXIX', 'MMCMXX', 'MMCMXXI', 'MMCMXXII', 'MMCMXXIII', 'MMCMXXIV', 'MMCMXXV', 'MMCMXXVI', 'MMCMXXVII', 'MMCMXXVIII', 'MMCMXXIX', 'MMCMXXX', 'MMCMXXXI', 'MMCMXXXII', 'MMCMXXXIII', 'MMCMXXXIV', 'MMCMXXXV', 'MMCMXXXVI', 'MMCMXXXVII', 'MMCMXXXVIII', 'MMCMXXXIX', 'MMCMXL', 'MMCMXLI', 'MMCMXLII', 'MMCMXLIII', 'MMCMXLIV', 'MMCMVL', 'MMCMVLI', 'MMCMVLII', 'MMCMVLIII', 'MMCMIL', 'MMLM', 'MMLMI', 'MMLMII', 'MMLMIII', 'MMLMIV', 'MMLMV', 'MMLMVI', 'MMLMVII', 'MMLMVIII', 'MMLMIX', 'MMLMX', 'MMLMXI', 'MMLMXII', 'MMLMXIII', 'MMLMXIV', 'MMLMXV', 'MMLMXVI', 'MMLMXVII', 'MMLMXVIII', 'MMLMXIX', 'MMLMXX', 'MMLMXXI', 'MMLMXXII', 'MMLMXXIII', 'MMLMXXIV', 'MMLMXXV', 'MMLMXXVI', 'MMLMXXVII', 'MMLMXXVIII', 'MMLMXXIX', 'MMLMXXX', 'MMLMXXXI', 'MMLMXXXII', 'MMLMXXXIII', 'MMLMXXXIV', 'MMLMXXXV', 'MMLMXXXVI', 'MMLMXXXVII', 'MMLMXXXVIII', 'MMLMXXXIX', 'MMXM', 'MMXMI', 'MMXMII', 'MMXMIII', 'MMXMIV', 'MMVM', 'MMVMI', 'MMVMII', 'MMVMIII', 'MMVMIV', 'MMM', 'MMMI', 'MMMII', 'MMMIII', 'MMMIV', 'MMMV', 'MMMVI', 'MMMVII', 'MMMVIII', 'MMMIX', 'MMMX', 'MMMXI', 'MMMXII', 'MMMXIII', 'MMMXIV', 'MMMXV', 'MMMXVI', 'MMMXVII', 'MMMXVIII', 'MMMXIX', 'MMMXX', 'MMMXXI', 'MMMXXII', 'MMMXXIII', 'MMMXXIV', 'MMMXXV', 'MMMXXVI', 'MMMXXVII', 'MMMXXVIII', 'MMMXXIX', 'MMMXXX', 'MMMXXXI', 'MMMXXXII', 'MMMXXXIII', 'MMMXXXIV', 'MMMXXXV', 'MMMXXXVI', 'MMMXXXVII', 'MMMXXXVIII', 'MMMXXXIX', 'MMMXL', 'MMMXLI', 'MMMXLII', 'MMMXLIII', 'MMMXLIV', 'MMMVL', 'MMMVLI', 'MMMVLII', 'MMMVLIII', 'MMMIL', 'MMML', 'MMMLI', 'MMMLII', 'MMMLIII', 'MMMLIV', 'MMMLV', 'MMMLVI', 'MMMLVII', 'MMMLVIII', 'MMMLIX', 'MMMLX', 'MMMLXI', 'MMMLXII', 'MMMLXIII', 'MMMLXIV', 'MMMLXV', 'MMMLXVI', 'MMMLXVII', 'MMMLXVIII', 'MMMLXIX', 'MMMLXX', 'MMMLXXI', 'MMMLXXII', 'MMMLXXIII', 'MMMLXXIV', 'MMMLXXV', 'MMMLXXVI', 'MMMLXXVII', 'MMMLXXVIII', 'MMMLXXIX', 'MMMLXXX', 'MMMLXXXI', 'MMMLXXXII', 'MMMLXXXIII', 'MMMLXXXIV', 'MMMLXXXV', 'MMMLXXXVI', 'MMMLXXXVII', 'MMMLXXXVIII', 'MMMLXXXIX', 'MMMXC', 'MMMXCI', 'MMMXCII', 'MMMXCIII', 'MMMXCIV', 'MMMVC', 'MMMVCI', 'MMMVCII', 'MMMVCIII', 'MMMIC', 'MMMC', 'MMMCI', 'MMMCII', 'MMMCIII', 'MMMCIV', 'MMMCV', 'MMMCVI', 'MMMCVII', 'MMMCVIII', 'MMMCIX', 'MMMCX', 'MMMCXI', 'MMMCXII', 'MMMCXIII', 'MMMCXIV', 'MMMCXV', 'MMMCXVI', 'MMMCXVII', 'MMMCXVIII', 'MMMCXIX', 'MMMCXX', 'MMMCXXI', 'MMMCXXII', 'MMMCXXIII', 'MMMCXXIV', 'MMMCXXV', 'MMMCXXVI', 'MMMCXXVII', 'MMMCXXVIII', 'MMMCXXIX', 'MMMCXXX', 'MMMCXXXI', 'MMMCXXXII', 'MMMCXXXIII', 'MMMCXXXIV', 'MMMCXXXV', 'MMMCXXXVI', 'MMMCXXXVII', 'MMMCXXXVIII', 'MMMCXXXIX', 'MMMCXL', 'MMMCXLI', 'MMMCXLII', 'MMMCXLIII', 'MMMCXLIV', 'MMMCVL', 'MMMCVLI', 'MMMCVLII', 'MMMCVLIII', 'MMMCIL', 'MMMCL', 'MMMCLI', 'MMMCLII', 'MMMCLIII', 'MMMCLIV', 'MMMCLV', 'MMMCLVI', 'MMMCLVII', 'MMMCLVIII', 'MMMCLIX', 'MMMCLX', 'MMMCLXI', 'MMMCLXII', 'MMMCLXIII', 'MMMCLXIV', 'MMMCLXV', 'MMMCLXVI', 'MMMCLXVII', 'MMMCLXVIII', 'MMMCLXIX', 'MMMCLXX', 'MMMCLXXI', 'MMMCLXXII', 'MMMCLXXIII', 'MMMCLXXIV', 'MMMCLXXV', 'MMMCLXXVI', 'MMMCLXXVII', 'MMMCLXXVIII', 'MMMCLXXIX', 'MMMCLXXX', 'MMMCLXXXI', 'MMMCLXXXII', 'MMMCLXXXIII', 'MMMCLXXXIV', 'MMMCLXXXV', 'MMMCLXXXVI', 'MMMCLXXXVII', 'MMMCLXXXVIII', 'MMMCLXXXIX', 'MMMCXC', 'MMMCXCI', 'MMMCXCII', 'MMMCXCIII', 'MMMCXCIV', 'MMMCVC', 'MMMCVCI', 'MMMCVCII', 'MMMCVCIII', 'MMMCIC', 'MMMCC', 'MMMCCI', 'MMMCCII', 'MMMCCIII', 'MMMCCIV', 'MMMCCV', 'MMMCCVI', 'MMMCCVII', 'MMMCCVIII', 'MMMCCIX', 'MMMCCX', 'MMMCCXI', 'MMMCCXII', 'MMMCCXIII', 'MMMCCXIV', 'MMMCCXV', 'MMMCCXVI', 'MMMCCXVII', 'MMMCCXVIII', 'MMMCCXIX', 'MMMCCXX', 'MMMCCXXI', 'MMMCCXXII', 'MMMCCXXIII', 'MMMCCXXIV', 'MMMCCXXV', 'MMMCCXXVI', 'MMMCCXXVII', 'MMMCCXXVIII', 'MMMCCXXIX', 'MMMCCXXX', 'MMMCCXXXI', 'MMMCCXXXII', 'MMMCCXXXIII', 'MMMCCXXXIV', 'MMMCCXXXV', 'MMMCCXXXVI', 'MMMCCXXXVII', 'MMMCCXXXVIII', 'MMMCCXXXIX', 'MMMCCXL', 'MMMCCXLI', 'MMMCCXLII', 'MMMCCXLIII', 'MMMCCXLIV', 'MMMCCVL', 'MMMCCVLI', 'MMMCCVLII', 'MMMCCVLIII', 'MMMCCIL', 'MMMCCL', 'MMMCCLI', 'MMMCCLII', 'MMMCCLIII', 'MMMCCLIV', 'MMMCCLV', 'MMMCCLVI', 'MMMCCLVII', 'MMMCCLVIII', 'MMMCCLIX', 'MMMCCLX', 'MMMCCLXI', 'MMMCCLXII', 'MMMCCLXIII', 'MMMCCLXIV', 'MMMCCLXV', 'MMMCCLXVI', 'MMMCCLXVII', 'MMMCCLXVIII', 'MMMCCLXIX', 'MMMCCLXX', 'MMMCCLXXI', 'MMMCCLXXII', 'MMMCCLXXIII', 'MMMCCLXXIV', 'MMMCCLXXV', 'MMMCCLXXVI', 'MMMCCLXXVII', 'MMMCCLXXVIII', 'MMMCCLXXIX', 'MMMCCLXXX', 'MMMCCLXXXI', 'MMMCCLXXXII', 'MMMCCLXXXIII', 'MMMCCLXXXIV', 'MMMCCLXXXV', 'MMMCCLXXXVI', 'MMMCCLXXXVII', 'MMMCCLXXXVIII', 'MMMCCLXXXIX', 'MMMCCXC', 'MMMCCXCI', 'MMMCCXCII', 'MMMCCXCIII', 'MMMCCXCIV', 'MMMCCVC', 'MMMCCVCI', 'MMMCCVCII', 'MMMCCVCIII', 'MMMCCIC', 'MMMCCC', 'MMMCCCI', 'MMMCCCII', 'MMMCCCIII', 'MMMCCCIV', 'MMMCCCV', 'MMMCCCVI', 'MMMCCCVII', 'MMMCCCVIII', 'MMMCCCIX', 'MMMCCCX', 'MMMCCCXI', 'MMMCCCXII', 'MMMCCCXIII', 'MMMCCCXIV', 'MMMCCCXV', 'MMMCCCXVI', 'MMMCCCXVII', 'MMMCCCXVIII', 'MMMCCCXIX', 'MMMCCCXX', 'MMMCCCXXI', 'MMMCCCXXII', 'MMMCCCXXIII', 'MMMCCCXXIV', 'MMMCCCXXV', 'MMMCCCXXVI', 'MMMCCCXXVII', 'MMMCCCXXVIII', 'MMMCCCXXIX', 'MMMCCCXXX', 'MMMCCCXXXI', 'MMMCCCXXXII', 'MMMCCCXXXIII', 'MMMCCCXXXIV', 'MMMCCCXXXV', 'MMMCCCXXXVI', 'MMMCCCXXXVII', 'MMMCCCXXXVIII', 'MMMCCCXXXIX', 'MMMCCCXL', 'MMMCCCXLI', 'MMMCCCXLII', 'MMMCCCXLIII', 'MMMCCCXLIV', 'MMMCCCVL', 'MMMCCCVLI', 'MMMCCCVLII', 'MMMCCCVLIII', 'MMMCCCIL', 'MMMCCCL', 'MMMCCCLI', 'MMMCCCLII', 'MMMCCCLIII', 'MMMCCCLIV', 'MMMCCCLV', 'MMMCCCLVI', 'MMMCCCLVII', 'MMMCCCLVIII', 'MMMCCCLIX', 'MMMCCCLX', 'MMMCCCLXI', 'MMMCCCLXII', 'MMMCCCLXIII', 'MMMCCCLXIV', 'MMMCCCLXV', 'MMMCCCLXVI', 'MMMCCCLXVII', 'MMMCCCLXVIII', 'MMMCCCLXIX', 'MMMCCCLXX', 'MMMCCCLXXI', 'MMMCCCLXXII', 'MMMCCCLXXIII', 'MMMCCCLXXIV', 'MMMCCCLXXV', 'MMMCCCLXXVI', 'MMMCCCLXXVII', 'MMMCCCLXXVIII', 'MMMCCCLXXIX', 'MMMCCCLXXX', 'MMMCCCLXXXI', 'MMMCCCLXXXII', 'MMMCCCLXXXIII', 'MMMCCCLXXXIV', 'MMMCCCLXXXV', 'MMMCCCLXXXVI', 'MMMCCCLXXXVII', 'MMMCCCLXXXVIII', 'MMMCCCLXXXIX', 'MMMCCCXC', 'MMMCCCXCI', 'MMMCCCXCII', 'MMMCCCXCIII', 'MMMCCCXCIV', 'MMMCCCVC', 'MMMCCCVCI', 'MMMCCCVCII', 'MMMCCCVCIII', 'MMMCCCIC', 'MMMCD', 'MMMCDI', 'MMMCDII', 'MMMCDIII', 'MMMCDIV', 'MMMCDV', 'MMMCDVI', 'MMMCDVII', 'MMMCDVIII', 'MMMCDIX', 'MMMCDX', 'MMMCDXI', 'MMMCDXII', 'MMMCDXIII', 'MMMCDXIV', 'MMMCDXV', 'MMMCDXVI', 'MMMCDXVII', 'MMMCDXVIII', 'MMMCDXIX', 'MMMCDXX', 'MMMCDXXI', 'MMMCDXXII', 'MMMCDXXIII', 'MMMCDXXIV', 'MMMCDXXV', 'MMMCDXXVI', 'MMMCDXXVII', 'MMMCDXXVIII', 'MMMCDXXIX', 'MMMCDXXX', 'MMMCDXXXI', 'MMMCDXXXII', 'MMMCDXXXIII', 'MMMCDXXXIV', 'MMMCDXXXV', 'MMMCDXXXVI', 'MMMCDXXXVII', 'MMMCDXXXVIII', 'MMMCDXXXIX', 'MMMCDXL', 'MMMCDXLI', 'MMMCDXLII', 'MMMCDXLIII', 'MMMCDXLIV', 'MMMCDVL', 'MMMCDVLI', 'MMMCDVLII', 'MMMCDVLIII', 'MMMCDIL', 'MMMLD', 'MMMLDI', 'MMMLDII', 'MMMLDIII', 'MMMLDIV', 'MMMLDV', 'MMMLDVI', 'MMMLDVII', 'MMMLDVIII', 'MMMLDIX', 'MMMLDX', 'MMMLDXI', 'MMMLDXII', 'MMMLDXIII', 'MMMLDXIV', 'MMMLDXV', 'MMMLDXVI', 'MMMLDXVII', 'MMMLDXVIII', 'MMMLDXIX', 'MMMLDXX', 'MMMLDXXI', 'MMMLDXXII', 'MMMLDXXIII', 'MMMLDXXIV', 'MMMLDXXV', 'MMMLDXXVI', 'MMMLDXXVII', 'MMMLDXXVIII', 'MMMLDXXIX', 'MMMLDXXX', 'MMMLDXXXI', 'MMMLDXXXII', 'MMMLDXXXIII', 'MMMLDXXXIV', 'MMMLDXXXV', 'MMMLDXXXVI', 'MMMLDXXXVII', 'MMMLDXXXVIII', 'MMMLDXXXIX', 'MMMXD', 'MMMXDI', 'MMMXDII', 'MMMXDIII', 'MMMXDIV', 'MMMVD', 'MMMVDI', 'MMMVDII', 'MMMVDIII', 'MMMVDIV', 'MMMD', 'MMMDI', 'MMMDII', 'MMMDIII', 'MMMDIV', 'MMMDV', 'MMMDVI', 'MMMDVII', 'MMMDVIII', 'MMMDIX', 'MMMDX', 'MMMDXI', 'MMMDXII', 'MMMDXIII', 'MMMDXIV', 'MMMDXV', 'MMMDXVI', 'MMMDXVII', 'MMMDXVIII', 'MMMDXIX', 'MMMDXX', 'MMMDXXI', 'MMMDXXII', 'MMMDXXIII', 'MMMDXXIV', 'MMMDXXV', 'MMMDXXVI', 'MMMDXXVII', 'MMMDXXVIII', 'MMMDXXIX', 'MMMDXXX', 'MMMDXXXI', 'MMMDXXXII', 'MMMDXXXIII', 'MMMDXXXIV', 'MMMDXXXV', 'MMMDXXXVI', 'MMMDXXXVII', 'MMMDXXXVIII', 'MMMDXXXIX', 'MMMDXL', 'MMMDXLI', 'MMMDXLII', 'MMMDXLIII', 'MMMDXLIV', 'MMMDVL', 'MMMDVLI', 'MMMDVLII', 'MMMDVLIII', 'MMMDIL', 'MMMDL', 'MMMDLI', 'MMMDLII', 'MMMDLIII', 'MMMDLIV', 'MMMDLV', 'MMMDLVI', 'MMMDLVII', 'MMMDLVIII', 'MMMDLIX', 'MMMDLX', 'MMMDLXI', 'MMMDLXII', 'MMMDLXIII', 'MMMDLXIV', 'MMMDLXV', 'MMMDLXVI', 'MMMDLXVII', 'MMMDLXVIII', 'MMMDLXIX', 'MMMDLXX', 'MMMDLXXI', 'MMMDLXXII', 'MMMDLXXIII', 'MMMDLXXIV', 'MMMDLXXV', 'MMMDLXXVI', 'MMMDLXXVII', 'MMMDLXXVIII', 'MMMDLXXIX', 'MMMDLXXX', 'MMMDLXXXI', 'MMMDLXXXII', 'MMMDLXXXIII', 'MMMDLXXXIV', 'MMMDLXXXV', 'MMMDLXXXVI', 'MMMDLXXXVII', 'MMMDLXXXVIII', 'MMMDLXXXIX', 'MMMDXC', 'MMMDXCI', 'MMMDXCII', 'MMMDXCIII', 'MMMDXCIV', 'MMMDVC', 'MMMDVCI', 'MMMDVCII', 'MMMDVCIII', 'MMMDIC', 'MMMDC', 'MMMDCI', 'MMMDCII', 'MMMDCIII', 'MMMDCIV', 'MMMDCV', 'MMMDCVI', 'MMMDCVII', 'MMMDCVIII', 'MMMDCIX', 'MMMDCX', 'MMMDCXI', 'MMMDCXII', 'MMMDCXIII', 'MMMDCXIV', 'MMMDCXV', 'MMMDCXVI', 'MMMDCXVII', 'MMMDCXVIII', 'MMMDCXIX', 'MMMDCXX', 'MMMDCXXI', 'MMMDCXXII', 'MMMDCXXIII', 'MMMDCXXIV', 'MMMDCXXV', 'MMMDCXXVI', 'MMMDCXXVII', 'MMMDCXXVIII', 'MMMDCXXIX', 'MMMDCXXX', 'MMMDCXXXI', 'MMMDCXXXII', 'MMMDCXXXIII', 'MMMDCXXXIV', 'MMMDCXXXV', 'MMMDCXXXVI', 'MMMDCXXXVII', 'MMMDCXXXVIII', 'MMMDCXXXIX', 'MMMDCXL', 'MMMDCXLI', 'MMMDCXLII', 'MMMDCXLIII', 'MMMDCXLIV', 'MMMDCVL', 'MMMDCVLI', 'MMMDCVLII', 'MMMDCVLIII', 'MMMDCIL', 'MMMDCL', 'MMMDCLI', 'MMMDCLII', 'MMMDCLIII', 'MMMDCLIV', 'MMMDCLV', 'MMMDCLVI', 'MMMDCLVII', 'MMMDCLVIII', 'MMMDCLIX', 'MMMDCLX', 'MMMDCLXI', 'MMMDCLXII', 'MMMDCLXIII', 'MMMDCLXIV', 'MMMDCLXV', 'MMMDCLXVI', 'MMMDCLXVII', 'MMMDCLXVIII', 'MMMDCLXIX', 'MMMDCLXX', 'MMMDCLXXI', 'MMMDCLXXII', 'MMMDCLXXIII', 'MMMDCLXXIV', 'MMMDCLXXV', 'MMMDCLXXVI', 'MMMDCLXXVII', 'MMMDCLXXVIII', 'MMMDCLXXIX', 'MMMDCLXXX', 'MMMDCLXXXI', 'MMMDCLXXXII', 'MMMDCLXXXIII', 'MMMDCLXXXIV', 'MMMDCLXXXV', 'MMMDCLXXXVI', 'MMMDCLXXXVII', 'MMMDCLXXXVIII', 'MMMDCLXXXIX', 'MMMDCXC', 'MMMDCXCI', 'MMMDCXCII', 'MMMDCXCIII', 'MMMDCXCIV', 'MMMDCVC', 'MMMDCVCI', 'MMMDCVCII', 'MMMDCVCIII', 'MMMDCIC', 'MMMDCC', 'MMMDCCI', 'MMMDCCII', 'MMMDCCIII', 'MMMDCCIV', 'MMMDCCV', 'MMMDCCVI', 'MMMDCCVII', 'MMMDCCVIII', 'MMMDCCIX', 'MMMDCCX', 'MMMDCCXI', 'MMMDCCXII', 'MMMDCCXIII', 'MMMDCCXIV', 'MMMDCCXV', 'MMMDCCXVI', 'MMMDCCXVII', 'MMMDCCXVIII', 'MMMDCCXIX', 'MMMDCCXX', 'MMMDCCXXI', 'MMMDCCXXII', 'MMMDCCXXIII', 'MMMDCCXXIV', 'MMMDCCXXV', 'MMMDCCXXVI', 'MMMDCCXXVII', 'MMMDCCXXVIII', 'MMMDCCXXIX', 'MMMDCCXXX', 'MMMDCCXXXI', 'MMMDCCXXXII', 'MMMDCCXXXIII', 'MMMDCCXXXIV', 'MMMDCCXXXV', 'MMMDCCXXXVI', 'MMMDCCXXXVII', 'MMMDCCXXXVIII', 'MMMDCCXXXIX', 'MMMDCCXL', 'MMMDCCXLI', 'MMMDCCXLII', 'MMMDCCXLIII', 'MMMDCCXLIV', 'MMMDCCVL', 'MMMDCCVLI', 'MMMDCCVLII', 'MMMDCCVLIII', 'MMMDCCIL', 'MMMDCCL', 'MMMDCCLI', 'MMMDCCLII', 'MMMDCCLIII', 'MMMDCCLIV', 'MMMDCCLV', 'MMMDCCLVI', 'MMMDCCLVII', 'MMMDCCLVIII', 'MMMDCCLIX', 'MMMDCCLX', 'MMMDCCLXI', 'MMMDCCLXII', 'MMMDCCLXIII', 'MMMDCCLXIV', 'MMMDCCLXV', 'MMMDCCLXVI', 'MMMDCCLXVII', 'MMMDCCLXVIII', 'MMMDCCLXIX', 'MMMDCCLXX', 'MMMDCCLXXI', 'MMMDCCLXXII', 'MMMDCCLXXIII', 'MMMDCCLXXIV', 'MMMDCCLXXV', 'MMMDCCLXXVI', 'MMMDCCLXXVII', 'MMMDCCLXXVIII', 'MMMDCCLXXIX', 'MMMDCCLXXX', 'MMMDCCLXXXI', 'MMMDCCLXXXII', 'MMMDCCLXXXIII', 'MMMDCCLXXXIV', 'MMMDCCLXXXV', 'MMMDCCLXXXVI', 'MMMDCCLXXXVII', 'MMMDCCLXXXVIII', 'MMMDCCLXXXIX', 'MMMDCCXC', 'MMMDCCXCI', 'MMMDCCXCII', 'MMMDCCXCIII', 'MMMDCCXCIV', 'MMMDCCVC', 'MMMDCCVCI', 'MMMDCCVCII', 'MMMDCCVCIII', 'MMMDCCIC', 'MMMDCCC', 'MMMDCCCI', 'MMMDCCCII', 'MMMDCCCIII', 'MMMDCCCIV', 'MMMDCCCV', 'MMMDCCCVI', 'MMMDCCCVII', 'MMMDCCCVIII', 'MMMDCCCIX', 'MMMDCCCX', 'MMMDCCCXI', 'MMMDCCCXII', 'MMMDCCCXIII', 'MMMDCCCXIV', 'MMMDCCCXV', 'MMMDCCCXVI', 'MMMDCCCXVII', 'MMMDCCCXVIII', 'MMMDCCCXIX', 'MMMDCCCXX', 'MMMDCCCXXI', 'MMMDCCCXXII', 'MMMDCCCXXIII', 'MMMDCCCXXIV', 'MMMDCCCXXV', 'MMMDCCCXXVI', 'MMMDCCCXXVII', 'MMMDCCCXXVIII', 'MMMDCCCXXIX', 'MMMDCCCXXX', 'MMMDCCCXXXI', 'MMMDCCCXXXII', 'MMMDCCCXXXIII', 'MMMDCCCXXXIV', 'MMMDCCCXXXV', 'MMMDCCCXXXVI', 'MMMDCCCXXXVII', 'MMMDCCCXXXVIII', 'MMMDCCCXXXIX', 'MMMDCCCXL', 'MMMDCCCXLI', 'MMMDCCCXLII', 'MMMDCCCXLIII', 'MMMDCCCXLIV', 'MMMDCCCVL', 'MMMDCCCVLI', 'MMMDCCCVLII', 'MMMDCCCVLIII', 'MMMDCCCIL', 'MMMDCCCL', 'MMMDCCCLI', 'MMMDCCCLII', 'MMMDCCCLIII', 'MMMDCCCLIV', 'MMMDCCCLV', 'MMMDCCCLVI', 'MMMDCCCLVII', 'MMMDCCCLVIII', 'MMMDCCCLIX', 'MMMDCCCLX', 'MMMDCCCLXI', 'MMMDCCCLXII', 'MMMDCCCLXIII', 'MMMDCCCLXIV', 'MMMDCCCLXV', 'MMMDCCCLXVI', 'MMMDCCCLXVII', 'MMMDCCCLXVIII', 'MMMDCCCLXIX', 'MMMDCCCLXX', 'MMMDCCCLXXI', 'MMMDCCCLXXII', 'MMMDCCCLXXIII', 'MMMDCCCLXXIV', 'MMMDCCCLXXV', 'MMMDCCCLXXVI', 'MMMDCCCLXXVII', 'MMMDCCCLXXVIII', 'MMMDCCCLXXIX', 'MMMDCCCLXXX', 'MMMDCCCLXXXI', 'MMMDCCCLXXXII', 'MMMDCCCLXXXIII', 'MMMDCCCLXXXIV', 'MMMDCCCLXXXV', 'MMMDCCCLXXXVI', 'MMMDCCCLXXXVII', 'MMMDCCCLXXXVIII', 'MMMDCCCLXXXIX', 'MMMDCCCXC', 'MMMDCCCXCI', 'MMMDCCCXCII', 'MMMDCCCXCIII', 'MMMDCCCXCIV', 'MMMDCCCVC', 'MMMDCCCVCI', 'MMMDCCCVCII', 'MMMDCCCVCIII', 'MMMDCCCIC', 'MMMCM', 'MMMCMI', 'MMMCMII', 'MMMCMIII', 'MMMCMIV', 'MMMCMV', 'MMMCMVI', 'MMMCMVII', 'MMMCMVIII', 'MMMCMIX', 'MMMCMX', 'MMMCMXI', 'MMMCMXII', 'MMMCMXIII', 'MMMCMXIV', 'MMMCMXV', 'MMMCMXVI', 'MMMCMXVII', 'MMMCMXVIII', 'MMMCMXIX', 'MMMCMXX', 'MMMCMXXI', 'MMMCMXXII', 'MMMCMXXIII', 'MMMCMXXIV', 'MMMCMXXV', 'MMMCMXXVI', 'MMMCMXXVII', 'MMMCMXXVIII', 'MMMCMXXIX', 'MMMCMXXX', 'MMMCMXXXI', 'MMMCMXXXII', 'MMMCMXXXIII', 'MMMCMXXXIV', 'MMMCMXXXV', 'MMMCMXXXVI', 'MMMCMXXXVII', 'MMMCMXXXVIII', 'MMMCMXXXIX', 'MMMCMXL', 'MMMCMXLI', 'MMMCMXLII', 'MMMCMXLIII', 'MMMCMXLIV', 'MMMCMVL', 'MMMCMVLI', 'MMMCMVLII', 'MMMCMVLIII', 'MMMCMIL', 'MMMLM', 'MMMLMI', 'MMMLMII', 'MMMLMIII', 'MMMLMIV', 'MMMLMV', 'MMMLMVI', 'MMMLMVII', 'MMMLMVIII', 'MMMLMIX', 'MMMLMX', 'MMMLMXI', 'MMMLMXII', 'MMMLMXIII', 'MMMLMXIV', 'MMMLMXV', 'MMMLMXVI', 'MMMLMXVII', 'MMMLMXVIII', 'MMMLMXIX', 'MMMLMXX', 'MMMLMXXI', 'MMMLMXXII', 'MMMLMXXIII', 'MMMLMXXIV', 'MMMLMXXV', 'MMMLMXXVI', 'MMMLMXXVII', 'MMMLMXXVIII', 'MMMLMXXIX', 'MMMLMXXX', 'MMMLMXXXI', 'MMMLMXXXII', 'MMMLMXXXIII', 'MMMLMXXXIV', 'MMMLMXXXV', 'MMMLMXXXVI', 'MMMLMXXXVII', 'MMMLMXXXVIII', 'MMMLMXXXIX', 'MMMXM', 'MMMXMI', 'MMMXMII', 'MMMXMIII', 'MMMXMIV', 'MMMVM', 'MMMVMI', 'MMMVMII', 'MMMVMIII', 'MMMVMIV', ] +const mode4 = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI', 'XII', 'XIII', 'XIV', 'XV', 'XVI', 'XVII', 'XVIII', 'XIX', 'XX', 'XXI', 'XXII', 'XXIII', 'XXIV', 'XXV', 'XXVI', 'XXVII', 'XXVIII', 'XXIX', 'XXX', 'XXXI', 'XXXII', 'XXXIII', 'XXXIV', 'XXXV', 'XXXVI', 'XXXVII', 'XXXVIII', 'XXXIX', 'XL', 'XLI', 'XLII', 'XLIII', 'XLIV', 'VL', 'VLI', 'VLII', 'VLIII', 'IL', 'L', 'LI', 'LII', 'LIII', 'LIV', 'LV', 'LVI', 'LVII', 'LVIII', 'LIX', 'LX', 'LXI', 'LXII', 'LXIII', 'LXIV', 'LXV', 'LXVI', 'LXVII', 'LXVIII', 'LXIX', 'LXX', 'LXXI', 'LXXII', 'LXXIII', 'LXXIV', 'LXXV', 'LXXVI', 'LXXVII', 'LXXVIII', 'LXXIX', 'LXXX', 'LXXXI', 'LXXXII', 'LXXXIII', 'LXXXIV', 'LXXXV', 'LXXXVI', 'LXXXVII', 'LXXXVIII', 'LXXXIX', 'XC', 'XCI', 'XCII', 'XCIII', 'XCIV', 'VC', 'VCI', 'VCII', 'VCIII', 'IC', 'C', 'CI', 'CII', 'CIII', 'CIV', 'CV', 'CVI', 'CVII', 'CVIII', 'CIX', 'CX', 'CXI', 'CXII', 'CXIII', 'CXIV', 'CXV', 'CXVI', 'CXVII', 'CXVIII', 'CXIX', 'CXX', 'CXXI', 'CXXII', 'CXXIII', 'CXXIV', 'CXXV', 'CXXVI', 'CXXVII', 'CXXVIII', 'CXXIX', 'CXXX', 'CXXXI', 'CXXXII', 'CXXXIII', 'CXXXIV', 'CXXXV', 'CXXXVI', 'CXXXVII', 'CXXXVIII', 'CXXXIX', 'CXL', 'CXLI', 'CXLII', 'CXLIII', 'CXLIV', 'CVL', 'CVLI', 'CVLII', 'CVLIII', 'CIL', 'CL', 'CLI', 'CLII', 'CLIII', 'CLIV', 'CLV', 'CLVI', 'CLVII', 'CLVIII', 'CLIX', 'CLX', 'CLXI', 'CLXII', 'CLXIII', 'CLXIV', 'CLXV', 'CLXVI', 'CLXVII', 'CLXVIII', 'CLXIX', 'CLXX', 'CLXXI', 'CLXXII', 'CLXXIII', 'CLXXIV', 'CLXXV', 'CLXXVI', 'CLXXVII', 'CLXXVIII', 'CLXXIX', 'CLXXX', 'CLXXXI', 'CLXXXII', 'CLXXXIII', 'CLXXXIV', 'CLXXXV', 'CLXXXVI', 'CLXXXVII', 'CLXXXVIII', 'CLXXXIX', 'CXC', 'CXCI', 'CXCII', 'CXCIII', 'CXCIV', 'CVC', 'CVCI', 'CVCII', 'CVCIII', 'CIC', 'CC', 'CCI', 'CCII', 'CCIII', 'CCIV', 'CCV', 'CCVI', 'CCVII', 'CCVIII', 'CCIX', 'CCX', 'CCXI', 'CCXII', 'CCXIII', 'CCXIV', 'CCXV', 'CCXVI', 'CCXVII', 'CCXVIII', 'CCXIX', 'CCXX', 'CCXXI', 'CCXXII', 'CCXXIII', 'CCXXIV', 'CCXXV', 'CCXXVI', 'CCXXVII', 'CCXXVIII', 'CCXXIX', 'CCXXX', 'CCXXXI', 'CCXXXII', 'CCXXXIII', 'CCXXXIV', 'CCXXXV', 'CCXXXVI', 'CCXXXVII', 'CCXXXVIII', 'CCXXXIX', 'CCXL', 'CCXLI', 'CCXLII', 'CCXLIII', 'CCXLIV', 'CCVL', 'CCVLI', 'CCVLII', 'CCVLIII', 'CCIL', 'CCL', 'CCLI', 'CCLII', 'CCLIII', 'CCLIV', 'CCLV', 'CCLVI', 'CCLVII', 'CCLVIII', 'CCLIX', 'CCLX', 'CCLXI', 'CCLXII', 'CCLXIII', 'CCLXIV', 'CCLXV', 'CCLXVI', 'CCLXVII', 'CCLXVIII', 'CCLXIX', 'CCLXX', 'CCLXXI', 'CCLXXII', 'CCLXXIII', 'CCLXXIV', 'CCLXXV', 'CCLXXVI', 'CCLXXVII', 'CCLXXVIII', 'CCLXXIX', 'CCLXXX', 'CCLXXXI', 'CCLXXXII', 'CCLXXXIII', 'CCLXXXIV', 'CCLXXXV', 'CCLXXXVI', 'CCLXXXVII', 'CCLXXXVIII', 'CCLXXXIX', 'CCXC', 'CCXCI', 'CCXCII', 'CCXCIII', 'CCXCIV', 'CCVC', 'CCVCI', 'CCVCII', 'CCVCIII', 'CCIC', 'CCC', 'CCCI', 'CCCII', 'CCCIII', 'CCCIV', 'CCCV', 'CCCVI', 'CCCVII', 'CCCVIII', 'CCCIX', 'CCCX', 'CCCXI', 'CCCXII', 'CCCXIII', 'CCCXIV', 'CCCXV', 'CCCXVI', 'CCCXVII', 'CCCXVIII', 'CCCXIX', 'CCCXX', 'CCCXXI', 'CCCXXII', 'CCCXXIII', 'CCCXXIV', 'CCCXXV', 'CCCXXVI', 'CCCXXVII', 'CCCXXVIII', 'CCCXXIX', 'CCCXXX', 'CCCXXXI', 'CCCXXXII', 'CCCXXXIII', 'CCCXXXIV', 'CCCXXXV', 'CCCXXXVI', 'CCCXXXVII', 'CCCXXXVIII', 'CCCXXXIX', 'CCCXL', 'CCCXLI', 'CCCXLII', 'CCCXLIII', 'CCCXLIV', 'CCCVL', 'CCCVLI', 'CCCVLII', 'CCCVLIII', 'CCCIL', 'CCCL', 'CCCLI', 'CCCLII', 'CCCLIII', 'CCCLIV', 'CCCLV', 'CCCLVI', 'CCCLVII', 'CCCLVIII', 'CCCLIX', 'CCCLX', 'CCCLXI', 'CCCLXII', 'CCCLXIII', 'CCCLXIV', 'CCCLXV', 'CCCLXVI', 'CCCLXVII', 'CCCLXVIII', 'CCCLXIX', 'CCCLXX', 'CCCLXXI', 'CCCLXXII', 'CCCLXXIII', 'CCCLXXIV', 'CCCLXXV', 'CCCLXXVI', 'CCCLXXVII', 'CCCLXXVIII', 'CCCLXXIX', 'CCCLXXX', 'CCCLXXXI', 'CCCLXXXII', 'CCCLXXXIII', 'CCCLXXXIV', 'CCCLXXXV', 'CCCLXXXVI', 'CCCLXXXVII', 'CCCLXXXVIII', 'CCCLXXXIX', 'CCCXC', 'CCCXCI', 'CCCXCII', 'CCCXCIII', 'CCCXCIV', 'CCCVC', 'CCCVCI', 'CCCVCII', 'CCCVCIII', 'CCCIC', 'CD', 'CDI', 'CDII', 'CDIII', 'CDIV', 'CDV', 'CDVI', 'CDVII', 'CDVIII', 'CDIX', 'CDX', 'CDXI', 'CDXII', 'CDXIII', 'CDXIV', 'CDXV', 'CDXVI', 'CDXVII', 'CDXVIII', 'CDXIX', 'CDXX', 'CDXXI', 'CDXXII', 'CDXXIII', 'CDXXIV', 'CDXXV', 'CDXXVI', 'CDXXVII', 'CDXXVIII', 'CDXXIX', 'CDXXX', 'CDXXXI', 'CDXXXII', 'CDXXXIII', 'CDXXXIV', 'CDXXXV', 'CDXXXVI', 'CDXXXVII', 'CDXXXVIII', 'CDXXXIX', 'CDXL', 'CDXLI', 'CDXLII', 'CDXLIII', 'CDXLIV', 'CDVL', 'CDVLI', 'CDVLII', 'CDVLIII', 'CDIL', 'LD', 'LDI', 'LDII', 'LDIII', 'LDIV', 'LDV', 'LDVI', 'LDVII', 'LDVIII', 'LDIX', 'LDX', 'LDXI', 'LDXII', 'LDXIII', 'LDXIV', 'LDXV', 'LDXVI', 'LDXVII', 'LDXVIII', 'LDXIX', 'LDXX', 'LDXXI', 'LDXXII', 'LDXXIII', 'LDXXIV', 'LDXXV', 'LDXXVI', 'LDXXVII', 'LDXXVIII', 'LDXXIX', 'LDXXX', 'LDXXXI', 'LDXXXII', 'LDXXXIII', 'LDXXXIV', 'LDXXXV', 'LDXXXVI', 'LDXXXVII', 'LDXXXVIII', 'LDXXXIX', 'XD', 'XDI', 'XDII', 'XDIII', 'XDIV', 'VD', 'VDI', 'VDII', 'VDIII', 'ID', 'D', 'DI', 'DII', 'DIII', 'DIV', 'DV', 'DVI', 'DVII', 'DVIII', 'DIX', 'DX', 'DXI', 'DXII', 'DXIII', 'DXIV', 'DXV', 'DXVI', 'DXVII', 'DXVIII', 'DXIX', 'DXX', 'DXXI', 'DXXII', 'DXXIII', 'DXXIV', 'DXXV', 'DXXVI', 'DXXVII', 'DXXVIII', 'DXXIX', 'DXXX', 'DXXXI', 'DXXXII', 'DXXXIII', 'DXXXIV', 'DXXXV', 'DXXXVI', 'DXXXVII', 'DXXXVIII', 'DXXXIX', 'DXL', 'DXLI', 'DXLII', 'DXLIII', 'DXLIV', 'DVL', 'DVLI', 'DVLII', 'DVLIII', 'DIL', 'DL', 'DLI', 'DLII', 'DLIII', 'DLIV', 'DLV', 'DLVI', 'DLVII', 'DLVIII', 'DLIX', 'DLX', 'DLXI', 'DLXII', 'DLXIII', 'DLXIV', 'DLXV', 'DLXVI', 'DLXVII', 'DLXVIII', 'DLXIX', 'DLXX', 'DLXXI', 'DLXXII', 'DLXXIII', 'DLXXIV', 'DLXXV', 'DLXXVI', 'DLXXVII', 'DLXXVIII', 'DLXXIX', 'DLXXX', 'DLXXXI', 'DLXXXII', 'DLXXXIII', 'DLXXXIV', 'DLXXXV', 'DLXXXVI', 'DLXXXVII', 'DLXXXVIII', 'DLXXXIX', 'DXC', 'DXCI', 'DXCII', 'DXCIII', 'DXCIV', 'DVC', 'DVCI', 'DVCII', 'DVCIII', 'DIC', 'DC', 'DCI', 'DCII', 'DCIII', 'DCIV', 'DCV', 'DCVI', 'DCVII', 'DCVIII', 'DCIX', 'DCX', 'DCXI', 'DCXII', 'DCXIII', 'DCXIV', 'DCXV', 'DCXVI', 'DCXVII', 'DCXVIII', 'DCXIX', 'DCXX', 'DCXXI', 'DCXXII', 'DCXXIII', 'DCXXIV', 'DCXXV', 'DCXXVI', 'DCXXVII', 'DCXXVIII', 'DCXXIX', 'DCXXX', 'DCXXXI', 'DCXXXII', 'DCXXXIII', 'DCXXXIV', 'DCXXXV', 'DCXXXVI', 'DCXXXVII', 'DCXXXVIII', 'DCXXXIX', 'DCXL', 'DCXLI', 'DCXLII', 'DCXLIII', 'DCXLIV', 'DCVL', 'DCVLI', 'DCVLII', 'DCVLIII', 'DCIL', 'DCL', 'DCLI', 'DCLII', 'DCLIII', 'DCLIV', 'DCLV', 'DCLVI', 'DCLVII', 'DCLVIII', 'DCLIX', 'DCLX', 'DCLXI', 'DCLXII', 'DCLXIII', 'DCLXIV', 'DCLXV', 'DCLXVI', 'DCLXVII', 'DCLXVIII', 'DCLXIX', 'DCLXX', 'DCLXXI', 'DCLXXII', 'DCLXXIII', 'DCLXXIV', 'DCLXXV', 'DCLXXVI', 'DCLXXVII', 'DCLXXVIII', 'DCLXXIX', 'DCLXXX', 'DCLXXXI', 'DCLXXXII', 'DCLXXXIII', 'DCLXXXIV', 'DCLXXXV', 'DCLXXXVI', 'DCLXXXVII', 'DCLXXXVIII', 'DCLXXXIX', 'DCXC', 'DCXCI', 'DCXCII', 'DCXCIII', 'DCXCIV', 'DCVC', 'DCVCI', 'DCVCII', 'DCVCIII', 'DCIC', 'DCC', 'DCCI', 'DCCII', 'DCCIII', 'DCCIV', 'DCCV', 'DCCVI', 'DCCVII', 'DCCVIII', 'DCCIX', 'DCCX', 'DCCXI', 'DCCXII', 'DCCXIII', 'DCCXIV', 'DCCXV', 'DCCXVI', 'DCCXVII', 'DCCXVIII', 'DCCXIX', 'DCCXX', 'DCCXXI', 'DCCXXII', 'DCCXXIII', 'DCCXXIV', 'DCCXXV', 'DCCXXVI', 'DCCXXVII', 'DCCXXVIII', 'DCCXXIX', 'DCCXXX', 'DCCXXXI', 'DCCXXXII', 'DCCXXXIII', 'DCCXXXIV', 'DCCXXXV', 'DCCXXXVI', 'DCCXXXVII', 'DCCXXXVIII', 'DCCXXXIX', 'DCCXL', 'DCCXLI', 'DCCXLII', 'DCCXLIII', 'DCCXLIV', 'DCCVL', 'DCCVLI', 'DCCVLII', 'DCCVLIII', 'DCCIL', 'DCCL', 'DCCLI', 'DCCLII', 'DCCLIII', 'DCCLIV', 'DCCLV', 'DCCLVI', 'DCCLVII', 'DCCLVIII', 'DCCLIX', 'DCCLX', 'DCCLXI', 'DCCLXII', 'DCCLXIII', 'DCCLXIV', 'DCCLXV', 'DCCLXVI', 'DCCLXVII', 'DCCLXVIII', 'DCCLXIX', 'DCCLXX', 'DCCLXXI', 'DCCLXXII', 'DCCLXXIII', 'DCCLXXIV', 'DCCLXXV', 'DCCLXXVI', 'DCCLXXVII', 'DCCLXXVIII', 'DCCLXXIX', 'DCCLXXX', 'DCCLXXXI', 'DCCLXXXII', 'DCCLXXXIII', 'DCCLXXXIV', 'DCCLXXXV', 'DCCLXXXVI', 'DCCLXXXVII', 'DCCLXXXVIII', 'DCCLXXXIX', 'DCCXC', 'DCCXCI', 'DCCXCII', 'DCCXCIII', 'DCCXCIV', 'DCCVC', 'DCCVCI', 'DCCVCII', 'DCCVCIII', 'DCCIC', 'DCCC', 'DCCCI', 'DCCCII', 'DCCCIII', 'DCCCIV', 'DCCCV', 'DCCCVI', 'DCCCVII', 'DCCCVIII', 'DCCCIX', 'DCCCX', 'DCCCXI', 'DCCCXII', 'DCCCXIII', 'DCCCXIV', 'DCCCXV', 'DCCCXVI', 'DCCCXVII', 'DCCCXVIII', 'DCCCXIX', 'DCCCXX', 'DCCCXXI', 'DCCCXXII', 'DCCCXXIII', 'DCCCXXIV', 'DCCCXXV', 'DCCCXXVI', 'DCCCXXVII', 'DCCCXXVIII', 'DCCCXXIX', 'DCCCXXX', 'DCCCXXXI', 'DCCCXXXII', 'DCCCXXXIII', 'DCCCXXXIV', 'DCCCXXXV', 'DCCCXXXVI', 'DCCCXXXVII', 'DCCCXXXVIII', 'DCCCXXXIX', 'DCCCXL', 'DCCCXLI', 'DCCCXLII', 'DCCCXLIII', 'DCCCXLIV', 'DCCCVL', 'DCCCVLI', 'DCCCVLII', 'DCCCVLIII', 'DCCCIL', 'DCCCL', 'DCCCLI', 'DCCCLII', 'DCCCLIII', 'DCCCLIV', 'DCCCLV', 'DCCCLVI', 'DCCCLVII', 'DCCCLVIII', 'DCCCLIX', 'DCCCLX', 'DCCCLXI', 'DCCCLXII', 'DCCCLXIII', 'DCCCLXIV', 'DCCCLXV', 'DCCCLXVI', 'DCCCLXVII', 'DCCCLXVIII', 'DCCCLXIX', 'DCCCLXX', 'DCCCLXXI', 'DCCCLXXII', 'DCCCLXXIII', 'DCCCLXXIV', 'DCCCLXXV', 'DCCCLXXVI', 'DCCCLXXVII', 'DCCCLXXVIII', 'DCCCLXXIX', 'DCCCLXXX', 'DCCCLXXXI', 'DCCCLXXXII', 'DCCCLXXXIII', 'DCCCLXXXIV', 'DCCCLXXXV', 'DCCCLXXXVI', 'DCCCLXXXVII', 'DCCCLXXXVIII', 'DCCCLXXXIX', 'DCCCXC', 'DCCCXCI', 'DCCCXCII', 'DCCCXCIII', 'DCCCXCIV', 'DCCCVC', 'DCCCVCI', 'DCCCVCII', 'DCCCVCIII', 'DCCCIC', 'CM', 'CMI', 'CMII', 'CMIII', 'CMIV', 'CMV', 'CMVI', 'CMVII', 'CMVIII', 'CMIX', 'CMX', 'CMXI', 'CMXII', 'CMXIII', 'CMXIV', 'CMXV', 'CMXVI', 'CMXVII', 'CMXVIII', 'CMXIX', 'CMXX', 'CMXXI', 'CMXXII', 'CMXXIII', 'CMXXIV', 'CMXXV', 'CMXXVI', 'CMXXVII', 'CMXXVIII', 'CMXXIX', 'CMXXX', 'CMXXXI', 'CMXXXII', 'CMXXXIII', 'CMXXXIV', 'CMXXXV', 'CMXXXVI', 'CMXXXVII', 'CMXXXVIII', 'CMXXXIX', 'CMXL', 'CMXLI', 'CMXLII', 'CMXLIII', 'CMXLIV', 'CMVL', 'CMVLI', 'CMVLII', 'CMVLIII', 'CMIL', 'LM', 'LMI', 'LMII', 'LMIII', 'LMIV', 'LMV', 'LMVI', 'LMVII', 'LMVIII', 'LMIX', 'LMX', 'LMXI', 'LMXII', 'LMXIII', 'LMXIV', 'LMXV', 'LMXVI', 'LMXVII', 'LMXVIII', 'LMXIX', 'LMXX', 'LMXXI', 'LMXXII', 'LMXXIII', 'LMXXIV', 'LMXXV', 'LMXXVI', 'LMXXVII', 'LMXXVIII', 'LMXXIX', 'LMXXX', 'LMXXXI', 'LMXXXII', 'LMXXXIII', 'LMXXXIV', 'LMXXXV', 'LMXXXVI', 'LMXXXVII', 'LMXXXVIII', 'LMXXXIX', 'XM', 'XMI', 'XMII', 'XMIII', 'XMIV', 'VM', 'VMI', 'VMII', 'VMIII', 'IM', 'M', 'MI', 'MII', 'MIII', 'MIV', 'MV', 'MVI', 'MVII', 'MVIII', 'MIX', 'MX', 'MXI', 'MXII', 'MXIII', 'MXIV', 'MXV', 'MXVI', 'MXVII', 'MXVIII', 'MXIX', 'MXX', 'MXXI', 'MXXII', 'MXXIII', 'MXXIV', 'MXXV', 'MXXVI', 'MXXVII', 'MXXVIII', 'MXXIX', 'MXXX', 'MXXXI', 'MXXXII', 'MXXXIII', 'MXXXIV', 'MXXXV', 'MXXXVI', 'MXXXVII', 'MXXXVIII', 'MXXXIX', 'MXL', 'MXLI', 'MXLII', 'MXLIII', 'MXLIV', 'MVL', 'MVLI', 'MVLII', 'MVLIII', 'MIL', 'ML', 'MLI', 'MLII', 'MLIII', 'MLIV', 'MLV', 'MLVI', 'MLVII', 'MLVIII', 'MLIX', 'MLX', 'MLXI', 'MLXII', 'MLXIII', 'MLXIV', 'MLXV', 'MLXVI', 'MLXVII', 'MLXVIII', 'MLXIX', 'MLXX', 'MLXXI', 'MLXXII', 'MLXXIII', 'MLXXIV', 'MLXXV', 'MLXXVI', 'MLXXVII', 'MLXXVIII', 'MLXXIX', 'MLXXX', 'MLXXXI', 'MLXXXII', 'MLXXXIII', 'MLXXXIV', 'MLXXXV', 'MLXXXVI', 'MLXXXVII', 'MLXXXVIII', 'MLXXXIX', 'MXC', 'MXCI', 'MXCII', 'MXCIII', 'MXCIV', 'MVC', 'MVCI', 'MVCII', 'MVCIII', 'MIC', 'MC', 'MCI', 'MCII', 'MCIII', 'MCIV', 'MCV', 'MCVI', 'MCVII', 'MCVIII', 'MCIX', 'MCX', 'MCXI', 'MCXII', 'MCXIII', 'MCXIV', 'MCXV', 'MCXVI', 'MCXVII', 'MCXVIII', 'MCXIX', 'MCXX', 'MCXXI', 'MCXXII', 'MCXXIII', 'MCXXIV', 'MCXXV', 'MCXXVI', 'MCXXVII', 'MCXXVIII', 'MCXXIX', 'MCXXX', 'MCXXXI', 'MCXXXII', 'MCXXXIII', 'MCXXXIV', 'MCXXXV', 'MCXXXVI', 'MCXXXVII', 'MCXXXVIII', 'MCXXXIX', 'MCXL', 'MCXLI', 'MCXLII', 'MCXLIII', 'MCXLIV', 'MCVL', 'MCVLI', 'MCVLII', 'MCVLIII', 'MCIL', 'MCL', 'MCLI', 'MCLII', 'MCLIII', 'MCLIV', 'MCLV', 'MCLVI', 'MCLVII', 'MCLVIII', 'MCLIX', 'MCLX', 'MCLXI', 'MCLXII', 'MCLXIII', 'MCLXIV', 'MCLXV', 'MCLXVI', 'MCLXVII', 'MCLXVIII', 'MCLXIX', 'MCLXX', 'MCLXXI', 'MCLXXII', 'MCLXXIII', 'MCLXXIV', 'MCLXXV', 'MCLXXVI', 'MCLXXVII', 'MCLXXVIII', 'MCLXXIX', 'MCLXXX', 'MCLXXXI', 'MCLXXXII', 'MCLXXXIII', 'MCLXXXIV', 'MCLXXXV', 'MCLXXXVI', 'MCLXXXVII', 'MCLXXXVIII', 'MCLXXXIX', 'MCXC', 'MCXCI', 'MCXCII', 'MCXCIII', 'MCXCIV', 'MCVC', 'MCVCI', 'MCVCII', 'MCVCIII', 'MCIC', 'MCC', 'MCCI', 'MCCII', 'MCCIII', 'MCCIV', 'MCCV', 'MCCVI', 'MCCVII', 'MCCVIII', 'MCCIX', 'MCCX', 'MCCXI', 'MCCXII', 'MCCXIII', 'MCCXIV', 'MCCXV', 'MCCXVI', 'MCCXVII', 'MCCXVIII', 'MCCXIX', 'MCCXX', 'MCCXXI', 'MCCXXII', 'MCCXXIII', 'MCCXXIV', 'MCCXXV', 'MCCXXVI', 'MCCXXVII', 'MCCXXVIII', 'MCCXXIX', 'MCCXXX', 'MCCXXXI', 'MCCXXXII', 'MCCXXXIII', 'MCCXXXIV', 'MCCXXXV', 'MCCXXXVI', 'MCCXXXVII', 'MCCXXXVIII', 'MCCXXXIX', 'MCCXL', 'MCCXLI', 'MCCXLII', 'MCCXLIII', 'MCCXLIV', 'MCCVL', 'MCCVLI', 'MCCVLII', 'MCCVLIII', 'MCCIL', 'MCCL', 'MCCLI', 'MCCLII', 'MCCLIII', 'MCCLIV', 'MCCLV', 'MCCLVI', 'MCCLVII', 'MCCLVIII', 'MCCLIX', 'MCCLX', 'MCCLXI', 'MCCLXII', 'MCCLXIII', 'MCCLXIV', 'MCCLXV', 'MCCLXVI', 'MCCLXVII', 'MCCLXVIII', 'MCCLXIX', 'MCCLXX', 'MCCLXXI', 'MCCLXXII', 'MCCLXXIII', 'MCCLXXIV', 'MCCLXXV', 'MCCLXXVI', 'MCCLXXVII', 'MCCLXXVIII', 'MCCLXXIX', 'MCCLXXX', 'MCCLXXXI', 'MCCLXXXII', 'MCCLXXXIII', 'MCCLXXXIV', 'MCCLXXXV', 'MCCLXXXVI', 'MCCLXXXVII', 'MCCLXXXVIII', 'MCCLXXXIX', 'MCCXC', 'MCCXCI', 'MCCXCII', 'MCCXCIII', 'MCCXCIV', 'MCCVC', 'MCCVCI', 'MCCVCII', 'MCCVCIII', 'MCCIC', 'MCCC', 'MCCCI', 'MCCCII', 'MCCCIII', 'MCCCIV', 'MCCCV', 'MCCCVI', 'MCCCVII', 'MCCCVIII', 'MCCCIX', 'MCCCX', 'MCCCXI', 'MCCCXII', 'MCCCXIII', 'MCCCXIV', 'MCCCXV', 'MCCCXVI', 'MCCCXVII', 'MCCCXVIII', 'MCCCXIX', 'MCCCXX', 'MCCCXXI', 'MCCCXXII', 'MCCCXXIII', 'MCCCXXIV', 'MCCCXXV', 'MCCCXXVI', 'MCCCXXVII', 'MCCCXXVIII', 'MCCCXXIX', 'MCCCXXX', 'MCCCXXXI', 'MCCCXXXII', 'MCCCXXXIII', 'MCCCXXXIV', 'MCCCXXXV', 'MCCCXXXVI', 'MCCCXXXVII', 'MCCCXXXVIII', 'MCCCXXXIX', 'MCCCXL', 'MCCCXLI', 'MCCCXLII', 'MCCCXLIII', 'MCCCXLIV', 'MCCCVL', 'MCCCVLI', 'MCCCVLII', 'MCCCVLIII', 'MCCCIL', 'MCCCL', 'MCCCLI', 'MCCCLII', 'MCCCLIII', 'MCCCLIV', 'MCCCLV', 'MCCCLVI', 'MCCCLVII', 'MCCCLVIII', 'MCCCLIX', 'MCCCLX', 'MCCCLXI', 'MCCCLXII', 'MCCCLXIII', 'MCCCLXIV', 'MCCCLXV', 'MCCCLXVI', 'MCCCLXVII', 'MCCCLXVIII', 'MCCCLXIX', 'MCCCLXX', 'MCCCLXXI', 'MCCCLXXII', 'MCCCLXXIII', 'MCCCLXXIV', 'MCCCLXXV', 'MCCCLXXVI', 'MCCCLXXVII', 'MCCCLXXVIII', 'MCCCLXXIX', 'MCCCLXXX', 'MCCCLXXXI', 'MCCCLXXXII', 'MCCCLXXXIII', 'MCCCLXXXIV', 'MCCCLXXXV', 'MCCCLXXXVI', 'MCCCLXXXVII', 'MCCCLXXXVIII', 'MCCCLXXXIX', 'MCCCXC', 'MCCCXCI', 'MCCCXCII', 'MCCCXCIII', 'MCCCXCIV', 'MCCCVC', 'MCCCVCI', 'MCCCVCII', 'MCCCVCIII', 'MCCCIC', 'MCD', 'MCDI', 'MCDII', 'MCDIII', 'MCDIV', 'MCDV', 'MCDVI', 'MCDVII', 'MCDVIII', 'MCDIX', 'MCDX', 'MCDXI', 'MCDXII', 'MCDXIII', 'MCDXIV', 'MCDXV', 'MCDXVI', 'MCDXVII', 'MCDXVIII', 'MCDXIX', 'MCDXX', 'MCDXXI', 'MCDXXII', 'MCDXXIII', 'MCDXXIV', 'MCDXXV', 'MCDXXVI', 'MCDXXVII', 'MCDXXVIII', 'MCDXXIX', 'MCDXXX', 'MCDXXXI', 'MCDXXXII', 'MCDXXXIII', 'MCDXXXIV', 'MCDXXXV', 'MCDXXXVI', 'MCDXXXVII', 'MCDXXXVIII', 'MCDXXXIX', 'MCDXL', 'MCDXLI', 'MCDXLII', 'MCDXLIII', 'MCDXLIV', 'MCDVL', 'MCDVLI', 'MCDVLII', 'MCDVLIII', 'MCDIL', 'MLD', 'MLDI', 'MLDII', 'MLDIII', 'MLDIV', 'MLDV', 'MLDVI', 'MLDVII', 'MLDVIII', 'MLDIX', 'MLDX', 'MLDXI', 'MLDXII', 'MLDXIII', 'MLDXIV', 'MLDXV', 'MLDXVI', 'MLDXVII', 'MLDXVIII', 'MLDXIX', 'MLDXX', 'MLDXXI', 'MLDXXII', 'MLDXXIII', 'MLDXXIV', 'MLDXXV', 'MLDXXVI', 'MLDXXVII', 'MLDXXVIII', 'MLDXXIX', 'MLDXXX', 'MLDXXXI', 'MLDXXXII', 'MLDXXXIII', 'MLDXXXIV', 'MLDXXXV', 'MLDXXXVI', 'MLDXXXVII', 'MLDXXXVIII', 'MLDXXXIX', 'MXD', 'MXDI', 'MXDII', 'MXDIII', 'MXDIV', 'MVD', 'MVDI', 'MVDII', 'MVDIII', 'MID', 'MD', 'MDI', 'MDII', 'MDIII', 'MDIV', 'MDV', 'MDVI', 'MDVII', 'MDVIII', 'MDIX', 'MDX', 'MDXI', 'MDXII', 'MDXIII', 'MDXIV', 'MDXV', 'MDXVI', 'MDXVII', 'MDXVIII', 'MDXIX', 'MDXX', 'MDXXI', 'MDXXII', 'MDXXIII', 'MDXXIV', 'MDXXV', 'MDXXVI', 'MDXXVII', 'MDXXVIII', 'MDXXIX', 'MDXXX', 'MDXXXI', 'MDXXXII', 'MDXXXIII', 'MDXXXIV', 'MDXXXV', 'MDXXXVI', 'MDXXXVII', 'MDXXXVIII', 'MDXXXIX', 'MDXL', 'MDXLI', 'MDXLII', 'MDXLIII', 'MDXLIV', 'MDVL', 'MDVLI', 'MDVLII', 'MDVLIII', 'MDIL', 'MDL', 'MDLI', 'MDLII', 'MDLIII', 'MDLIV', 'MDLV', 'MDLVI', 'MDLVII', 'MDLVIII', 'MDLIX', 'MDLX', 'MDLXI', 'MDLXII', 'MDLXIII', 'MDLXIV', 'MDLXV', 'MDLXVI', 'MDLXVII', 'MDLXVIII', 'MDLXIX', 'MDLXX', 'MDLXXI', 'MDLXXII', 'MDLXXIII', 'MDLXXIV', 'MDLXXV', 'MDLXXVI', 'MDLXXVII', 'MDLXXVIII', 'MDLXXIX', 'MDLXXX', 'MDLXXXI', 'MDLXXXII', 'MDLXXXIII', 'MDLXXXIV', 'MDLXXXV', 'MDLXXXVI', 'MDLXXXVII', 'MDLXXXVIII', 'MDLXXXIX', 'MDXC', 'MDXCI', 'MDXCII', 'MDXCIII', 'MDXCIV', 'MDVC', 'MDVCI', 'MDVCII', 'MDVCIII', 'MDIC', 'MDC', 'MDCI', 'MDCII', 'MDCIII', 'MDCIV', 'MDCV', 'MDCVI', 'MDCVII', 'MDCVIII', 'MDCIX', 'MDCX', 'MDCXI', 'MDCXII', 'MDCXIII', 'MDCXIV', 'MDCXV', 'MDCXVI', 'MDCXVII', 'MDCXVIII', 'MDCXIX', 'MDCXX', 'MDCXXI', 'MDCXXII', 'MDCXXIII', 'MDCXXIV', 'MDCXXV', 'MDCXXVI', 'MDCXXVII', 'MDCXXVIII', 'MDCXXIX', 'MDCXXX', 'MDCXXXI', 'MDCXXXII', 'MDCXXXIII', 'MDCXXXIV', 'MDCXXXV', 'MDCXXXVI', 'MDCXXXVII', 'MDCXXXVIII', 'MDCXXXIX', 'MDCXL', 'MDCXLI', 'MDCXLII', 'MDCXLIII', 'MDCXLIV', 'MDCVL', 'MDCVLI', 'MDCVLII', 'MDCVLIII', 'MDCIL', 'MDCL', 'MDCLI', 'MDCLII', 'MDCLIII', 'MDCLIV', 'MDCLV', 'MDCLVI', 'MDCLVII', 'MDCLVIII', 'MDCLIX', 'MDCLX', 'MDCLXI', 'MDCLXII', 'MDCLXIII', 'MDCLXIV', 'MDCLXV', 'MDCLXVI', 'MDCLXVII', 'MDCLXVIII', 'MDCLXIX', 'MDCLXX', 'MDCLXXI', 'MDCLXXII', 'MDCLXXIII', 'MDCLXXIV', 'MDCLXXV', 'MDCLXXVI', 'MDCLXXVII', 'MDCLXXVIII', 'MDCLXXIX', 'MDCLXXX', 'MDCLXXXI', 'MDCLXXXII', 'MDCLXXXIII', 'MDCLXXXIV', 'MDCLXXXV', 'MDCLXXXVI', 'MDCLXXXVII', 'MDCLXXXVIII', 'MDCLXXXIX', 'MDCXC', 'MDCXCI', 'MDCXCII', 'MDCXCIII', 'MDCXCIV', 'MDCVC', 'MDCVCI', 'MDCVCII', 'MDCVCIII', 'MDCIC', 'MDCC', 'MDCCI', 'MDCCII', 'MDCCIII', 'MDCCIV', 'MDCCV', 'MDCCVI', 'MDCCVII', 'MDCCVIII', 'MDCCIX', 'MDCCX', 'MDCCXI', 'MDCCXII', 'MDCCXIII', 'MDCCXIV', 'MDCCXV', 'MDCCXVI', 'MDCCXVII', 'MDCCXVIII', 'MDCCXIX', 'MDCCXX', 'MDCCXXI', 'MDCCXXII', 'MDCCXXIII', 'MDCCXXIV', 'MDCCXXV', 'MDCCXXVI', 'MDCCXXVII', 'MDCCXXVIII', 'MDCCXXIX', 'MDCCXXX', 'MDCCXXXI', 'MDCCXXXII', 'MDCCXXXIII', 'MDCCXXXIV', 'MDCCXXXV', 'MDCCXXXVI', 'MDCCXXXVII', 'MDCCXXXVIII', 'MDCCXXXIX', 'MDCCXL', 'MDCCXLI', 'MDCCXLII', 'MDCCXLIII', 'MDCCXLIV', 'MDCCVL', 'MDCCVLI', 'MDCCVLII', 'MDCCVLIII', 'MDCCIL', 'MDCCL', 'MDCCLI', 'MDCCLII', 'MDCCLIII', 'MDCCLIV', 'MDCCLV', 'MDCCLVI', 'MDCCLVII', 'MDCCLVIII', 'MDCCLIX', 'MDCCLX', 'MDCCLXI', 'MDCCLXII', 'MDCCLXIII', 'MDCCLXIV', 'MDCCLXV', 'MDCCLXVI', 'MDCCLXVII', 'MDCCLXVIII', 'MDCCLXIX', 'MDCCLXX', 'MDCCLXXI', 'MDCCLXXII', 'MDCCLXXIII', 'MDCCLXXIV', 'MDCCLXXV', 'MDCCLXXVI', 'MDCCLXXVII', 'MDCCLXXVIII', 'MDCCLXXIX', 'MDCCLXXX', 'MDCCLXXXI', 'MDCCLXXXII', 'MDCCLXXXIII', 'MDCCLXXXIV', 'MDCCLXXXV', 'MDCCLXXXVI', 'MDCCLXXXVII', 'MDCCLXXXVIII', 'MDCCLXXXIX', 'MDCCXC', 'MDCCXCI', 'MDCCXCII', 'MDCCXCIII', 'MDCCXCIV', 'MDCCVC', 'MDCCVCI', 'MDCCVCII', 'MDCCVCIII', 'MDCCIC', 'MDCCC', 'MDCCCI', 'MDCCCII', 'MDCCCIII', 'MDCCCIV', 'MDCCCV', 'MDCCCVI', 'MDCCCVII', 'MDCCCVIII', 'MDCCCIX', 'MDCCCX', 'MDCCCXI', 'MDCCCXII', 'MDCCCXIII', 'MDCCCXIV', 'MDCCCXV', 'MDCCCXVI', 'MDCCCXVII', 'MDCCCXVIII', 'MDCCCXIX', 'MDCCCXX', 'MDCCCXXI', 'MDCCCXXII', 'MDCCCXXIII', 'MDCCCXXIV', 'MDCCCXXV', 'MDCCCXXVI', 'MDCCCXXVII', 'MDCCCXXVIII', 'MDCCCXXIX', 'MDCCCXXX', 'MDCCCXXXI', 'MDCCCXXXII', 'MDCCCXXXIII', 'MDCCCXXXIV', 'MDCCCXXXV', 'MDCCCXXXVI', 'MDCCCXXXVII', 'MDCCCXXXVIII', 'MDCCCXXXIX', 'MDCCCXL', 'MDCCCXLI', 'MDCCCXLII', 'MDCCCXLIII', 'MDCCCXLIV', 'MDCCCVL', 'MDCCCVLI', 'MDCCCVLII', 'MDCCCVLIII', 'MDCCCIL', 'MDCCCL', 'MDCCCLI', 'MDCCCLII', 'MDCCCLIII', 'MDCCCLIV', 'MDCCCLV', 'MDCCCLVI', 'MDCCCLVII', 'MDCCCLVIII', 'MDCCCLIX', 'MDCCCLX', 'MDCCCLXI', 'MDCCCLXII', 'MDCCCLXIII', 'MDCCCLXIV', 'MDCCCLXV', 'MDCCCLXVI', 'MDCCCLXVII', 'MDCCCLXVIII', 'MDCCCLXIX', 'MDCCCLXX', 'MDCCCLXXI', 'MDCCCLXXII', 'MDCCCLXXIII', 'MDCCCLXXIV', 'MDCCCLXXV', 'MDCCCLXXVI', 'MDCCCLXXVII', 'MDCCCLXXVIII', 'MDCCCLXXIX', 'MDCCCLXXX', 'MDCCCLXXXI', 'MDCCCLXXXII', 'MDCCCLXXXIII', 'MDCCCLXXXIV', 'MDCCCLXXXV', 'MDCCCLXXXVI', 'MDCCCLXXXVII', 'MDCCCLXXXVIII', 'MDCCCLXXXIX', 'MDCCCXC', 'MDCCCXCI', 'MDCCCXCII', 'MDCCCXCIII', 'MDCCCXCIV', 'MDCCCVC', 'MDCCCVCI', 'MDCCCVCII', 'MDCCCVCIII', 'MDCCCIC', 'MCM', 'MCMI', 'MCMII', 'MCMIII', 'MCMIV', 'MCMV', 'MCMVI', 'MCMVII', 'MCMVIII', 'MCMIX', 'MCMX', 'MCMXI', 'MCMXII', 'MCMXIII', 'MCMXIV', 'MCMXV', 'MCMXVI', 'MCMXVII', 'MCMXVIII', 'MCMXIX', 'MCMXX', 'MCMXXI', 'MCMXXII', 'MCMXXIII', 'MCMXXIV', 'MCMXXV', 'MCMXXVI', 'MCMXXVII', 'MCMXXVIII', 'MCMXXIX', 'MCMXXX', 'MCMXXXI', 'MCMXXXII', 'MCMXXXIII', 'MCMXXXIV', 'MCMXXXV', 'MCMXXXVI', 'MCMXXXVII', 'MCMXXXVIII', 'MCMXXXIX', 'MCMXL', 'MCMXLI', 'MCMXLII', 'MCMXLIII', 'MCMXLIV', 'MCMVL', 'MCMVLI', 'MCMVLII', 'MCMVLIII', 'MCMIL', 'MLM', 'MLMI', 'MLMII', 'MLMIII', 'MLMIV', 'MLMV', 'MLMVI', 'MLMVII', 'MLMVIII', 'MLMIX', 'MLMX', 'MLMXI', 'MLMXII', 'MLMXIII', 'MLMXIV', 'MLMXV', 'MLMXVI', 'MLMXVII', 'MLMXVIII', 'MLMXIX', 'MLMXX', 'MLMXXI', 'MLMXXII', 'MLMXXIII', 'MLMXXIV', 'MLMXXV', 'MLMXXVI', 'MLMXXVII', 'MLMXXVIII', 'MLMXXIX', 'MLMXXX', 'MLMXXXI', 'MLMXXXII', 'MLMXXXIII', 'MLMXXXIV', 'MLMXXXV', 'MLMXXXVI', 'MLMXXXVII', 'MLMXXXVIII', 'MLMXXXIX', 'MXM', 'MXMI', 'MXMII', 'MXMIII', 'MXMIV', 'MVM', 'MVMI', 'MVMII', 'MVMIII', 'MIM', 'MM', 'MMI', 'MMII', 'MMIII', 'MMIV', 'MMV', 'MMVI', 'MMVII', 'MMVIII', 'MMIX', 'MMX', 'MMXI', 'MMXII', 'MMXIII', 'MMXIV', 'MMXV', 'MMXVI', 'MMXVII', 'MMXVIII', 'MMXIX', 'MMXX', 'MMXXI', 'MMXXII', 'MMXXIII', 'MMXXIV', 'MMXXV', 'MMXXVI', 'MMXXVII', 'MMXXVIII', 'MMXXIX', 'MMXXX', 'MMXXXI', 'MMXXXII', 'MMXXXIII', 'MMXXXIV', 'MMXXXV', 'MMXXXVI', 'MMXXXVII', 'MMXXXVIII', 'MMXXXIX', 'MMXL', 'MMXLI', 'MMXLII', 'MMXLIII', 'MMXLIV', 'MMVL', 'MMVLI', 'MMVLII', 'MMVLIII', 'MMIL', 'MML', 'MMLI', 'MMLII', 'MMLIII', 'MMLIV', 'MMLV', 'MMLVI', 'MMLVII', 'MMLVIII', 'MMLIX', 'MMLX', 'MMLXI', 'MMLXII', 'MMLXIII', 'MMLXIV', 'MMLXV', 'MMLXVI', 'MMLXVII', 'MMLXVIII', 'MMLXIX', 'MMLXX', 'MMLXXI', 'MMLXXII', 'MMLXXIII', 'MMLXXIV', 'MMLXXV', 'MMLXXVI', 'MMLXXVII', 'MMLXXVIII', 'MMLXXIX', 'MMLXXX', 'MMLXXXI', 'MMLXXXII', 'MMLXXXIII', 'MMLXXXIV', 'MMLXXXV', 'MMLXXXVI', 'MMLXXXVII', 'MMLXXXVIII', 'MMLXXXIX', 'MMXC', 'MMXCI', 'MMXCII', 'MMXCIII', 'MMXCIV', 'MMVC', 'MMVCI', 'MMVCII', 'MMVCIII', 'MMIC', 'MMC', 'MMCI', 'MMCII', 'MMCIII', 'MMCIV', 'MMCV', 'MMCVI', 'MMCVII', 'MMCVIII', 'MMCIX', 'MMCX', 'MMCXI', 'MMCXII', 'MMCXIII', 'MMCXIV', 'MMCXV', 'MMCXVI', 'MMCXVII', 'MMCXVIII', 'MMCXIX', 'MMCXX', 'MMCXXI', 'MMCXXII', 'MMCXXIII', 'MMCXXIV', 'MMCXXV', 'MMCXXVI', 'MMCXXVII', 'MMCXXVIII', 'MMCXXIX', 'MMCXXX', 'MMCXXXI', 'MMCXXXII', 'MMCXXXIII', 'MMCXXXIV', 'MMCXXXV', 'MMCXXXVI', 'MMCXXXVII', 'MMCXXXVIII', 'MMCXXXIX', 'MMCXL', 'MMCXLI', 'MMCXLII', 'MMCXLIII', 'MMCXLIV', 'MMCVL', 'MMCVLI', 'MMCVLII', 'MMCVLIII', 'MMCIL', 'MMCL', 'MMCLI', 'MMCLII', 'MMCLIII', 'MMCLIV', 'MMCLV', 'MMCLVI', 'MMCLVII', 'MMCLVIII', 'MMCLIX', 'MMCLX', 'MMCLXI', 'MMCLXII', 'MMCLXIII', 'MMCLXIV', 'MMCLXV', 'MMCLXVI', 'MMCLXVII', 'MMCLXVIII', 'MMCLXIX', 'MMCLXX', 'MMCLXXI', 'MMCLXXII', 'MMCLXXIII', 'MMCLXXIV', 'MMCLXXV', 'MMCLXXVI', 'MMCLXXVII', 'MMCLXXVIII', 'MMCLXXIX', 'MMCLXXX', 'MMCLXXXI', 'MMCLXXXII', 'MMCLXXXIII', 'MMCLXXXIV', 'MMCLXXXV', 'MMCLXXXVI', 'MMCLXXXVII', 'MMCLXXXVIII', 'MMCLXXXIX', 'MMCXC', 'MMCXCI', 'MMCXCII', 'MMCXCIII', 'MMCXCIV', 'MMCVC', 'MMCVCI', 'MMCVCII', 'MMCVCIII', 'MMCIC', 'MMCC', 'MMCCI', 'MMCCII', 'MMCCIII', 'MMCCIV', 'MMCCV', 'MMCCVI', 'MMCCVII', 'MMCCVIII', 'MMCCIX', 'MMCCX', 'MMCCXI', 'MMCCXII', 'MMCCXIII', 'MMCCXIV', 'MMCCXV', 'MMCCXVI', 'MMCCXVII', 'MMCCXVIII', 'MMCCXIX', 'MMCCXX', 'MMCCXXI', 'MMCCXXII', 'MMCCXXIII', 'MMCCXXIV', 'MMCCXXV', 'MMCCXXVI', 'MMCCXXVII', 'MMCCXXVIII', 'MMCCXXIX', 'MMCCXXX', 'MMCCXXXI', 'MMCCXXXII', 'MMCCXXXIII', 'MMCCXXXIV', 'MMCCXXXV', 'MMCCXXXVI', 'MMCCXXXVII', 'MMCCXXXVIII', 'MMCCXXXIX', 'MMCCXL', 'MMCCXLI', 'MMCCXLII', 'MMCCXLIII', 'MMCCXLIV', 'MMCCVL', 'MMCCVLI', 'MMCCVLII', 'MMCCVLIII', 'MMCCIL', 'MMCCL', 'MMCCLI', 'MMCCLII', 'MMCCLIII', 'MMCCLIV', 'MMCCLV', 'MMCCLVI', 'MMCCLVII', 'MMCCLVIII', 'MMCCLIX', 'MMCCLX', 'MMCCLXI', 'MMCCLXII', 'MMCCLXIII', 'MMCCLXIV', 'MMCCLXV', 'MMCCLXVI', 'MMCCLXVII', 'MMCCLXVIII', 'MMCCLXIX', 'MMCCLXX', 'MMCCLXXI', 'MMCCLXXII', 'MMCCLXXIII', 'MMCCLXXIV', 'MMCCLXXV', 'MMCCLXXVI', 'MMCCLXXVII', 'MMCCLXXVIII', 'MMCCLXXIX', 'MMCCLXXX', 'MMCCLXXXI', 'MMCCLXXXII', 'MMCCLXXXIII', 'MMCCLXXXIV', 'MMCCLXXXV', 'MMCCLXXXVI', 'MMCCLXXXVII', 'MMCCLXXXVIII', 'MMCCLXXXIX', 'MMCCXC', 'MMCCXCI', 'MMCCXCII', 'MMCCXCIII', 'MMCCXCIV', 'MMCCVC', 'MMCCVCI', 'MMCCVCII', 'MMCCVCIII', 'MMCCIC', 'MMCCC', 'MMCCCI', 'MMCCCII', 'MMCCCIII', 'MMCCCIV', 'MMCCCV', 'MMCCCVI', 'MMCCCVII', 'MMCCCVIII', 'MMCCCIX', 'MMCCCX', 'MMCCCXI', 'MMCCCXII', 'MMCCCXIII', 'MMCCCXIV', 'MMCCCXV', 'MMCCCXVI', 'MMCCCXVII', 'MMCCCXVIII', 'MMCCCXIX', 'MMCCCXX', 'MMCCCXXI', 'MMCCCXXII', 'MMCCCXXIII', 'MMCCCXXIV', 'MMCCCXXV', 'MMCCCXXVI', 'MMCCCXXVII', 'MMCCCXXVIII', 'MMCCCXXIX', 'MMCCCXXX', 'MMCCCXXXI', 'MMCCCXXXII', 'MMCCCXXXIII', 'MMCCCXXXIV', 'MMCCCXXXV', 'MMCCCXXXVI', 'MMCCCXXXVII', 'MMCCCXXXVIII', 'MMCCCXXXIX', 'MMCCCXL', 'MMCCCXLI', 'MMCCCXLII', 'MMCCCXLIII', 'MMCCCXLIV', 'MMCCCVL', 'MMCCCVLI', 'MMCCCVLII', 'MMCCCVLIII', 'MMCCCIL', 'MMCCCL', 'MMCCCLI', 'MMCCCLII', 'MMCCCLIII', 'MMCCCLIV', 'MMCCCLV', 'MMCCCLVI', 'MMCCCLVII', 'MMCCCLVIII', 'MMCCCLIX', 'MMCCCLX', 'MMCCCLXI', 'MMCCCLXII', 'MMCCCLXIII', 'MMCCCLXIV', 'MMCCCLXV', 'MMCCCLXVI', 'MMCCCLXVII', 'MMCCCLXVIII', 'MMCCCLXIX', 'MMCCCLXX', 'MMCCCLXXI', 'MMCCCLXXII', 'MMCCCLXXIII', 'MMCCCLXXIV', 'MMCCCLXXV', 'MMCCCLXXVI', 'MMCCCLXXVII', 'MMCCCLXXVIII', 'MMCCCLXXIX', 'MMCCCLXXX', 'MMCCCLXXXI', 'MMCCCLXXXII', 'MMCCCLXXXIII', 'MMCCCLXXXIV', 'MMCCCLXXXV', 'MMCCCLXXXVI', 'MMCCCLXXXVII', 'MMCCCLXXXVIII', 'MMCCCLXXXIX', 'MMCCCXC', 'MMCCCXCI', 'MMCCCXCII', 'MMCCCXCIII', 'MMCCCXCIV', 'MMCCCVC', 'MMCCCVCI', 'MMCCCVCII', 'MMCCCVCIII', 'MMCCCIC', 'MMCD', 'MMCDI', 'MMCDII', 'MMCDIII', 'MMCDIV', 'MMCDV', 'MMCDVI', 'MMCDVII', 'MMCDVIII', 'MMCDIX', 'MMCDX', 'MMCDXI', 'MMCDXII', 'MMCDXIII', 'MMCDXIV', 'MMCDXV', 'MMCDXVI', 'MMCDXVII', 'MMCDXVIII', 'MMCDXIX', 'MMCDXX', 'MMCDXXI', 'MMCDXXII', 'MMCDXXIII', 'MMCDXXIV', 'MMCDXXV', 'MMCDXXVI', 'MMCDXXVII', 'MMCDXXVIII', 'MMCDXXIX', 'MMCDXXX', 'MMCDXXXI', 'MMCDXXXII', 'MMCDXXXIII', 'MMCDXXXIV', 'MMCDXXXV', 'MMCDXXXVI', 'MMCDXXXVII', 'MMCDXXXVIII', 'MMCDXXXIX', 'MMCDXL', 'MMCDXLI', 'MMCDXLII', 'MMCDXLIII', 'MMCDXLIV', 'MMCDVL', 'MMCDVLI', 'MMCDVLII', 'MMCDVLIII', 'MMCDIL', 'MMLD', 'MMLDI', 'MMLDII', 'MMLDIII', 'MMLDIV', 'MMLDV', 'MMLDVI', 'MMLDVII', 'MMLDVIII', 'MMLDIX', 'MMLDX', 'MMLDXI', 'MMLDXII', 'MMLDXIII', 'MMLDXIV', 'MMLDXV', 'MMLDXVI', 'MMLDXVII', 'MMLDXVIII', 'MMLDXIX', 'MMLDXX', 'MMLDXXI', 'MMLDXXII', 'MMLDXXIII', 'MMLDXXIV', 'MMLDXXV', 'MMLDXXVI', 'MMLDXXVII', 'MMLDXXVIII', 'MMLDXXIX', 'MMLDXXX', 'MMLDXXXI', 'MMLDXXXII', 'MMLDXXXIII', 'MMLDXXXIV', 'MMLDXXXV', 'MMLDXXXVI', 'MMLDXXXVII', 'MMLDXXXVIII', 'MMLDXXXIX', 'MMXD', 'MMXDI', 'MMXDII', 'MMXDIII', 'MMXDIV', 'MMVD', 'MMVDI', 'MMVDII', 'MMVDIII', 'MMID', 'MMD', 'MMDI', 'MMDII', 'MMDIII', 'MMDIV', 'MMDV', 'MMDVI', 'MMDVII', 'MMDVIII', 'MMDIX', 'MMDX', 'MMDXI', 'MMDXII', 'MMDXIII', 'MMDXIV', 'MMDXV', 'MMDXVI', 'MMDXVII', 'MMDXVIII', 'MMDXIX', 'MMDXX', 'MMDXXI', 'MMDXXII', 'MMDXXIII', 'MMDXXIV', 'MMDXXV', 'MMDXXVI', 'MMDXXVII', 'MMDXXVIII', 'MMDXXIX', 'MMDXXX', 'MMDXXXI', 'MMDXXXII', 'MMDXXXIII', 'MMDXXXIV', 'MMDXXXV', 'MMDXXXVI', 'MMDXXXVII', 'MMDXXXVIII', 'MMDXXXIX', 'MMDXL', 'MMDXLI', 'MMDXLII', 'MMDXLIII', 'MMDXLIV', 'MMDVL', 'MMDVLI', 'MMDVLII', 'MMDVLIII', 'MMDIL', 'MMDL', 'MMDLI', 'MMDLII', 'MMDLIII', 'MMDLIV', 'MMDLV', 'MMDLVI', 'MMDLVII', 'MMDLVIII', 'MMDLIX', 'MMDLX', 'MMDLXI', 'MMDLXII', 'MMDLXIII', 'MMDLXIV', 'MMDLXV', 'MMDLXVI', 'MMDLXVII', 'MMDLXVIII', 'MMDLXIX', 'MMDLXX', 'MMDLXXI', 'MMDLXXII', 'MMDLXXIII', 'MMDLXXIV', 'MMDLXXV', 'MMDLXXVI', 'MMDLXXVII', 'MMDLXXVIII', 'MMDLXXIX', 'MMDLXXX', 'MMDLXXXI', 'MMDLXXXII', 'MMDLXXXIII', 'MMDLXXXIV', 'MMDLXXXV', 'MMDLXXXVI', 'MMDLXXXVII', 'MMDLXXXVIII', 'MMDLXXXIX', 'MMDXC', 'MMDXCI', 'MMDXCII', 'MMDXCIII', 'MMDXCIV', 'MMDVC', 'MMDVCI', 'MMDVCII', 'MMDVCIII', 'MMDIC', 'MMDC', 'MMDCI', 'MMDCII', 'MMDCIII', 'MMDCIV', 'MMDCV', 'MMDCVI', 'MMDCVII', 'MMDCVIII', 'MMDCIX', 'MMDCX', 'MMDCXI', 'MMDCXII', 'MMDCXIII', 'MMDCXIV', 'MMDCXV', 'MMDCXVI', 'MMDCXVII', 'MMDCXVIII', 'MMDCXIX', 'MMDCXX', 'MMDCXXI', 'MMDCXXII', 'MMDCXXIII', 'MMDCXXIV', 'MMDCXXV', 'MMDCXXVI', 'MMDCXXVII', 'MMDCXXVIII', 'MMDCXXIX', 'MMDCXXX', 'MMDCXXXI', 'MMDCXXXII', 'MMDCXXXIII', 'MMDCXXXIV', 'MMDCXXXV', 'MMDCXXXVI', 'MMDCXXXVII', 'MMDCXXXVIII', 'MMDCXXXIX', 'MMDCXL', 'MMDCXLI', 'MMDCXLII', 'MMDCXLIII', 'MMDCXLIV', 'MMDCVL', 'MMDCVLI', 'MMDCVLII', 'MMDCVLIII', 'MMDCIL', 'MMDCL', 'MMDCLI', 'MMDCLII', 'MMDCLIII', 'MMDCLIV', 'MMDCLV', 'MMDCLVI', 'MMDCLVII', 'MMDCLVIII', 'MMDCLIX', 'MMDCLX', 'MMDCLXI', 'MMDCLXII', 'MMDCLXIII', 'MMDCLXIV', 'MMDCLXV', 'MMDCLXVI', 'MMDCLXVII', 'MMDCLXVIII', 'MMDCLXIX', 'MMDCLXX', 'MMDCLXXI', 'MMDCLXXII', 'MMDCLXXIII', 'MMDCLXXIV', 'MMDCLXXV', 'MMDCLXXVI', 'MMDCLXXVII', 'MMDCLXXVIII', 'MMDCLXXIX', 'MMDCLXXX', 'MMDCLXXXI', 'MMDCLXXXII', 'MMDCLXXXIII', 'MMDCLXXXIV', 'MMDCLXXXV', 'MMDCLXXXVI', 'MMDCLXXXVII', 'MMDCLXXXVIII', 'MMDCLXXXIX', 'MMDCXC', 'MMDCXCI', 'MMDCXCII', 'MMDCXCIII', 'MMDCXCIV', 'MMDCVC', 'MMDCVCI', 'MMDCVCII', 'MMDCVCIII', 'MMDCIC', 'MMDCC', 'MMDCCI', 'MMDCCII', 'MMDCCIII', 'MMDCCIV', 'MMDCCV', 'MMDCCVI', 'MMDCCVII', 'MMDCCVIII', 'MMDCCIX', 'MMDCCX', 'MMDCCXI', 'MMDCCXII', 'MMDCCXIII', 'MMDCCXIV', 'MMDCCXV', 'MMDCCXVI', 'MMDCCXVII', 'MMDCCXVIII', 'MMDCCXIX', 'MMDCCXX', 'MMDCCXXI', 'MMDCCXXII', 'MMDCCXXIII', 'MMDCCXXIV', 'MMDCCXXV', 'MMDCCXXVI', 'MMDCCXXVII', 'MMDCCXXVIII', 'MMDCCXXIX', 'MMDCCXXX', 'MMDCCXXXI', 'MMDCCXXXII', 'MMDCCXXXIII', 'MMDCCXXXIV', 'MMDCCXXXV', 'MMDCCXXXVI', 'MMDCCXXXVII', 'MMDCCXXXVIII', 'MMDCCXXXIX', 'MMDCCXL', 'MMDCCXLI', 'MMDCCXLII', 'MMDCCXLIII', 'MMDCCXLIV', 'MMDCCVL', 'MMDCCVLI', 'MMDCCVLII', 'MMDCCVLIII', 'MMDCCIL', 'MMDCCL', 'MMDCCLI', 'MMDCCLII', 'MMDCCLIII', 'MMDCCLIV', 'MMDCCLV', 'MMDCCLVI', 'MMDCCLVII', 'MMDCCLVIII', 'MMDCCLIX', 'MMDCCLX', 'MMDCCLXI', 'MMDCCLXII', 'MMDCCLXIII', 'MMDCCLXIV', 'MMDCCLXV', 'MMDCCLXVI', 'MMDCCLXVII', 'MMDCCLXVIII', 'MMDCCLXIX', 'MMDCCLXX', 'MMDCCLXXI', 'MMDCCLXXII', 'MMDCCLXXIII', 'MMDCCLXXIV', 'MMDCCLXXV', 'MMDCCLXXVI', 'MMDCCLXXVII', 'MMDCCLXXVIII', 'MMDCCLXXIX', 'MMDCCLXXX', 'MMDCCLXXXI', 'MMDCCLXXXII', 'MMDCCLXXXIII', 'MMDCCLXXXIV', 'MMDCCLXXXV', 'MMDCCLXXXVI', 'MMDCCLXXXVII', 'MMDCCLXXXVIII', 'MMDCCLXXXIX', 'MMDCCXC', 'MMDCCXCI', 'MMDCCXCII', 'MMDCCXCIII', 'MMDCCXCIV', 'MMDCCVC', 'MMDCCVCI', 'MMDCCVCII', 'MMDCCVCIII', 'MMDCCIC', 'MMDCCC', 'MMDCCCI', 'MMDCCCII', 'MMDCCCIII', 'MMDCCCIV', 'MMDCCCV', 'MMDCCCVI', 'MMDCCCVII', 'MMDCCCVIII', 'MMDCCCIX', 'MMDCCCX', 'MMDCCCXI', 'MMDCCCXII', 'MMDCCCXIII', 'MMDCCCXIV', 'MMDCCCXV', 'MMDCCCXVI', 'MMDCCCXVII', 'MMDCCCXVIII', 'MMDCCCXIX', 'MMDCCCXX', 'MMDCCCXXI', 'MMDCCCXXII', 'MMDCCCXXIII', 'MMDCCCXXIV', 'MMDCCCXXV', 'MMDCCCXXVI', 'MMDCCCXXVII', 'MMDCCCXXVIII', 'MMDCCCXXIX', 'MMDCCCXXX', 'MMDCCCXXXI', 'MMDCCCXXXII', 'MMDCCCXXXIII', 'MMDCCCXXXIV', 'MMDCCCXXXV', 'MMDCCCXXXVI', 'MMDCCCXXXVII', 'MMDCCCXXXVIII', 'MMDCCCXXXIX', 'MMDCCCXL', 'MMDCCCXLI', 'MMDCCCXLII', 'MMDCCCXLIII', 'MMDCCCXLIV', 'MMDCCCVL', 'MMDCCCVLI', 'MMDCCCVLII', 'MMDCCCVLIII', 'MMDCCCIL', 'MMDCCCL', 'MMDCCCLI', 'MMDCCCLII', 'MMDCCCLIII', 'MMDCCCLIV', 'MMDCCCLV', 'MMDCCCLVI', 'MMDCCCLVII', 'MMDCCCLVIII', 'MMDCCCLIX', 'MMDCCCLX', 'MMDCCCLXI', 'MMDCCCLXII', 'MMDCCCLXIII', 'MMDCCCLXIV', 'MMDCCCLXV', 'MMDCCCLXVI', 'MMDCCCLXVII', 'MMDCCCLXVIII', 'MMDCCCLXIX', 'MMDCCCLXX', 'MMDCCCLXXI', 'MMDCCCLXXII', 'MMDCCCLXXIII', 'MMDCCCLXXIV', 'MMDCCCLXXV', 'MMDCCCLXXVI', 'MMDCCCLXXVII', 'MMDCCCLXXVIII', 'MMDCCCLXXIX', 'MMDCCCLXXX', 'MMDCCCLXXXI', 'MMDCCCLXXXII', 'MMDCCCLXXXIII', 'MMDCCCLXXXIV', 'MMDCCCLXXXV', 'MMDCCCLXXXVI', 'MMDCCCLXXXVII', 'MMDCCCLXXXVIII', 'MMDCCCLXXXIX', 'MMDCCCXC', 'MMDCCCXCI', 'MMDCCCXCII', 'MMDCCCXCIII', 'MMDCCCXCIV', 'MMDCCCVC', 'MMDCCCVCI', 'MMDCCCVCII', 'MMDCCCVCIII', 'MMDCCCIC', 'MMCM', 'MMCMI', 'MMCMII', 'MMCMIII', 'MMCMIV', 'MMCMV', 'MMCMVI', 'MMCMVII', 'MMCMVIII', 'MMCMIX', 'MMCMX', 'MMCMXI', 'MMCMXII', 'MMCMXIII', 'MMCMXIV', 'MMCMXV', 'MMCMXVI', 'MMCMXVII', 'MMCMXVIII', 'MMCMXIX', 'MMCMXX', 'MMCMXXI', 'MMCMXXII', 'MMCMXXIII', 'MMCMXXIV', 'MMCMXXV', 'MMCMXXVI', 'MMCMXXVII', 'MMCMXXVIII', 'MMCMXXIX', 'MMCMXXX', 'MMCMXXXI', 'MMCMXXXII', 'MMCMXXXIII', 'MMCMXXXIV', 'MMCMXXXV', 'MMCMXXXVI', 'MMCMXXXVII', 'MMCMXXXVIII', 'MMCMXXXIX', 'MMCMXL', 'MMCMXLI', 'MMCMXLII', 'MMCMXLIII', 'MMCMXLIV', 'MMCMVL', 'MMCMVLI', 'MMCMVLII', 'MMCMVLIII', 'MMCMIL', 'MMLM', 'MMLMI', 'MMLMII', 'MMLMIII', 'MMLMIV', 'MMLMV', 'MMLMVI', 'MMLMVII', 'MMLMVIII', 'MMLMIX', 'MMLMX', 'MMLMXI', 'MMLMXII', 'MMLMXIII', 'MMLMXIV', 'MMLMXV', 'MMLMXVI', 'MMLMXVII', 'MMLMXVIII', 'MMLMXIX', 'MMLMXX', 'MMLMXXI', 'MMLMXXII', 'MMLMXXIII', 'MMLMXXIV', 'MMLMXXV', 'MMLMXXVI', 'MMLMXXVII', 'MMLMXXVIII', 'MMLMXXIX', 'MMLMXXX', 'MMLMXXXI', 'MMLMXXXII', 'MMLMXXXIII', 'MMLMXXXIV', 'MMLMXXXV', 'MMLMXXXVI', 'MMLMXXXVII', 'MMLMXXXVIII', 'MMLMXXXIX', 'MMXM', 'MMXMI', 'MMXMII', 'MMXMIII', 'MMXMIV', 'MMVM', 'MMVMI', 'MMVMII', 'MMVMIII', 'MMIM', 'MMM', 'MMMI', 'MMMII', 'MMMIII', 'MMMIV', 'MMMV', 'MMMVI', 'MMMVII', 'MMMVIII', 'MMMIX', 'MMMX', 'MMMXI', 'MMMXII', 'MMMXIII', 'MMMXIV', 'MMMXV', 'MMMXVI', 'MMMXVII', 'MMMXVIII', 'MMMXIX', 'MMMXX', 'MMMXXI', 'MMMXXII', 'MMMXXIII', 'MMMXXIV', 'MMMXXV', 'MMMXXVI', 'MMMXXVII', 'MMMXXVIII', 'MMMXXIX', 'MMMXXX', 'MMMXXXI', 'MMMXXXII', 'MMMXXXIII', 'MMMXXXIV', 'MMMXXXV', 'MMMXXXVI', 'MMMXXXVII', 'MMMXXXVIII', 'MMMXXXIX', 'MMMXL', 'MMMXLI', 'MMMXLII', 'MMMXLIII', 'MMMXLIV', 'MMMVL', 'MMMVLI', 'MMMVLII', 'MMMVLIII', 'MMMIL', 'MMML', 'MMMLI', 'MMMLII', 'MMMLIII', 'MMMLIV', 'MMMLV', 'MMMLVI', 'MMMLVII', 'MMMLVIII', 'MMMLIX', 'MMMLX', 'MMMLXI', 'MMMLXII', 'MMMLXIII', 'MMMLXIV', 'MMMLXV', 'MMMLXVI', 'MMMLXVII', 'MMMLXVIII', 'MMMLXIX', 'MMMLXX', 'MMMLXXI', 'MMMLXXII', 'MMMLXXIII', 'MMMLXXIV', 'MMMLXXV', 'MMMLXXVI', 'MMMLXXVII', 'MMMLXXVIII', 'MMMLXXIX', 'MMMLXXX', 'MMMLXXXI', 'MMMLXXXII', 'MMMLXXXIII', 'MMMLXXXIV', 'MMMLXXXV', 'MMMLXXXVI', 'MMMLXXXVII', 'MMMLXXXVIII', 'MMMLXXXIX', 'MMMXC', 'MMMXCI', 'MMMXCII', 'MMMXCIII', 'MMMXCIV', 'MMMVC', 'MMMVCI', 'MMMVCII', 'MMMVCIII', 'MMMIC', 'MMMC', 'MMMCI', 'MMMCII', 'MMMCIII', 'MMMCIV', 'MMMCV', 'MMMCVI', 'MMMCVII', 'MMMCVIII', 'MMMCIX', 'MMMCX', 'MMMCXI', 'MMMCXII', 'MMMCXIII', 'MMMCXIV', 'MMMCXV', 'MMMCXVI', 'MMMCXVII', 'MMMCXVIII', 'MMMCXIX', 'MMMCXX', 'MMMCXXI', 'MMMCXXII', 'MMMCXXIII', 'MMMCXXIV', 'MMMCXXV', 'MMMCXXVI', 'MMMCXXVII', 'MMMCXXVIII', 'MMMCXXIX', 'MMMCXXX', 'MMMCXXXI', 'MMMCXXXII', 'MMMCXXXIII', 'MMMCXXXIV', 'MMMCXXXV', 'MMMCXXXVI', 'MMMCXXXVII', 'MMMCXXXVIII', 'MMMCXXXIX', 'MMMCXL', 'MMMCXLI', 'MMMCXLII', 'MMMCXLIII', 'MMMCXLIV', 'MMMCVL', 'MMMCVLI', 'MMMCVLII', 'MMMCVLIII', 'MMMCIL', 'MMMCL', 'MMMCLI', 'MMMCLII', 'MMMCLIII', 'MMMCLIV', 'MMMCLV', 'MMMCLVI', 'MMMCLVII', 'MMMCLVIII', 'MMMCLIX', 'MMMCLX', 'MMMCLXI', 'MMMCLXII', 'MMMCLXIII', 'MMMCLXIV', 'MMMCLXV', 'MMMCLXVI', 'MMMCLXVII', 'MMMCLXVIII', 'MMMCLXIX', 'MMMCLXX', 'MMMCLXXI', 'MMMCLXXII', 'MMMCLXXIII', 'MMMCLXXIV', 'MMMCLXXV', 'MMMCLXXVI', 'MMMCLXXVII', 'MMMCLXXVIII', 'MMMCLXXIX', 'MMMCLXXX', 'MMMCLXXXI', 'MMMCLXXXII', 'MMMCLXXXIII', 'MMMCLXXXIV', 'MMMCLXXXV', 'MMMCLXXXVI', 'MMMCLXXXVII', 'MMMCLXXXVIII', 'MMMCLXXXIX', 'MMMCXC', 'MMMCXCI', 'MMMCXCII', 'MMMCXCIII', 'MMMCXCIV', 'MMMCVC', 'MMMCVCI', 'MMMCVCII', 'MMMCVCIII', 'MMMCIC', 'MMMCC', 'MMMCCI', 'MMMCCII', 'MMMCCIII', 'MMMCCIV', 'MMMCCV', 'MMMCCVI', 'MMMCCVII', 'MMMCCVIII', 'MMMCCIX', 'MMMCCX', 'MMMCCXI', 'MMMCCXII', 'MMMCCXIII', 'MMMCCXIV', 'MMMCCXV', 'MMMCCXVI', 'MMMCCXVII', 'MMMCCXVIII', 'MMMCCXIX', 'MMMCCXX', 'MMMCCXXI', 'MMMCCXXII', 'MMMCCXXIII', 'MMMCCXXIV', 'MMMCCXXV', 'MMMCCXXVI', 'MMMCCXXVII', 'MMMCCXXVIII', 'MMMCCXXIX', 'MMMCCXXX', 'MMMCCXXXI', 'MMMCCXXXII', 'MMMCCXXXIII', 'MMMCCXXXIV', 'MMMCCXXXV', 'MMMCCXXXVI', 'MMMCCXXXVII', 'MMMCCXXXVIII', 'MMMCCXXXIX', 'MMMCCXL', 'MMMCCXLI', 'MMMCCXLII', 'MMMCCXLIII', 'MMMCCXLIV', 'MMMCCVL', 'MMMCCVLI', 'MMMCCVLII', 'MMMCCVLIII', 'MMMCCIL', 'MMMCCL', 'MMMCCLI', 'MMMCCLII', 'MMMCCLIII', 'MMMCCLIV', 'MMMCCLV', 'MMMCCLVI', 'MMMCCLVII', 'MMMCCLVIII', 'MMMCCLIX', 'MMMCCLX', 'MMMCCLXI', 'MMMCCLXII', 'MMMCCLXIII', 'MMMCCLXIV', 'MMMCCLXV', 'MMMCCLXVI', 'MMMCCLXVII', 'MMMCCLXVIII', 'MMMCCLXIX', 'MMMCCLXX', 'MMMCCLXXI', 'MMMCCLXXII', 'MMMCCLXXIII', 'MMMCCLXXIV', 'MMMCCLXXV', 'MMMCCLXXVI', 'MMMCCLXXVII', 'MMMCCLXXVIII', 'MMMCCLXXIX', 'MMMCCLXXX', 'MMMCCLXXXI', 'MMMCCLXXXII', 'MMMCCLXXXIII', 'MMMCCLXXXIV', 'MMMCCLXXXV', 'MMMCCLXXXVI', 'MMMCCLXXXVII', 'MMMCCLXXXVIII', 'MMMCCLXXXIX', 'MMMCCXC', 'MMMCCXCI', 'MMMCCXCII', 'MMMCCXCIII', 'MMMCCXCIV', 'MMMCCVC', 'MMMCCVCI', 'MMMCCVCII', 'MMMCCVCIII', 'MMMCCIC', 'MMMCCC', 'MMMCCCI', 'MMMCCCII', 'MMMCCCIII', 'MMMCCCIV', 'MMMCCCV', 'MMMCCCVI', 'MMMCCCVII', 'MMMCCCVIII', 'MMMCCCIX', 'MMMCCCX', 'MMMCCCXI', 'MMMCCCXII', 'MMMCCCXIII', 'MMMCCCXIV', 'MMMCCCXV', 'MMMCCCXVI', 'MMMCCCXVII', 'MMMCCCXVIII', 'MMMCCCXIX', 'MMMCCCXX', 'MMMCCCXXI', 'MMMCCCXXII', 'MMMCCCXXIII', 'MMMCCCXXIV', 'MMMCCCXXV', 'MMMCCCXXVI', 'MMMCCCXXVII', 'MMMCCCXXVIII', 'MMMCCCXXIX', 'MMMCCCXXX', 'MMMCCCXXXI', 'MMMCCCXXXII', 'MMMCCCXXXIII', 'MMMCCCXXXIV', 'MMMCCCXXXV', 'MMMCCCXXXVI', 'MMMCCCXXXVII', 'MMMCCCXXXVIII', 'MMMCCCXXXIX', 'MMMCCCXL', 'MMMCCCXLI', 'MMMCCCXLII', 'MMMCCCXLIII', 'MMMCCCXLIV', 'MMMCCCVL', 'MMMCCCVLI', 'MMMCCCVLII', 'MMMCCCVLIII', 'MMMCCCIL', 'MMMCCCL', 'MMMCCCLI', 'MMMCCCLII', 'MMMCCCLIII', 'MMMCCCLIV', 'MMMCCCLV', 'MMMCCCLVI', 'MMMCCCLVII', 'MMMCCCLVIII', 'MMMCCCLIX', 'MMMCCCLX', 'MMMCCCLXI', 'MMMCCCLXII', 'MMMCCCLXIII', 'MMMCCCLXIV', 'MMMCCCLXV', 'MMMCCCLXVI', 'MMMCCCLXVII', 'MMMCCCLXVIII', 'MMMCCCLXIX', 'MMMCCCLXX', 'MMMCCCLXXI', 'MMMCCCLXXII', 'MMMCCCLXXIII', 'MMMCCCLXXIV', 'MMMCCCLXXV', 'MMMCCCLXXVI', 'MMMCCCLXXVII', 'MMMCCCLXXVIII', 'MMMCCCLXXIX', 'MMMCCCLXXX', 'MMMCCCLXXXI', 'MMMCCCLXXXII', 'MMMCCCLXXXIII', 'MMMCCCLXXXIV', 'MMMCCCLXXXV', 'MMMCCCLXXXVI', 'MMMCCCLXXXVII', 'MMMCCCLXXXVIII', 'MMMCCCLXXXIX', 'MMMCCCXC', 'MMMCCCXCI', 'MMMCCCXCII', 'MMMCCCXCIII', 'MMMCCCXCIV', 'MMMCCCVC', 'MMMCCCVCI', 'MMMCCCVCII', 'MMMCCCVCIII', 'MMMCCCIC', 'MMMCD', 'MMMCDI', 'MMMCDII', 'MMMCDIII', 'MMMCDIV', 'MMMCDV', 'MMMCDVI', 'MMMCDVII', 'MMMCDVIII', 'MMMCDIX', 'MMMCDX', 'MMMCDXI', 'MMMCDXII', 'MMMCDXIII', 'MMMCDXIV', 'MMMCDXV', 'MMMCDXVI', 'MMMCDXVII', 'MMMCDXVIII', 'MMMCDXIX', 'MMMCDXX', 'MMMCDXXI', 'MMMCDXXII', 'MMMCDXXIII', 'MMMCDXXIV', 'MMMCDXXV', 'MMMCDXXVI', 'MMMCDXXVII', 'MMMCDXXVIII', 'MMMCDXXIX', 'MMMCDXXX', 'MMMCDXXXI', 'MMMCDXXXII', 'MMMCDXXXIII', 'MMMCDXXXIV', 'MMMCDXXXV', 'MMMCDXXXVI', 'MMMCDXXXVII', 'MMMCDXXXVIII', 'MMMCDXXXIX', 'MMMCDXL', 'MMMCDXLI', 'MMMCDXLII', 'MMMCDXLIII', 'MMMCDXLIV', 'MMMCDVL', 'MMMCDVLI', 'MMMCDVLII', 'MMMCDVLIII', 'MMMCDIL', 'MMMLD', 'MMMLDI', 'MMMLDII', 'MMMLDIII', 'MMMLDIV', 'MMMLDV', 'MMMLDVI', 'MMMLDVII', 'MMMLDVIII', 'MMMLDIX', 'MMMLDX', 'MMMLDXI', 'MMMLDXII', 'MMMLDXIII', 'MMMLDXIV', 'MMMLDXV', 'MMMLDXVI', 'MMMLDXVII', 'MMMLDXVIII', 'MMMLDXIX', 'MMMLDXX', 'MMMLDXXI', 'MMMLDXXII', 'MMMLDXXIII', 'MMMLDXXIV', 'MMMLDXXV', 'MMMLDXXVI', 'MMMLDXXVII', 'MMMLDXXVIII', 'MMMLDXXIX', 'MMMLDXXX', 'MMMLDXXXI', 'MMMLDXXXII', 'MMMLDXXXIII', 'MMMLDXXXIV', 'MMMLDXXXV', 'MMMLDXXXVI', 'MMMLDXXXVII', 'MMMLDXXXVIII', 'MMMLDXXXIX', 'MMMXD', 'MMMXDI', 'MMMXDII', 'MMMXDIII', 'MMMXDIV', 'MMMVD', 'MMMVDI', 'MMMVDII', 'MMMVDIII', 'MMMID', 'MMMD', 'MMMDI', 'MMMDII', 'MMMDIII', 'MMMDIV', 'MMMDV', 'MMMDVI', 'MMMDVII', 'MMMDVIII', 'MMMDIX', 'MMMDX', 'MMMDXI', 'MMMDXII', 'MMMDXIII', 'MMMDXIV', 'MMMDXV', 'MMMDXVI', 'MMMDXVII', 'MMMDXVIII', 'MMMDXIX', 'MMMDXX', 'MMMDXXI', 'MMMDXXII', 'MMMDXXIII', 'MMMDXXIV', 'MMMDXXV', 'MMMDXXVI', 'MMMDXXVII', 'MMMDXXVIII', 'MMMDXXIX', 'MMMDXXX', 'MMMDXXXI', 'MMMDXXXII', 'MMMDXXXIII', 'MMMDXXXIV', 'MMMDXXXV', 'MMMDXXXVI', 'MMMDXXXVII', 'MMMDXXXVIII', 'MMMDXXXIX', 'MMMDXL', 'MMMDXLI', 'MMMDXLII', 'MMMDXLIII', 'MMMDXLIV', 'MMMDVL', 'MMMDVLI', 'MMMDVLII', 'MMMDVLIII', 'MMMDIL', 'MMMDL', 'MMMDLI', 'MMMDLII', 'MMMDLIII', 'MMMDLIV', 'MMMDLV', 'MMMDLVI', 'MMMDLVII', 'MMMDLVIII', 'MMMDLIX', 'MMMDLX', 'MMMDLXI', 'MMMDLXII', 'MMMDLXIII', 'MMMDLXIV', 'MMMDLXV', 'MMMDLXVI', 'MMMDLXVII', 'MMMDLXVIII', 'MMMDLXIX', 'MMMDLXX', 'MMMDLXXI', 'MMMDLXXII', 'MMMDLXXIII', 'MMMDLXXIV', 'MMMDLXXV', 'MMMDLXXVI', 'MMMDLXXVII', 'MMMDLXXVIII', 'MMMDLXXIX', 'MMMDLXXX', 'MMMDLXXXI', 'MMMDLXXXII', 'MMMDLXXXIII', 'MMMDLXXXIV', 'MMMDLXXXV', 'MMMDLXXXVI', 'MMMDLXXXVII', 'MMMDLXXXVIII', 'MMMDLXXXIX', 'MMMDXC', 'MMMDXCI', 'MMMDXCII', 'MMMDXCIII', 'MMMDXCIV', 'MMMDVC', 'MMMDVCI', 'MMMDVCII', 'MMMDVCIII', 'MMMDIC', 'MMMDC', 'MMMDCI', 'MMMDCII', 'MMMDCIII', 'MMMDCIV', 'MMMDCV', 'MMMDCVI', 'MMMDCVII', 'MMMDCVIII', 'MMMDCIX', 'MMMDCX', 'MMMDCXI', 'MMMDCXII', 'MMMDCXIII', 'MMMDCXIV', 'MMMDCXV', 'MMMDCXVI', 'MMMDCXVII', 'MMMDCXVIII', 'MMMDCXIX', 'MMMDCXX', 'MMMDCXXI', 'MMMDCXXII', 'MMMDCXXIII', 'MMMDCXXIV', 'MMMDCXXV', 'MMMDCXXVI', 'MMMDCXXVII', 'MMMDCXXVIII', 'MMMDCXXIX', 'MMMDCXXX', 'MMMDCXXXI', 'MMMDCXXXII', 'MMMDCXXXIII', 'MMMDCXXXIV', 'MMMDCXXXV', 'MMMDCXXXVI', 'MMMDCXXXVII', 'MMMDCXXXVIII', 'MMMDCXXXIX', 'MMMDCXL', 'MMMDCXLI', 'MMMDCXLII', 'MMMDCXLIII', 'MMMDCXLIV', 'MMMDCVL', 'MMMDCVLI', 'MMMDCVLII', 'MMMDCVLIII', 'MMMDCIL', 'MMMDCL', 'MMMDCLI', 'MMMDCLII', 'MMMDCLIII', 'MMMDCLIV', 'MMMDCLV', 'MMMDCLVI', 'MMMDCLVII', 'MMMDCLVIII', 'MMMDCLIX', 'MMMDCLX', 'MMMDCLXI', 'MMMDCLXII', 'MMMDCLXIII', 'MMMDCLXIV', 'MMMDCLXV', 'MMMDCLXVI', 'MMMDCLXVII', 'MMMDCLXVIII', 'MMMDCLXIX', 'MMMDCLXX', 'MMMDCLXXI', 'MMMDCLXXII', 'MMMDCLXXIII', 'MMMDCLXXIV', 'MMMDCLXXV', 'MMMDCLXXVI', 'MMMDCLXXVII', 'MMMDCLXXVIII', 'MMMDCLXXIX', 'MMMDCLXXX', 'MMMDCLXXXI', 'MMMDCLXXXII', 'MMMDCLXXXIII', 'MMMDCLXXXIV', 'MMMDCLXXXV', 'MMMDCLXXXVI', 'MMMDCLXXXVII', 'MMMDCLXXXVIII', 'MMMDCLXXXIX', 'MMMDCXC', 'MMMDCXCI', 'MMMDCXCII', 'MMMDCXCIII', 'MMMDCXCIV', 'MMMDCVC', 'MMMDCVCI', 'MMMDCVCII', 'MMMDCVCIII', 'MMMDCIC', 'MMMDCC', 'MMMDCCI', 'MMMDCCII', 'MMMDCCIII', 'MMMDCCIV', 'MMMDCCV', 'MMMDCCVI', 'MMMDCCVII', 'MMMDCCVIII', 'MMMDCCIX', 'MMMDCCX', 'MMMDCCXI', 'MMMDCCXII', 'MMMDCCXIII', 'MMMDCCXIV', 'MMMDCCXV', 'MMMDCCXVI', 'MMMDCCXVII', 'MMMDCCXVIII', 'MMMDCCXIX', 'MMMDCCXX', 'MMMDCCXXI', 'MMMDCCXXII', 'MMMDCCXXIII', 'MMMDCCXXIV', 'MMMDCCXXV', 'MMMDCCXXVI', 'MMMDCCXXVII', 'MMMDCCXXVIII', 'MMMDCCXXIX', 'MMMDCCXXX', 'MMMDCCXXXI', 'MMMDCCXXXII', 'MMMDCCXXXIII', 'MMMDCCXXXIV', 'MMMDCCXXXV', 'MMMDCCXXXVI', 'MMMDCCXXXVII', 'MMMDCCXXXVIII', 'MMMDCCXXXIX', 'MMMDCCXL', 'MMMDCCXLI', 'MMMDCCXLII', 'MMMDCCXLIII', 'MMMDCCXLIV', 'MMMDCCVL', 'MMMDCCVLI', 'MMMDCCVLII', 'MMMDCCVLIII', 'MMMDCCIL', 'MMMDCCL', 'MMMDCCLI', 'MMMDCCLII', 'MMMDCCLIII', 'MMMDCCLIV', 'MMMDCCLV', 'MMMDCCLVI', 'MMMDCCLVII', 'MMMDCCLVIII', 'MMMDCCLIX', 'MMMDCCLX', 'MMMDCCLXI', 'MMMDCCLXII', 'MMMDCCLXIII', 'MMMDCCLXIV', 'MMMDCCLXV', 'MMMDCCLXVI', 'MMMDCCLXVII', 'MMMDCCLXVIII', 'MMMDCCLXIX', 'MMMDCCLXX', 'MMMDCCLXXI', 'MMMDCCLXXII', 'MMMDCCLXXIII', 'MMMDCCLXXIV', 'MMMDCCLXXV', 'MMMDCCLXXVI', 'MMMDCCLXXVII', 'MMMDCCLXXVIII', 'MMMDCCLXXIX', 'MMMDCCLXXX', 'MMMDCCLXXXI', 'MMMDCCLXXXII', 'MMMDCCLXXXIII', 'MMMDCCLXXXIV', 'MMMDCCLXXXV', 'MMMDCCLXXXVI', 'MMMDCCLXXXVII', 'MMMDCCLXXXVIII', 'MMMDCCLXXXIX', 'MMMDCCXC', 'MMMDCCXCI', 'MMMDCCXCII', 'MMMDCCXCIII', 'MMMDCCXCIV', 'MMMDCCVC', 'MMMDCCVCI', 'MMMDCCVCII', 'MMMDCCVCIII', 'MMMDCCIC', 'MMMDCCC', 'MMMDCCCI', 'MMMDCCCII', 'MMMDCCCIII', 'MMMDCCCIV', 'MMMDCCCV', 'MMMDCCCVI', 'MMMDCCCVII', 'MMMDCCCVIII', 'MMMDCCCIX', 'MMMDCCCX', 'MMMDCCCXI', 'MMMDCCCXII', 'MMMDCCCXIII', 'MMMDCCCXIV', 'MMMDCCCXV', 'MMMDCCCXVI', 'MMMDCCCXVII', 'MMMDCCCXVIII', 'MMMDCCCXIX', 'MMMDCCCXX', 'MMMDCCCXXI', 'MMMDCCCXXII', 'MMMDCCCXXIII', 'MMMDCCCXXIV', 'MMMDCCCXXV', 'MMMDCCCXXVI', 'MMMDCCCXXVII', 'MMMDCCCXXVIII', 'MMMDCCCXXIX', 'MMMDCCCXXX', 'MMMDCCCXXXI', 'MMMDCCCXXXII', 'MMMDCCCXXXIII', 'MMMDCCCXXXIV', 'MMMDCCCXXXV', 'MMMDCCCXXXVI', 'MMMDCCCXXXVII', 'MMMDCCCXXXVIII', 'MMMDCCCXXXIX', 'MMMDCCCXL', 'MMMDCCCXLI', 'MMMDCCCXLII', 'MMMDCCCXLIII', 'MMMDCCCXLIV', 'MMMDCCCVL', 'MMMDCCCVLI', 'MMMDCCCVLII', 'MMMDCCCVLIII', 'MMMDCCCIL', 'MMMDCCCL', 'MMMDCCCLI', 'MMMDCCCLII', 'MMMDCCCLIII', 'MMMDCCCLIV', 'MMMDCCCLV', 'MMMDCCCLVI', 'MMMDCCCLVII', 'MMMDCCCLVIII', 'MMMDCCCLIX', 'MMMDCCCLX', 'MMMDCCCLXI', 'MMMDCCCLXII', 'MMMDCCCLXIII', 'MMMDCCCLXIV', 'MMMDCCCLXV', 'MMMDCCCLXVI', 'MMMDCCCLXVII', 'MMMDCCCLXVIII', 'MMMDCCCLXIX', 'MMMDCCCLXX', 'MMMDCCCLXXI', 'MMMDCCCLXXII', 'MMMDCCCLXXIII', 'MMMDCCCLXXIV', 'MMMDCCCLXXV', 'MMMDCCCLXXVI', 'MMMDCCCLXXVII', 'MMMDCCCLXXVIII', 'MMMDCCCLXXIX', 'MMMDCCCLXXX', 'MMMDCCCLXXXI', 'MMMDCCCLXXXII', 'MMMDCCCLXXXIII', 'MMMDCCCLXXXIV', 'MMMDCCCLXXXV', 'MMMDCCCLXXXVI', 'MMMDCCCLXXXVII', 'MMMDCCCLXXXVIII', 'MMMDCCCLXXXIX', 'MMMDCCCXC', 'MMMDCCCXCI', 'MMMDCCCXCII', 'MMMDCCCXCIII', 'MMMDCCCXCIV', 'MMMDCCCVC', 'MMMDCCCVCI', 'MMMDCCCVCII', 'MMMDCCCVCIII', 'MMMDCCCIC', 'MMMCM', 'MMMCMI', 'MMMCMII', 'MMMCMIII', 'MMMCMIV', 'MMMCMV', 'MMMCMVI', 'MMMCMVII', 'MMMCMVIII', 'MMMCMIX', 'MMMCMX', 'MMMCMXI', 'MMMCMXII', 'MMMCMXIII', 'MMMCMXIV', 'MMMCMXV', 'MMMCMXVI', 'MMMCMXVII', 'MMMCMXVIII', 'MMMCMXIX', 'MMMCMXX', 'MMMCMXXI', 'MMMCMXXII', 'MMMCMXXIII', 'MMMCMXXIV', 'MMMCMXXV', 'MMMCMXXVI', 'MMMCMXXVII', 'MMMCMXXVIII', 'MMMCMXXIX', 'MMMCMXXX', 'MMMCMXXXI', 'MMMCMXXXII', 'MMMCMXXXIII', 'MMMCMXXXIV', 'MMMCMXXXV', 'MMMCMXXXVI', 'MMMCMXXXVII', 'MMMCMXXXVIII', 'MMMCMXXXIX', 'MMMCMXL', 'MMMCMXLI', 'MMMCMXLII', 'MMMCMXLIII', 'MMMCMXLIV', 'MMMCMVL', 'MMMCMVLI', 'MMMCMVLII', 'MMMCMVLIII', 'MMMCMIL', 'MMMLM', 'MMMLMI', 'MMMLMII', 'MMMLMIII', 'MMMLMIV', 'MMMLMV', 'MMMLMVI', 'MMMLMVII', 'MMMLMVIII', 'MMMLMIX', 'MMMLMX', 'MMMLMXI', 'MMMLMXII', 'MMMLMXIII', 'MMMLMXIV', 'MMMLMXV', 'MMMLMXVI', 'MMMLMXVII', 'MMMLMXVIII', 'MMMLMXIX', 'MMMLMXX', 'MMMLMXXI', 'MMMLMXXII', 'MMMLMXXIII', 'MMMLMXXIV', 'MMMLMXXV', 'MMMLMXXVI', 'MMMLMXXVII', 'MMMLMXXVIII', 'MMMLMXXIX', 'MMMLMXXX', 'MMMLMXXXI', 'MMMLMXXXII', 'MMMLMXXXIII', 'MMMLMXXXIV', 'MMMLMXXXV', 'MMMLMXXXVI', 'MMMLMXXXVII', 'MMMLMXXXVIII', 'MMMLMXXXIX', 'MMMXM', 'MMMXMI', 'MMMXMII', 'MMMXMIII', 'MMMXMIV', 'MMMVM', 'MMMVMI', 'MMMVMII', 'MMMVMIII', 'MMMIM', ] diff --git a/test/unit/interpreter/function-sum.spec.ts b/test/unit/interpreter/function-sum.spec.ts index 939c499b2..5435b42a5 100644 --- a/test/unit/interpreter/function-sum.spec.ts +++ b/test/unit/interpreter/function-sum.spec.ts @@ -184,7 +184,7 @@ describe('SUM', () => { expect(engine.getCellValue(adr('A4'))).toEqual(9) }) - it('when 1st row address is absolute and one of the ranges is a single value', () => { + it('when 1st row address is absolute and one of the ranges is a single value (2)', () => { const engine = HyperFormula.buildFromArray([ ['=SUM(B$2:B1)', 1], // R1C[+1]:R[+0]C[+1] ['=SUM(B$2:B2)', 2], // R1C[+1]:R[+0]C[+1] @@ -194,7 +194,7 @@ describe('SUM', () => { expect(engine.getCellValue(adr('A2'))).toEqual(2) }) - it('when 2nd row address is absolute and one of the ranges is a single value', () => { + it('when 2nd row address is absolute and one of the ranges is a single value (2)', () => { const engine = HyperFormula.buildFromArray([ ['=SUM(B1:B$1)', 1], // R[+0]C[+1]:R0C[+1] ['=SUM(B2:B$1)', 2], // R[+0]C[+1]:R0C[+1] diff --git a/test/unit/interpreter/function-sumproduct.spec.ts b/test/unit/interpreter/function-sumproduct.spec.ts index cef1aaffa..08dfe4d43 100644 --- a/test/unit/interpreter/function-sumproduct.spec.ts +++ b/test/unit/interpreter/function-sumproduct.spec.ts @@ -85,7 +85,7 @@ describe('Function SUMPRODUCT', () => { expect(engine.getCellValue(adr('A4'))).toEqual(5) }) - it('it makes a coercion from other values', () => { + it('makes a coercion from other values', () => { const engine = HyperFormula.buildFromArray([ ['=TRUE()', '42'], ['=SUMPRODUCT(A1,B1)'], diff --git a/test/unit/interpreter/function-version.spec.ts b/test/unit/interpreter/function-version.spec.ts index 0e0a60713..754eb4b57 100644 --- a/test/unit/interpreter/function-version.spec.ts +++ b/test/unit/interpreter/function-version.spec.ts @@ -81,7 +81,7 @@ describe('Function VERSION', () => { }).toThrow(ProtectedFunctionError.cannotRegisterFunctionWithId('VERSION')) }) - it('should be available even if anyone unregistered ', () => { + it('should be available even if anyone unregistered', () => { expect(() => { HyperFormula.unregisterFunction('VERSION') }).toThrow(ProtectedFunctionError.cannotUnregisterFunctionWithId('VERSION')) diff --git a/test/unit/interpreter/function-vlookup.spec.ts b/test/unit/interpreter/function-vlookup.spec.ts index b667f2d91..23b72e30a 100644 --- a/test/unit/interpreter/function-vlookup.spec.ts +++ b/test/unit/interpreter/function-vlookup.spec.ts @@ -203,19 +203,6 @@ describe('ColumnIndex strategy', () => { expect(engine.getCellValue(adr('A6'))).toEqual('d') }) - it('should find value in unsorted range using linearSearch', () => { - const engine = HyperFormula.buildFromArray([ - ['5', 'a'], - ['4', 'b'], - ['3', 'c'], - ['2', 'd'], - ['1', 'e'], - ['=VLOOKUP(2, A1:B5, 2, FALSE())'], - ], { useColumnIndex: true }) - - expect(engine.getCellValue(adr('A6'))).toEqual('d') - }) - it('should find value in sorted range with different types', () => { const engine = HyperFormula.buildFromArray([ ['1', 'a'], @@ -566,19 +553,6 @@ describe('BinarySearchStrategy', () => { expect(engine.getCellValue(adr('A6'))).toEqual('d') }) - it('should find value in unsorted range using linearSearch', () => { - const engine = HyperFormula.buildFromArray([ - ['5', 'a'], - ['4', 'b'], - ['3', 'c'], - ['2', 'd'], - ['1', 'e'], - ['=VLOOKUP(2, A1:B5, 2, FALSE())'], - ], { useColumnIndex: false }) - - expect(engine.getCellValue(adr('A6'))).toEqual('d') - }) - it('should find value in sorted range with different types', () => { const engine = HyperFormula.buildFromArray([ ['1', 'a'], diff --git a/test/unit/interpreter/function-xlookup.spec.ts b/test/unit/interpreter/function-xlookup.spec.ts index 6cdbbbc0a..fc3f4fe1b 100644 --- a/test/unit/interpreter/function-xlookup.spec.ts +++ b/test/unit/interpreter/function-xlookup.spec.ts @@ -232,7 +232,7 @@ describe('Function XLOOKUP', () => { }) }) - describe('with BinarySearch column search strategy, when provided with searchMode = ', () => { + describe('with BinarySearch column search strategy, when provided with searchMode =', () => { it('1, finds the first match in unsorted horizontal range', () => { const engine = HyperFormula.buildFromArray([ ['=XLOOKUP(1, A2:E2, A3:E3, "NotFound", 0, 1)'], @@ -376,7 +376,7 @@ describe('Function XLOOKUP', () => { }) }) - describe('with ColumnIndex column search strategy, when provided with searchMode = ', () => { + describe('with ColumnIndex column search strategy, when provided with searchMode =', () => { it('1, finds the first match in unsorted horizontal range', () => { const engine = HyperFormula.buildFromArray([ ['=XLOOKUP(1, A2:E2, A3:E3, "NotFound", 0, 1)'], @@ -520,7 +520,7 @@ describe('Function XLOOKUP', () => { }) }) - describe('with BinarySearch column search strategy, when provided with matchMode = ', () => { + describe('with BinarySearch column search strategy, when provided with matchMode =', () => { describe('-1 (looking for a lower bound)', () => { describe('in array ordered ascending', () => { it('returns exact match if exists', () => { @@ -852,7 +852,7 @@ describe('Function XLOOKUP', () => { }) }) - describe('with ColumnIndex column search strategy, when provided with matchMode = ', () => { + describe('with ColumnIndex column search strategy, when provided with matchMode =', () => { describe('-1 (looking for a lower bound', () => { describe('in array ordered ascending', () => { it('returns exact match if exists', () => { diff --git a/test/unit/matchers.spec.ts b/test/unit/matchers.spec.ts index 34fa6ac25..6e3cdd8d3 100644 --- a/test/unit/matchers.spec.ts +++ b/test/unit/matchers.spec.ts @@ -1,7 +1,7 @@ import {DetailedCellError} from '../../src' import {ArraySize} from '../../src/ArraySize' import {CellError, ErrorType} from '../../src/Cell' -import {FormulaVertex} from '../../src/DependencyGraph/FormulaCellVertex' +import {FormulaVertex} from '../../src/DependencyGraph/FormulaVertex' import {buildNumberAst} from '../../src/parser/Ast' import {adr} from './testUtils' diff --git a/test/unit/matrix-mapping.spec.ts b/test/unit/matrix-mapping.spec.ts index f53a341d4..d03f5d670 100644 --- a/test/unit/matrix-mapping.spec.ts +++ b/test/unit/matrix-mapping.spec.ts @@ -1,6 +1,6 @@ import {AbsoluteCellRange} from '../../src/AbsoluteCellRange' import {ArraySize} from '../../src/ArraySize' -import {ArrayMapping, ArrayVertex} from '../../src/DependencyGraph' +import {ArrayMapping, ArrayFormulaVertex} from '../../src/DependencyGraph' import {buildNumberAst} from '../../src/parser/Ast' import {adr} from './testUtils' @@ -13,7 +13,7 @@ describe('MatrixMapping', () => { it('should set matrix', () => { const matrixMapping = new ArrayMapping() - const vertex = new ArrayVertex(buildNumberAst(1), adr('A1'), new ArraySize(2, 2)) + const vertex = new ArrayFormulaVertex(buildNumberAst(1), adr('A1'), new ArraySize(2, 2)) const range = AbsoluteCellRange.spanFrom(adr('A1'), 2, 2) matrixMapping.setArray(range, vertex) @@ -23,7 +23,7 @@ describe('MatrixMapping', () => { it('should answer some questions', () => { const matrixMapping = new ArrayMapping() - const vertex = new ArrayVertex(buildNumberAst(1), adr('B2'), new ArraySize(2, 2)) + const vertex = new ArrayFormulaVertex(buildNumberAst(1), adr('B2'), new ArraySize(2, 2)) const range = AbsoluteCellRange.spanFrom(adr('B2'), 2, 2) matrixMapping.setArray(range, vertex) @@ -42,9 +42,9 @@ describe('MatrixMapping', () => { it('should move matrices below row', () => { const matrixMapping = new ArrayMapping() - const vertex1 = new ArrayVertex(buildNumberAst(1), adr('B1'), new ArraySize(2, 2)) + const vertex1 = new ArrayFormulaVertex(buildNumberAst(1), adr('B1'), new ArraySize(2, 2)) const range1 = AbsoluteCellRange.spanFrom(adr('B1'), 2, 2) - const vertex2 = new ArrayVertex(buildNumberAst(1), adr('D2'), new ArraySize(2, 2)) + const vertex2 = new ArrayFormulaVertex(buildNumberAst(1), adr('D2'), new ArraySize(2, 2)) const range2 = AbsoluteCellRange.spanFrom(adr('D2'), 2, 2) matrixMapping.setArray(range1, vertex1) matrixMapping.setArray(range2, vertex2) diff --git a/test/unit/matrix.spec.ts b/test/unit/matrix.spec.ts index f569c1343..394656765 100644 --- a/test/unit/matrix.spec.ts +++ b/test/unit/matrix.spec.ts @@ -229,15 +229,15 @@ describe('Matrix', () => { [4, 5, 6], [7, 8, 9], ]) - expect(matrix.set(0, 0, 11)) - expect(matrix.set(1, 0, 12)) - expect(matrix.set(2, 0, 13)) - expect(matrix.set(0, 1, 14)) - expect(matrix.set(1, 1, 15)) - expect(matrix.set(2, 1, 16)) - expect(matrix.set(0, 2, 17)) - expect(matrix.set(1, 2, 18)) - expect(matrix.set(2, 2, 19)) + matrix.set(0, 0, 11) + matrix.set(1, 0, 12) + matrix.set(2, 0, 13) + matrix.set(0, 1, 14) + matrix.set(1, 1, 15) + matrix.set(2, 1, 16) + matrix.set(0, 2, 17) + matrix.set(1, 2, 18) + matrix.set(2, 2, 19) expect(matrix.raw()).toEqual([ [11, 12, 13], [14, 15, 16], diff --git a/test/unit/named-expressions.spec.ts b/test/unit/named-expressions.spec.ts index d3a88d601..774a8a372 100644 --- a/test/unit/named-expressions.spec.ts +++ b/test/unit/named-expressions.spec.ts @@ -1168,15 +1168,16 @@ describe('Named expressions - evaluation', () => { expect(engine.graph.adjacentNodes(fooVertex).size).toBe(0) }) - it('named expressions are transformed during CRUDs', () => { + it('named expression returns REF error after removing referenced sheet', () => { const engine = HyperFormula.buildFromArray([ ['=42'] ]) engine.addNamedExpression('FOO', '=Sheet1!$A$1 + 10') - engine.removeSheet(0) + engine.removeSheet(engine.getSheetId('Sheet1')!) - expect(engine.getNamedExpressionFormula('FOO')).toEqual('=#REF! + 10') + expect(engine.getNamedExpressionFormula('FOO')).toEqual('=Sheet1!$A$1 + 10') + expect(engine.getNamedExpressionValue('FOO')).toEqualError(detailedError(ErrorType.REF)) }) it('local named expression shadows global one', () => { @@ -1323,14 +1324,15 @@ describe('Named expressions - evaluation', () => { expect(engine.getCellValue(adr('A1'))).toEqual(52) }) - it('named expressions are transformed during CRUDs', () => { + it('named expression returns REF error after removing referenced sheet', () => { const engine = HyperFormula.buildFromArray([ ['=42'] ], {}, [{ name: 'FOO', expression: '=Sheet1!$A$1 + 10' }]) engine.removeSheet(0) - expect(engine.getNamedExpressionFormula('FOO')).toEqual('=#REF! + 10') + expect(engine.getNamedExpressionFormula('FOO')).toEqual('=Sheet1!$A$1 + 10') + expect(engine.getNamedExpressionValue('FOO')).toEqualError(detailedError(ErrorType.REF)) }) it('local named expression shadows global one', () => { @@ -1627,6 +1629,34 @@ describe('Named expressions - named ranges', () => { expect(changes).toContainEqual(new ExportedNamedExpressionChange('fooo', [[1, 3], [2, 4]])) }) + + it('should update automatically when row is added to the referenced range', () => { + const engine = HyperFormula.buildFromArray([ + [1, 2], + [3, 4], + ]) + + engine.addNamedExpression('range', '=Sheet1!$A$1:$B$2') + expect(engine.getNamedExpressionFormula('range')).toEqual('=Sheet1!$A$1:$B$2') + + engine.addRows(0, [1, 1]) + expect(engine.getSheetValues(0)).toEqual([[1, 2], [], [3, 4]]) + expect(engine.getNamedExpressionFormula('range')).toEqual('=Sheet1!$A$1:$B$3') + }) + + it('should update automatically when column is added to the referenced range', () => { + const engine = HyperFormula.buildFromArray([ + [1, 2], + [3, 4], + ]) + + engine.addNamedExpression('range', '=Sheet1!$A$1:$B$2') + expect(engine.getNamedExpressionFormula('range')).toEqual('=Sheet1!$A$1:$B$2') + + engine.addColumns(0, [1, 1]) + expect(engine.getSheetValues(0)).toEqual([[1, null, 2], [3, null, 4]]) + expect(engine.getNamedExpressionFormula('range')).toEqual('=Sheet1!$A$1:$C$2') + }) }) describe('when created on initialization', () => { @@ -1651,6 +1681,30 @@ describe('Named expressions - named ranges', () => { expect(engine.getCellValue(adr('B1'))).toEqual(4) expect(engine.getCellValue(adr('C1'))).toEqual(4) }) + + it('should update automatically when row is added to the referenced range', () => { + const engine = HyperFormula.buildFromArray([ + [1, 2], + [3, 4], + ], {}, [{ name: 'range', expression: '=Sheet1!$A$1:$B$2' }]) + expect(engine.getNamedExpressionFormula('range')).toEqual('=Sheet1!$A$1:$B$2') + + engine.addRows(0, [1, 1]) + expect(engine.getSheetValues(0)).toEqual([[1, 2], [], [3, 4]]) + expect(engine.getNamedExpressionFormula('range')).toEqual('=Sheet1!$A$1:$B$3') + }) + + it('should update automatically when column is added to the referenced range', () => { + const engine = HyperFormula.buildFromArray([ + [1, 2], + [3, 4], + ], {}, [{ name: 'range', expression: '=Sheet1!$A$1:$B$2' }]) + expect(engine.getNamedExpressionFormula('range')).toEqual('=Sheet1!$A$1:$B$2') + + engine.addColumns(0, [1, 1]) + expect(engine.getSheetValues(0)).toEqual([[1, null, 2], [3, null, 4]]) + expect(engine.getNamedExpressionFormula('range')).toEqual('=Sheet1!$A$1:$C$2') + }) }) }) @@ -1859,11 +1913,14 @@ describe('Named expressions - actions at the Operations layer', () => { const lazilyTransformingAstService = new LazilyTransformingAstService(stats) const dependencyGraph = DependencyGraph.buildEmpty(lazilyTransformingAstService, config, functionRegistry, namedExpressions, stats) const columnSearch = buildColumnSearchStrategy(dependencyGraph, config, stats) - const sheetMapping = dependencyGraph.sheetMapping const dateTimeHelper = new DateTimeHelper(config) const numberLiteralHelper = new NumberLiteralHelper(config) const cellContentParser = new CellContentParser(config, dateTimeHelper, numberLiteralHelper) - const parser = new ParserWithCaching(config, functionRegistry, sheetMapping.get) + const parser = new ParserWithCaching( + config, + functionRegistry, + dependencyGraph.sheetReferenceRegistrar.ensureSheetRegistered.bind(dependencyGraph.sheetReferenceRegistrar) + ) const arraySizePredictor = new ArraySizePredictor(config, functionRegistry) operations = new Operations(config, dependencyGraph, columnSearch, cellContentParser, parser, stats, lazilyTransformingAstService, namedExpressions, arraySizePredictor) }) diff --git a/test/unit/parser/cell-address-from-string.spec.ts b/test/unit/parser/cell-address-from-string.spec.ts index dc9602613..7a19e8f3e 100644 --- a/test/unit/parser/cell-address-from-string.spec.ts +++ b/test/unit/parser/cell-address-from-string.spec.ts @@ -1,46 +1,62 @@ -import {SheetMapping} from '../../../src/DependencyGraph' +import { AlwaysDense } from '../../../src' +import {AddressMapping, SheetMapping, SheetReferenceRegistrar} from '../../../src/DependencyGraph' import {buildTranslationPackage} from '../../../src/i18n' import {enGB} from '../../../src/i18n/languages' import {CellAddress, cellAddressFromString} from '../../../src/parser' import {adr} from '../testUtils' describe('cellAddressFromString', () => { + let sheetMapping: SheetMapping + let addressMapping: AddressMapping + let resolveSheetReference: (sheetName: string) => number + beforeEach(() => { + sheetMapping = new SheetMapping(buildTranslationPackage(enGB)) + addressMapping = new AddressMapping(new AlwaysDense()) + const registrar = new SheetReferenceRegistrar(sheetMapping, addressMapping) + resolveSheetReference = registrar.ensureSheetRegistered.bind(registrar) + }) + it('is zero based', () => { - expect(cellAddressFromString(new SheetMapping(buildTranslationPackage(enGB)).get, 'A1', adr('A1'))).toEqual(CellAddress.relative(0, 0)) + expect(cellAddressFromString('A1', adr('A1'), resolveSheetReference)).toEqual(CellAddress.relative(0, 0)) }) it('works for bigger rows', () => { - expect(cellAddressFromString(new SheetMapping(buildTranslationPackage(enGB)).get, 'A123', adr('A1'))).toEqual(CellAddress.relative(0, 122)) + expect(cellAddressFromString('A123', adr('A1'), resolveSheetReference)).toEqual(CellAddress.relative(0, 122)) }) it('one letter', () => { - expect(cellAddressFromString(new SheetMapping(buildTranslationPackage(enGB)).get, 'Z1', adr('A1'))).toEqual(CellAddress.relative(25, 0)) + expect(cellAddressFromString('Z1', adr('A1'), resolveSheetReference)).toEqual(CellAddress.relative(25, 0)) }) it('last letter is Z', () => { - expect(cellAddressFromString(new SheetMapping(buildTranslationPackage(enGB)).get, 'AA1', adr('A1'))).toEqual(CellAddress.relative(26, 0)) + expect(cellAddressFromString('AA1', adr('A1'), resolveSheetReference)).toEqual(CellAddress.relative(26, 0)) }) it('works for many letters', () => { - expect(cellAddressFromString(new SheetMapping(buildTranslationPackage(enGB)).get, 'ABC1', adr('A1'))).toEqual(CellAddress.relative(730, 0)) + expect(cellAddressFromString('ABC1', adr('A1'), resolveSheetReference)).toEqual(CellAddress.relative(730, 0)) }) it('is not case sensitive', () => { - expect(cellAddressFromString(new SheetMapping(buildTranslationPackage(enGB)).get, 'abc1', adr('A1'))).toEqual(CellAddress.relative(730, 0)) + expect(cellAddressFromString('abc1', adr('A1'), resolveSheetReference)).toEqual(CellAddress.relative(730, 0)) }) it('when sheet is missing, its took from base address', () => { - expect(cellAddressFromString(new SheetMapping(buildTranslationPackage(enGB)).get, 'B3', adr('A1', 42))).toEqual(CellAddress.relative(1, 2)) + expect(cellAddressFromString('B3', adr('A1', 42), resolveSheetReference)).toEqual(CellAddress.relative(1, 2)) }) it('can into sheets', () => { - const sheetMapping = new SheetMapping(buildTranslationPackage(enGB)) const sheet1 = sheetMapping.addSheet('Sheet1') const sheet2 = sheetMapping.addSheet('Sheet2') const sheet3 = sheetMapping.addSheet('~`!@#$%^&*()_-+_=/|?{}[]\"') - expect(cellAddressFromString(sheetMapping.get, 'Sheet1!B3', adr('A1', sheet1))).toEqual(CellAddress.relative(1, 2, sheet1)) - expect(cellAddressFromString(sheetMapping.get, 'Sheet2!B3', adr('A1', sheet1))).toEqual(CellAddress.relative(1, 2, sheet2)) - expect(cellAddressFromString(sheetMapping.get, "'~`!@#$%^&*()_-+_=/|?{}[]\"'!B3", adr('A1', sheet1))).toEqual(CellAddress.relative(1, 2, sheet3)) + expect(cellAddressFromString('Sheet1!B3', adr('A1', sheet1), resolveSheetReference)).toEqual(CellAddress.relative(1, 2, sheet1)) + expect(cellAddressFromString('Sheet2!B3', adr('A1', sheet1), resolveSheetReference)).toEqual(CellAddress.relative(1, 2, sheet2)) + expect(cellAddressFromString("'~`!@#$%^&*()_-+_=/|?{}[]\"'!B3", adr('A1', sheet1), resolveSheetReference)).toEqual(CellAddress.relative(1, 2, sheet3)) + }) + + it('returns undefined when sheet resolver fails', () => { + const failingResolver = () => undefined + + expect(cellAddressFromString('Ghost!A1', adr('A1'), failingResolver)).toBeUndefined() }) }) diff --git a/test/unit/parser/common.ts b/test/unit/parser/common.ts index 8ec2fd11d..86e66780c 100644 --- a/test/unit/parser/common.ts +++ b/test/unit/parser/common.ts @@ -1,11 +1,18 @@ +import { AlwaysDense } from '../../../src' import {Config} from '../../../src/Config' -import {SheetMapping} from '../../../src/DependencyGraph' +import {AddressMapping, SheetMapping, SheetReferenceRegistrar} from '../../../src/DependencyGraph' import {buildTranslationPackage} from '../../../src/i18n' import {enGB} from '../../../src/i18n/languages' import {FunctionRegistry} from '../../../src/interpreter/FunctionRegistry' import {ParserWithCaching} from '../../../src/parser' -export function buildEmptyParserWithCaching(config: Config, sheetMapping?: SheetMapping): ParserWithCaching { +export function buildEmptyParserWithCaching(config: Config, sheetMapping?: SheetMapping, addressMapping?: AddressMapping): ParserWithCaching { sheetMapping = sheetMapping || new SheetMapping(buildTranslationPackage(enGB)) - return new ParserWithCaching(config, new FunctionRegistry(config), sheetMapping.get) + addressMapping = addressMapping || new AddressMapping(new AlwaysDense()) + const registrar = new SheetReferenceRegistrar(sheetMapping, addressMapping) + return new ParserWithCaching( + config, + new FunctionRegistry(config), + registrar.ensureSheetRegistered.bind(registrar) + ) } diff --git a/test/unit/parser/compute-hash-from-tokens.spec.ts b/test/unit/parser/compute-hash-from-tokens.spec.ts index 6bdc44eae..1407f0db2 100644 --- a/test/unit/parser/compute-hash-from-tokens.spec.ts +++ b/test/unit/parser/compute-hash-from-tokens.spec.ts @@ -80,7 +80,7 @@ describe('computeHashFromTokens', () => { it('cell ref to not exsiting sheet', () => { const code = '=Sheet3!A1' - expect(computeFunc(code, adr('B2'))).toEqual('=Sheet3!A1') + expect(computeFunc(code, adr('B2'))).toBe('=#2#-1R-1') }) it('cell range', () => { @@ -182,7 +182,7 @@ describe('computeHashFromTokens', () => { expect(hash).toEqual('= - 1 + 2 / 3 - 4 % * (1 + 2 ) + SUM( #0#0R0, #0R0:#1R0 )') }) - it('should skip whitespaces inside range ', () => { + it('should skip whitespaces inside range', () => { const formula = '=SUM( A1 : A2 )' const hash = computeFunc(formula, adr('A1')) expect(hash).toEqual('=SUM( #0R0:#1R0 )') diff --git a/test/unit/parser/offset-translation.spec.ts b/test/unit/parser/offset-translation.spec.ts index b16e4645f..a75c6bef8 100644 --- a/test/unit/parser/offset-translation.spec.ts +++ b/test/unit/parser/offset-translation.spec.ts @@ -68,6 +68,18 @@ describe('Parser - OFFSET to reference translation', () => { expect(ast.end).toEqual(CellAddress.relative(8, 13)) }) + it('OFFSET parsing into cell range in different sheet', () => { + const sheetMapping = new SheetMapping(buildTranslationPackage(enUS)) + sheetMapping.addSheet('Sheet1') + sheetMapping.addSheet('Sheet2') + const parser = buildEmptyParserWithCaching(new Config(), sheetMapping) + + const ast = parser.parse('=OFFSET(Sheet2!F16, 0, 2, 1, 3)', adr('B3', 0)).ast as CellRangeAst + expect(ast.type).toBe(AstNodeType.CELL_RANGE) + expect(ast.start).toEqual(CellAddress.relative(6, 13, 1)) + expect(ast.end).toEqual(CellAddress.relative(8, 13, 1)) + }) + it('OFFSET first argument need to be reference', () => { const parser = buildEmptyParserWithCaching(new Config()) @@ -186,9 +198,9 @@ describe('Parser - OFFSET to reference translation', () => { sheetMapping.addSheet('Sheet2') const parser = buildEmptyParserWithCaching(new Config(), sheetMapping) - const ast = parser.parse('=OFFSET(Sheet1!A1, 0, 0)', adr('A1', 1)).ast as CellReferenceAst + const ast = parser.parse('=OFFSET(Sheet2!A1, 0, 0)', adr('A1', 0)).ast as CellReferenceAst expect(ast.type).toBe(AstNodeType.CELL_REFERENCE) - expect(ast.reference).toEqual(CellAddress.relative(0, 0, 0)) + expect(ast.reference).toEqual(CellAddress.relative(0, 0, 1)) }) it('function OFFSET can reference a different sheet', () => { diff --git a/test/unit/parser/parser-caching.spec.ts b/test/unit/parser/parser-caching.spec.ts index 5d1fa37b2..40310cd49 100644 --- a/test/unit/parser/parser-caching.spec.ts +++ b/test/unit/parser/parser-caching.spec.ts @@ -3,7 +3,7 @@ import {adr} from '../testUtils' import {buildEmptyParserWithCaching} from './common' describe('ParserWithCaching - caching', () => { - it('it use cache for similar formulas', () => { + it('use cache for similar formulas', () => { const parser = buildEmptyParserWithCaching(new Config()) const ast1 = parser.parse('=A1', adr('A1')).ast diff --git a/test/unit/parser/parser.spec.ts b/test/unit/parser/parser.spec.ts index 6931b5a19..c49494d0e 100644 --- a/test/unit/parser/parser.spec.ts +++ b/test/unit/parser/parser.spec.ts @@ -24,6 +24,7 @@ import { import {columnIndexToLabel} from '../../../src/parser/addressRepresentationConverters' import { ArrayAst, + buildCellRangeAst, buildCellReferenceAst, buildColumnRangeAst, buildErrorWithRawInputAst, @@ -197,14 +198,13 @@ describe('ParserWithCaching', () => { expect(ast).toEqual(buildCellErrorAst(new CellError(ErrorType.REF))) }) - it('reference to address in nonexisting range returns ref error with data input ast', () => { + it('reference to address in nonexisting sheet returns cell reference ast', () => { const sheetMapping = new SheetMapping(buildTranslationPackage(enGB)) sheetMapping.addSheet('Sheet1') const parser = buildEmptyParserWithCaching(new Config(), sheetMapping) - const ast = parser.parse('=Sheet2!A1', adr('A1')).ast - expect(ast).toEqual(buildErrorWithRawInputAst('Sheet2!A1', new CellError(ErrorType.REF))) + expect(ast).toEqual(buildCellReferenceAst(CellAddress.relative(0, 0, 1))) }) }) @@ -373,16 +373,16 @@ describe('cell references and ranges', () => { const sheetName = 'Sheet3' expect(() => { - sheetMapping.fetch('Sheet3') + sheetMapping.getSheetIdOrThrowError('Sheet3') }).toThrow(new NoSheetWithNameError(sheetName)) }) - it('using unknown sheet gives REF', () => { + it('using unknown sheet gives cell reference ast', () => { const parser = buildEmptyParserWithCaching(new Config()) const ast = parser.parse('=Sheet2!A1', adr('A1')).ast - expect(ast).toEqual(buildErrorWithRawInputAst('Sheet2!A1', new CellError(ErrorType.REF))) + expect(ast).toEqual(buildCellReferenceAst(CellAddress.relative(0, 0, 0))) }) it('sheet name with other characters', () => { @@ -517,7 +517,7 @@ describe('cell references and ranges', () => { expect(ast.reference.sheet).toBe(undefined) }) - it('cell range with nonexsiting start sheet should return REF error with data input', () => { + it('cell range with nonexsiting start sheet should return cell range ast', () => { const sheetMapping = new SheetMapping(buildTranslationPackage(enGB)) sheetMapping.addSheet('Sheet1') sheetMapping.addSheet('Sheet2') @@ -525,10 +525,10 @@ describe('cell references and ranges', () => { const ast = parser.parse('=Sheet3!A1:Sheet2!B2', adr('A1')).ast - expect(ast).toEqual(buildErrorWithRawInputAst('Sheet3!A1:Sheet2!B2', new CellError(ErrorType.REF))) + expect(ast).toEqual(buildCellRangeAst(CellAddress.relative(0, 0, 1), CellAddress.relative(1, 1, 2), RangeSheetReferenceType.BOTH_ABSOLUTE)) }) - it('cell range with nonexsiting end sheet should return REF error with data input', () => { + it('cell range with nonexsiting end sheet should return cell range ast', () => { const sheetMapping = new SheetMapping(buildTranslationPackage(enGB)) sheetMapping.addSheet('Sheet1') sheetMapping.addSheet('Sheet2') @@ -536,7 +536,7 @@ describe('cell references and ranges', () => { const ast = parser.parse('=Sheet2!A1:Sheet3!B2', adr('A1')).ast - expect(ast).toEqual(buildErrorWithRawInputAst('Sheet2!A1:Sheet3!B2', new CellError(ErrorType.REF))) + expect(ast).toEqual(buildCellRangeAst(CellAddress.relative(0, 0, 1), CellAddress.relative(1, 1, 2), RangeSheetReferenceType.BOTH_ABSOLUTE)) }) it('cell reference beyond maximum row limit is #NAME', () => { diff --git a/test/unit/parser/unparse.spec.ts b/test/unit/parser/unparse.spec.ts index 66a29193e..60ec67736 100644 --- a/test/unit/parser/unparse.spec.ts +++ b/test/unit/parser/unparse.spec.ts @@ -4,13 +4,12 @@ import {SheetMapping} from '../../../src/DependencyGraph' import {buildTranslationPackage} from '../../../src/i18n' import {enGB, plPL} from '../../../src/i18n/languages' import {NamedExpressions} from '../../../src/NamedExpressions' -import {AstNodeType, buildLexerConfig, Unparser} from '../../../src/parser' +import {AstNodeType, Unparser} from '../../../src/parser' import {adr, unregisterAllLanguages} from '../testUtils' import {buildEmptyParserWithCaching} from './common' describe('Unparse', () => { - const config = new Config() - const lexerConfig = buildLexerConfig(config) + const config = new Config({ maxRows: 10 }) const sheetMapping = new SheetMapping(buildTranslationPackage(enGB)) sheetMapping.addSheet('Sheet1') sheetMapping.addSheet('Sheet2') @@ -18,7 +17,7 @@ describe('Unparse', () => { sheetMapping.addSheet("Sheet'With'Quotes") const parser = buildEmptyParserWithCaching(config, sheetMapping) const namedExpressions = new NamedExpressions() - const unparser = new Unparser(config, lexerConfig, sheetMapping.fetchDisplayName, namedExpressions) + const unparser = new Unparser(config, sheetMapping, namedExpressions) beforeEach(() => { unregisterAllLanguages() @@ -130,18 +129,20 @@ describe('Unparse', () => { }) it('#unparse error with data input', () => { - const formula = '=NotExistingSheet!A1' - const ast = parser.parse(formula, adr('A1')).ast - const unparsed = unparser.unparse(ast, adr('A1')) + const cellReferenceExceedingMaxRowsLimit = '=A100' + const ast = parser.parse(cellReferenceExceedingMaxRowsLimit, adr('A1')).ast expect(ast.type).toEqual(AstNodeType.ERROR_WITH_RAW_INPUT) - expect(unparsed).toEqual('=NotExistingSheet!A1') + + const unparsed = unparser.unparse(ast, adr('A1')) + + expect(unparsed).toEqual('=A100') }) it('#unparse with known error with translation', () => { const config = new Config({language: 'plPL'}) const parser = buildEmptyParserWithCaching(config, sheetMapping) - const unparser = new Unparser(config, buildLexerConfig(config), sheetMapping.fetchDisplayName, new NamedExpressions()) + const unparser = new Unparser(config, sheetMapping, new NamedExpressions()) const formula = '=#ADR!' const ast = parser.parse(formula, adr('A1')).ast const unparsed = unparser.unparse(ast, adr('A1')) @@ -179,7 +180,7 @@ describe('Unparse', () => { it('#unparse named expression returns original form', () => { const namedExpressions = new NamedExpressions() namedExpressions.addNamedExpression('SomeWEIRD_name', undefined) - const unparser = new Unparser(config, lexerConfig, sheetMapping.fetchDisplayName, namedExpressions) + const unparser = new Unparser(config, sheetMapping, namedExpressions) const formula = '=someWeird_Name' const ast = parser.parse(formula, adr('A1')).ast @@ -192,7 +193,7 @@ describe('Unparse', () => { const namedExpressions = new NamedExpressions() namedExpressions.addNamedExpression('SomeWEIRD_name', undefined) namedExpressions.addNamedExpression('SomeWEIRD_NAME', 0) - const unparser = new Unparser(config, lexerConfig, sheetMapping.fetchDisplayName, namedExpressions) + const unparser = new Unparser(config, sheetMapping, namedExpressions) const formula = '=someWeird_Name' const ast = parser.parse(formula, adr('A1')).ast @@ -203,7 +204,7 @@ describe('Unparse', () => { it('#unparse nonexisting named expression returns original input', () => { const namedExpressions = new NamedExpressions() - const unparser = new Unparser(config, lexerConfig, sheetMapping.fetchDisplayName, namedExpressions) + const unparser = new Unparser(config, sheetMapping, namedExpressions) const formula = '=someWeird_Name' const ast = parser.parse(formula, adr('A1')).ast @@ -216,7 +217,7 @@ describe('Unparse', () => { const namedExpressions = new NamedExpressions() namedExpressions.addNamedExpression('SomeWEIRD_name', undefined) namedExpressions.remove('SomeWEIRD_name', undefined) - const unparser = new Unparser(config, lexerConfig, sheetMapping.fetchDisplayName, namedExpressions) + const unparser = new Unparser(config, sheetMapping, namedExpressions) const formula = '=someWeird_Name' const ast = parser.parse(formula, adr('A1')).ast @@ -347,8 +348,8 @@ describe('Unparse', () => { const parser = buildEmptyParserWithCaching(configPL, sheetMapping) - const unparserPL = new Unparser(configPL, buildLexerConfig(configPL), sheetMapping.fetchDisplayName, new NamedExpressions()) - const unparserEN = new Unparser(configEN, buildLexerConfig(configEN), sheetMapping.fetchDisplayName, new NamedExpressions()) + const unparserPL = new Unparser(configPL, sheetMapping, new NamedExpressions()) + const unparserEN = new Unparser(configEN, sheetMapping, new NamedExpressions()) const formula = '=SUMA(1, 2)' @@ -411,11 +412,10 @@ describe('Unparse', () => { it('unparsing numbers with decimal separator', () => { const config = new Config({decimalSeparator: ',', functionArgSeparator: ';'}) - const lexerConfig = buildLexerConfig(config) const sheetMapping = new SheetMapping(buildTranslationPackage(enGB)) sheetMapping.addSheet('Sheet1') const parser = buildEmptyParserWithCaching(config, sheetMapping) - const unparser = new Unparser(config, lexerConfig, sheetMapping.fetchDisplayName, new NamedExpressions()) + const unparser = new Unparser(config, sheetMapping, new NamedExpressions()) const formula = '=1+1234,567' const ast = parser.parse(formula, adr('A1')).ast @@ -491,13 +491,12 @@ describe('Unparse', () => { describe('whitespaces', () => { const config = new Config() - const lexerConfig = buildLexerConfig(config) const sheetMapping = new SheetMapping(buildTranslationPackage(enGB)) sheetMapping.addSheet('Sheet1') sheetMapping.addSheet('Sheet2') sheetMapping.addSheet('Sheet with spaces') const parser = buildEmptyParserWithCaching(config, sheetMapping) - const unparser = new Unparser(config, lexerConfig, sheetMapping.fetchDisplayName, new NamedExpressions()) + const unparser = new Unparser(config, sheetMapping, new NamedExpressions()) it('should unparse with original whitespaces', () => { const formula = '= 1' @@ -600,9 +599,8 @@ describe('whitespaces', () => { it('when ignoreWhiteSpace = \'any\', should unparse a non-breakable space character', () => { const config = new Config({ ignoreWhiteSpace: 'any' }) - const lexerConfig = buildLexerConfig(config) const parser = buildEmptyParserWithCaching(config, sheetMapping) - const unparser = new Unparser(config, lexerConfig, sheetMapping.fetchDisplayName, new NamedExpressions()) + const unparser = new Unparser(config, sheetMapping, new NamedExpressions()) const formula = '=\u00A01' const ast = parser.parse(formula, adr('A1')).ast diff --git a/test/unit/parser/white-spaces.spec.ts b/test/unit/parser/white-spaces.spec.ts index d28da340d..39d8d3e9d 100644 --- a/test/unit/parser/white-spaces.spec.ts +++ b/test/unit/parser/white-spaces.spec.ts @@ -55,7 +55,7 @@ describe('tokenizeFormula', () => { expectArrayWithSameContent(tokenTypes, ['EqualsOp', 'ProcedureName', 'CellReference', 'ArrayColSep', 'WhiteSpace', 'CellReference', 'RParen']) }) - it('should not skip whitespace when there is empty argument ', () => { + it('should not skip whitespace when there is empty argument', () => { const tokens = lexer.tokenizeFormula('=PV(A1 , ,A2)').tokens const tokenTypes = tokens.map(token => token.tokenType.name) @@ -77,7 +77,7 @@ describe('tokenizeFormula', () => { expect(tokens[1].tokenType.name).toEqual('WhiteSpace') }) - it('should treat line feed (U+000A) as whitespace ', () => { + it('should treat line feed (U+000A) as whitespace', () => { const tokens = lexer.tokenizeFormula('=\n1').tokens expect(tokens[1].tokenType.name).toEqual('WhiteSpace') }) @@ -119,7 +119,7 @@ describe('processWhitespaces', () => { const tokens = parser.tokenizeFormula('= SUM(A1:A2)').tokens const processed = parser.bindWhitespacesToTokens(tokens) expect(processed.length).toBe(6) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + expect(processed[1].leadingWhitespace!.image).toBe(' ') expectArrayWithSameContent( [EqualsOp, ProcedureName, CellReference, RangeSeparator, CellReference, RParen], @@ -131,7 +131,7 @@ describe('processWhitespaces', () => { const tokens = parser.tokenizeFormula('= SUM(A1:A2)').tokens const processed = parser.bindWhitespacesToTokens(tokens) expect(processed.length).toBe(6) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + expect(processed[1].leadingWhitespace!.image).toBe(' ') expectArrayWithSameContent( [EqualsOp, ProcedureName, CellReference, RangeSeparator, CellReference, RParen], @@ -143,7 +143,7 @@ describe('processWhitespaces', () => { const tokens = parser.tokenizeFormula(' =SUM(A1:A2)').tokens const processed = parser.bindWhitespacesToTokens(tokens) expect(processed.length).toBe(6) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + expect(processed[0].leadingWhitespace!.image).toBe(' ') expectArrayWithSameContent( [EqualsOp, ProcedureName, CellReference, RangeSeparator, CellReference, RParen], diff --git a/test/unit/range-mapping.spec.ts b/test/unit/range-mapping.spec.ts index a048f3234..b3ac938f7 100644 --- a/test/unit/range-mapping.spec.ts +++ b/test/unit/range-mapping.spec.ts @@ -8,7 +8,7 @@ describe('RangeMapping', () => { const start = adr('A1') const end = adr('U50') - expect(mapping.getRange(start, end)).toBe(undefined) + expect(mapping.getRangeVertex(start, end)).toBe(undefined) }) it('setting range mapping', () => { @@ -17,9 +17,9 @@ describe('RangeMapping', () => { const end = adr('U50') const vertex = new RangeVertex(new AbsoluteCellRange(start, end)) - mapping.setRange(vertex) + mapping.addOrUpdateVertex(vertex) - expect(mapping.getRange(start, end)).toBe(vertex) + expect(mapping.getRangeVertex(start, end)).toBe(vertex) }) it('set column range', () => { @@ -28,8 +28,8 @@ describe('RangeMapping', () => { const end = colEnd('U') const vertex = new RangeVertex(new AbsoluteColumnRange(start.sheet, start.col, end.col)) - mapping.setRange(vertex) + mapping.addOrUpdateVertex(vertex) - expect(mapping.getRange(start, end)).toBe(vertex) + expect(mapping.getRangeVertex(start, end)).toBe(vertex) }) }) diff --git a/test/unit/range-vertex.spec.ts b/test/unit/range-vertex.spec.ts index 471883861..c099254ea 100644 --- a/test/unit/range-vertex.spec.ts +++ b/test/unit/range-vertex.spec.ts @@ -24,11 +24,11 @@ describe('RangeVertex with cache', () => { const rangeVertex = new RangeVertex(new AbsoluteCellRange(adr('B2'), adr('B11'))) const criterionString1 = '>=0' - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const criterion1 = buildCriterionLambda(criterionBuilder.parseCriterion(criterionString1, arithmeticHelper)!, arithmeticHelper) const criterionString2 = '=1' - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const criterion2 = buildCriterionLambda(criterionBuilder.parseCriterion(criterionString2, arithmeticHelper)!, arithmeticHelper) const criterionCache: CriterionCache = new Map() diff --git a/test/unit/rebuild.spec.ts b/test/unit/rebuild.spec.ts index b3fefbce9..b66951a75 100644 --- a/test/unit/rebuild.spec.ts +++ b/test/unit/rebuild.spec.ts @@ -53,4 +53,14 @@ describe('Rebuilding engine', () => { expect(rebuildEngineSpy).toHaveBeenCalled() }) + + it('doesn\'t throw after adding named expression (#1194)', () => { + const hf = HyperFormula.buildFromArray([['=42']], { + licenseKey: 'gpl-v3' + }) + + + hf.addNamedExpression('ABC', '=Sheet1!$A$1') + expect(() => hf.rebuildAndRecalculate()).not.toThrow() + }) }) diff --git a/test/unit/row-range.spec.ts b/test/unit/row-range.spec.ts index eedc2da51..1c1e36666 100644 --- a/test/unit/row-range.spec.ts +++ b/test/unit/row-range.spec.ts @@ -18,7 +18,7 @@ describe('Row ranges', () => { ['=SUM(C3:D4)'], ]) - const rowRange = engine.rangeMapping.getRange(rowStart(3), rowEnd(4))! + const rowRange = engine.rangeMapping.getRangeVertex(rowStart(3), rowEnd(4))! const c3 = engine.dependencyGraph.fetchCell(adr('C3')) const c4 = engine.dependencyGraph.fetchCell(adr('C4')) @@ -39,8 +39,8 @@ describe('Row ranges', () => { engine.setCellContents(adr('B1'), '=SUM(Z4:Z8)') - const rowRange35 = engine.rangeMapping.getRange(rowStart(3), rowEnd(5))! - const rowRange47 = engine.rangeMapping.getRange(rowStart(4), rowEnd(7))! + const rowRange35 = engine.rangeMapping.getRangeVertex(rowStart(3), rowEnd(5))! + const rowRange47 = engine.rangeMapping.getRangeVertex(rowStart(4), rowEnd(7))! const z4 = engine.dependencyGraph.fetchCell(adr('Z4')) const z5 = engine.dependencyGraph.fetchCell(adr('Z5')) @@ -58,4 +58,17 @@ describe('Row ranges', () => { expect(engine.graph.existsEdge(z7, rowRange47)).toBe(true) expect(engine.graph.existsEdge(z8, rowRange47)).toBe(false) }) + + it('should correctly handle infinite row ranges when setting cell values (line 890)', () => { + const engine = HyperFormula.buildFromArray([ + ['=SUM(3:4)'], + ]) + + expect(engine.getCellValue(adr('A1'))).toBe(0) + + engine.setCellContents(adr('E3'), 100) + engine.setCellContents(adr('F4'), 200) + + expect(engine.getCellValue(adr('A1'))).toBe(300) + }) }) diff --git a/test/unit/serialization.spec.ts b/test/unit/serialization.spec.ts index 2ffd4df3b..38abef07d 100644 --- a/test/unit/serialization.spec.ts +++ b/test/unit/serialization.spec.ts @@ -102,4 +102,23 @@ describe('serialization', () => { expect(engine1.getCellValueFormat(adr('K1'))).toEqual('Date()') expect(engine1.getCellValueDetailedType(adr('K1'))).toEqual(CellValueDetailedType.NUMBER_DATE) }) + + it('should return raw value for array formula spill cells', () => { + const engine = HyperFormula.buildFromArray([ + [1, 2, 3], + ['=TRANSPOSE(A1:C1)'], + ], { useArrayArithmetic: true }) + + expect(engine.getCellSerialized(adr('A2'))).toEqual('=TRANSPOSE(A1:C1)') + }) + + it('should return raw value for array formula non-top-left cells', () => { + const engine = HyperFormula.buildFromArray([ + [1, 2, 3], + ['=TRANSPOSE(A1:C1)'], + ], { useArrayArithmetic: true }) + + expect(engine.getCellSerialized(adr('A3'))).toEqual(2) + expect(engine.getCellSerialized(adr('A4'))).toEqual(3) + }) }) diff --git a/test/unit/testUtils.ts b/test/unit/testUtils.ts index 55ee232d9..41579e5c2 100644 --- a/test/unit/testUtils.ts +++ b/test/unit/testUtils.ts @@ -3,7 +3,7 @@ import {AbsoluteCellRange, AbsoluteColumnRange, AbsoluteRowRange} from '../../sr import {CellError, SimpleCellAddress, simpleCellAddress} from '../../src/Cell' import {Config} from '../../src/Config' import {DateTimeHelper} from '../../src/DateTimeHelper' -import {ArrayVertex, FormulaCellVertex, Graph, RangeVertex} from '../../src/DependencyGraph' +import {ArrayFormulaVertex, ScalarFormulaVertex, Graph, RangeVertex} from '../../src/DependencyGraph' import {ErrorMessage} from '../../src/error-message' import {defaultStringifyDateTime} from '../../src/format/format' import {complex} from '../../src/interpreter/ArithmeticHelper' @@ -21,41 +21,41 @@ import {ColumnRangeAst, RowRangeAst} from '../../src/parser/Ast' import {EngineComparator} from './graphComparator' export const extractReference = (engine: HyperFormula, address: SimpleCellAddress): CellAddress => { - return ((engine.addressMapping.fetchCell(address) as FormulaCellVertex).getFormula(engine.lazilyTransformingAstService) as CellReferenceAst).reference + return ((engine.addressMapping.getCell(address) as ScalarFormulaVertex).getFormula(engine.lazilyTransformingAstService) as CellReferenceAst).reference } export const extractRange = (engine: HyperFormula, address: SimpleCellAddress): AbsoluteCellRange => { - const formula = (engine.addressMapping.fetchCell(address) as FormulaCellVertex).getFormula(engine.lazilyTransformingAstService) as ProcedureAst + const formula = (engine.addressMapping.getCell(address) as ScalarFormulaVertex).getFormula(engine.lazilyTransformingAstService) as ProcedureAst const rangeAst = formula.args[0] as CellRangeAst return new AbsoluteCellRange(rangeAst.start.toSimpleCellAddress(address), rangeAst.end.toSimpleCellAddress(address)) } export const extractColumnRange = (engine: HyperFormula, address: SimpleCellAddress): AbsoluteColumnRange => { - const formula = (engine.addressMapping.fetchCell(address) as FormulaCellVertex).getFormula(engine.lazilyTransformingAstService) as ProcedureAst + const formula = (engine.addressMapping.getCell(address) as ScalarFormulaVertex).getFormula(engine.lazilyTransformingAstService) as ProcedureAst const rangeAst = formula.args[0] as ColumnRangeAst return AbsoluteColumnRange.fromColumnRange(rangeAst, address) } export const extractRowRange = (engine: HyperFormula, address: SimpleCellAddress): AbsoluteRowRange => { - const formula = (engine.addressMapping.fetchCell(address) as FormulaCellVertex).getFormula(engine.lazilyTransformingAstService) as ProcedureAst + const formula = (engine.addressMapping.getCell(address) as ScalarFormulaVertex).getFormula(engine.lazilyTransformingAstService) as ProcedureAst const rangeAst = formula.args[0] as RowRangeAst return AbsoluteRowRange.fromRowRangeAst(rangeAst, address) } export const extractMatrixRange = (engine: HyperFormula, address: SimpleCellAddress): AbsoluteCellRange => { - const formula = (engine.addressMapping.fetchCell(address) as ArrayVertex).getFormula(engine.lazilyTransformingAstService) as ProcedureAst + const formula = (engine.addressMapping.getCell(address) as ArrayFormulaVertex).getFormula(engine.lazilyTransformingAstService) as ProcedureAst const rangeAst = formula.args[0] as CellRangeAst return AbsoluteCellRange.fromCellRange(rangeAst, address) } export const expectReferenceToHaveRefError = (engine: HyperFormula, address: SimpleCellAddress) => { - const errorAst = (engine.addressMapping.fetchCell(address) as FormulaCellVertex).getFormula(engine.lazilyTransformingAstService) as ErrorAst + const errorAst = (engine.addressMapping.getCell(address) as ScalarFormulaVertex).getFormula(engine.lazilyTransformingAstService) as ErrorAst expect(errorAst.type).toEqual(AstNodeType.ERROR) expect(errorAst.error).toEqualError(new CellError(ErrorType.REF)) } export const expectFunctionToHaveRefError = (engine: HyperFormula, address: SimpleCellAddress) => { - const formula = (engine.addressMapping.fetchCell(address) as FormulaCellVertex).getFormula(engine.lazilyTransformingAstService) as ProcedureAst + const formula = (engine.addressMapping.getCell(address) as ScalarFormulaVertex).getFormula(engine.lazilyTransformingAstService) as ProcedureAst const errorAst = formula.args.find((arg) => arg !== undefined && arg.type === AstNodeType.ERROR) as ErrorAst expect(errorAst.type).toEqual(AstNodeType.ERROR) expect(errorAst.error).toEqualError(new CellError(ErrorType.REF)) @@ -117,19 +117,19 @@ export const rowEnd = (input: number, sheet: number = 0): SimpleCellAddress => { } export const colStart = (input: string, sheet: number = 0): SimpleCellAddress => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const result = /^(\$?)([A-Za-z]+)/.exec(input)! return simpleCellAddress(sheet, colNumber(result[2]), 0) } export const colEnd = (input: string, sheet: number = 0): SimpleCellAddress => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const result = /^(\$?)([A-Za-z]+)/.exec(input)! return simpleCellAddress(sheet, colNumber(result[2]), Number.POSITIVE_INFINITY) } export const adr = (stringAddress: string, sheet: number = 0): SimpleCellAddress => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const result = /^(\$([A-Za-z0-9_]+)\.)?(\$?)([A-Za-z]+)(\$?)([0-9]+)$/.exec(stringAddress)! const row = Number(result[6]) - 1 return simpleCellAddress(sheet, colNumber(result[4]), row) @@ -213,7 +213,7 @@ export function columnIndexToSheet(columnIndex: ColumnIndex, width: number, heig for (const row of index) { result[row] = result[row] ?? [] if (result[row][col] !== undefined) { - throw new Error('ColumnIndex ambiguity.') + throw new Error(`ColumnIndex ambiguity. Expected ${JSON.stringify(result[row][col])}, but found ${JSON.stringify(value)} at row ${JSON.stringify(row)}, column ${JSON.stringify(col)}`) } result[row][col] = value } diff --git a/test/unit/undo-redo.spec.ts b/test/unit/undo-redo.spec.ts index 3d37a6807..14927f153 100644 --- a/test/unit/undo-redo.spec.ts +++ b/test/unit/undo-redo.spec.ts @@ -1,3 +1,4 @@ +/* eslint-disable jest/expect-expect */ import {ErrorType, HyperFormula, NoOperationToRedoError, NoOperationToUndoError} from '../../src' import {AbsoluteCellRange} from '../../src/AbsoluteCellRange' import {ErrorMessage} from '../../src/error-message' @@ -104,7 +105,7 @@ describe('Undo - removing rows', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray(sheet)) }) - it('dummy operation should also be undoable', () => { + it('dummy operation removeRows should also be undoable', () => { const sheet = [ ['1'] ] @@ -133,7 +134,7 @@ describe('Undo - removing rows', () => { }) describe('Undo - adding rows', () => { - it('works', () => { + it('restores original state after adding single row', () => { const sheet = [ ['1'], // add after that ['3'], @@ -146,7 +147,7 @@ describe('Undo - adding rows', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray(sheet)) }) - it('dummy operation should also be undoable', () => { + it('dummy operation addRows should also be undoable', () => { const sheet = [ ['1'] ] @@ -174,7 +175,7 @@ describe('Undo - adding rows', () => { }) describe('Undo - moving rows', () => { - it('works', () => { + it('restores original row order after move', () => { const sheet = [ [0], [1], [2], [3], [4], [5], [6], [7], ] @@ -185,7 +186,7 @@ describe('Undo - moving rows', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray(sheet)) }) - it('works in both directions', () => { + it('restores original row order when moving rows backward', () => { const sheet = [ [0], [1], [2], [3], [4], [5], [6], [7], ] @@ -196,7 +197,7 @@ describe('Undo - moving rows', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray(sheet)) }) - it('should restore range', () => { + it('restores range formula after moving rows forward', () => { const engine = HyperFormula.buildFromArray([ [1, null], [2, '=SUM(A1:A2)'], @@ -204,7 +205,7 @@ describe('Undo - moving rows', () => { engine.moveRows(0, 1, 1, 3) engine.undo() - expect(engine.getCellFormula(adr('B2'))).toEqual('=SUM(A1:A2)') + expect(engine.getCellFormula(adr('B2'))).toBe('=SUM(A1:A2)') }) it('should restore range when moving other way', () => { @@ -216,12 +217,12 @@ describe('Undo - moving rows', () => { engine.moveRows(0, 2, 1, 1) engine.undo() - expect(engine.getCellFormula(adr('B2'))).toEqual('=SUM(A1:A2)') + expect(engine.getCellFormula(adr('B2'))).toBe('=SUM(A1:A2)') }) }) describe('Undo - moving columns', () => { - it('works', () => { + it('restores original column order after move', () => { const sheet = [ [0, 1, 2, 3, 4, 5, 6, 7], ] @@ -232,7 +233,7 @@ describe('Undo - moving columns', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray(sheet)) }) - it('works in both directions', () => { + it('restores original column order when moving columns backward', () => { const sheet = [ [0, 1, 2, 3, 4, 5, 6, 7], ] @@ -243,7 +244,7 @@ describe('Undo - moving columns', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray(sheet)) }) - it('should restore range', () => { + it('restores range formula after moving columns forward', () => { const engine = HyperFormula.buildFromArray([ [1, 2], [null, '=SUM(A1:B1)'], @@ -251,7 +252,7 @@ describe('Undo - moving columns', () => { engine.moveColumns(0, 1, 1, 3) engine.undo() - expect(engine.getCellFormula(adr('B2'))).toEqual('=SUM(A1:B1)') + expect(engine.getCellFormula(adr('B2'))).toBe('=SUM(A1:B1)') }) it('should restore range when moving to left', () => { @@ -263,12 +264,12 @@ describe('Undo - moving columns', () => { engine.moveColumns(0, 2, 1, 1) engine.undo() - expect(engine.getCellFormula(adr('B2'))).toEqual('=SUM(A1:B1)') + expect(engine.getCellFormula(adr('B2'))).toBe('=SUM(A1:B1)') }) }) describe('Undo - adding columns', () => { - it('works', () => { + it('restores original state after adding single column', () => { const sheet = [ ['1', /* */ '3'], ] @@ -280,7 +281,7 @@ describe('Undo - adding columns', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray(sheet)) }) - it('dummy operation should also be undoable', () => { + it('dummy operation addColumns should also be undoable', () => { const sheet = [ ['1'] ] @@ -292,7 +293,7 @@ describe('Undo - adding columns', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray(sheet)) }) - it('works for more addition segments', () => { + it('restores state after adding multiple column segments', () => { const sheet = [ ['1', '2', '3'], ] @@ -318,7 +319,7 @@ describe('Undo - removing columns', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray(sheet)) }) - it('works for simple values', () => { + it('restores column with simple values', () => { const sheet = [ ['1', '2', '3'], ] @@ -342,7 +343,7 @@ describe('Undo - removing columns', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray(sheet)) }) - it('restores dependent cell formulas', () => { + it('restores dependent cell formulas after column removal', () => { const sheet = [ ['=A2', '42', '3'], ] @@ -354,7 +355,7 @@ describe('Undo - removing columns', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray(sheet)) }) - it('formulas are built correctly when there was a pause in computation', () => { + it('builds formulas correctly with suspended evaluation during column removal', () => { const sheet = [ ['=A2', '42', '3'], ] @@ -380,7 +381,7 @@ describe('Undo - removing columns', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray(sheet)) }) - it('dummy operation should also be undoable', () => { + it('dummy operation removeColumns should also be undoable', () => { const sheet = [ ['1'] ] @@ -392,7 +393,7 @@ describe('Undo - removing columns', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray(sheet)) }) - it('works for more removal segments', () => { + it('restores state after removing multiple column segments', () => { const sheet = [ ['1', '2', '3', '4'], ] @@ -439,7 +440,7 @@ describe('Undo - removing sheet', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray(sheet)) }) - it('restores dependent cell formulas', () => { + it('restores cross-sheet formula dependencies after sheet removal', () => { const sheets = { Sheet1: [['=Sheet2!A1']], Sheet2: [['42']], @@ -452,7 +453,7 @@ describe('Undo - removing sheet', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(sheets)) }) - it('formulas are built correctly when there was a pause in computation', () => { + it('builds formulas correctly with suspended evaluation during sheet removal', () => { const sheets = { Sheet1: [['=Sheet2!A1']], Sheet2: [['42']], @@ -466,6 +467,70 @@ describe('Undo - removing sheet', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(sheets)) }) + + it('restores sheet correctly after multiple undo/redo cycles', () => { + const sheets = { + Sheet1: [['1', '2']], + Sheet2: [['3', '4']], + } + const engine = HyperFormula.buildFromSheets(sheets) + engine.removeSheet(1) + + engine.undo() + expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(sheets)) + + engine.redo() + + expect(engine.getSheetNames()).toEqual(['Sheet1']) + + engine.undo() + + expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(sheets)) + + engine.redo() + + expect(engine.getSheetNames()).toEqual(['Sheet1']) + + engine.undo() + + expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(sheets)) + + engine.redo() + + expect(engine.getSheetNames()).toEqual(['Sheet1']) + + engine.undo() + + expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(sheets)) + }) + + it('restores sheet and cross-sheet references after row removal', () => { + const sheets = { + Sheet1: [['1'], ['2'], ['=Sheet2!A1']], + Sheet2: [['42']], + } + const engine = HyperFormula.buildFromSheets(sheets) + engine.removeSheet(1) + engine.undo() + + expect(engine.getSheetNames()).toEqual(['Sheet1', 'Sheet2']) + expect(engine.getCellValue(adr('A1'))).toBe(1) + expect(engine.getCellValue(adr('A2'))).toBe(2) + expect(engine.getCellValue(adr('A3'))).toBe(42) + expect(engine.getCellValue(adr('A1', 1))).toBe(42) + }) + + it('restores scoped named expressions', () => { + const engine = HyperFormula.buildFromSheets({ + Sheet1: [['=MyName']], + Sheet2: [['1']], + }) + engine.addNamedExpression('MyName', '=42', 0) + engine.removeSheet(0) + engine.undo() + + expect(engine.getCellValue(adr('A1'))).toBe(42) + }) }) describe('Undo - renaming sheet', () => { @@ -476,8 +541,8 @@ describe('Undo - renaming sheet', () => { engine.undo() - expect(engine.getCellValue(adr('A1'))).toEqual(1) - expect(engine.getSheetName(0)).toEqual('Sheet1') + expect(engine.getCellValue(adr('A1'))).toBe(1) + expect(engine.getSheetName(0)).toBe('Sheet1') }) it('undo rename sheet', () => { @@ -486,12 +551,405 @@ describe('Undo - renaming sheet', () => { engine.undo() - expect(engine.getSheetName(0)).toEqual('Sheet1') + expect(engine.getSheetName(0)).toBe('Sheet1') + }) + + it('undo rename with case change only', () => { + const engine = HyperFormula.buildFromSheets({'Sheet1': [[1]]}) + engine.renameSheet(0, 'SHEET1') + + expect(engine.getSheetName(0)).toBe('SHEET1') + + engine.undo() + + expect(engine.getSheetName(0)).toBe('Sheet1') + }) + + it('undo rename preserves cell values', () => { + const engine = HyperFormula.buildFromSheets({'Sheet1': [[42], ['=A1*2']]}) + engine.renameSheet(0, 'NewName') + engine.undo() + + expect(engine.getSheetName(0)).toBe('Sheet1') + expect(engine.getCellValue(adr('A1'))).toBe(42) + expect(engine.getCellValue(adr('A2'))).toBe(84) + }) + + it('undo rename with suspended evaluation', () => { + const engine = HyperFormula.buildFromSheets({'Sheet1': [[1]]}) + engine.suspendEvaluation() + engine.renameSheet(0, 'Foo') + engine.undo() + engine.resumeEvaluation() + + expect(engine.getSheetName(0)).toBe('Sheet1') + }) + + it('undo rename that merged with placeholder sheet', () => { + const engine = HyperFormula.buildFromSheets({ + 'Sheet1': [['=OldName!A1', '=NewName!A1']], + 'OldName': [[42]], + }) + const sheet1Id = engine.getSheetId('Sheet1')! + const oldNameId = engine.getSheetId('OldName')! + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(42) + expect(engine.getCellValue(adr('B1', sheet1Id))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.SheetRef)) + + engine.renameSheet(oldNameId, 'NewName') + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(42) + expect(engine.getCellValue(adr('B1', sheet1Id))).toBe(42) + + engine.undo() + + expect(engine.getSheetName(oldNameId)).toBe('OldName') + expect(engine.getCellFormula(adr('A1', sheet1Id))).toBe('=OldName!A1') + expect(engine.getCellFormula(adr('B1', sheet1Id))).toBe('=NewName!A1') + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(42) + expect(engine.getCellValue(adr('B1', sheet1Id))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.SheetRef)) + }) + + it('undo rename with range reference updates formula (merged with placeholder sheet)', () => { + const engine = HyperFormula.buildFromSheets({ + 'Sheet1': [['=SUM(OldName!A1:B2)', '=SUM(NewName!A1:B2)']], + 'OldName': [[10, 20], [30, 40]], + }) + const sheet1Id = engine.getSheetId('Sheet1')! + const oldNameId = engine.getSheetId('OldName')! + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(100) + expect(engine.getCellValue(adr('B1', sheet1Id))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.SheetRef)) + + engine.renameSheet(oldNameId, 'NewName') + + expect(engine.getCellFormula(adr('A1', sheet1Id))).toBe('=SUM(NewName!A1:B2)') + expect(engine.getCellFormula(adr('B1', sheet1Id))).toBe('=SUM(NewName!A1:B2)') + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(100) + expect(engine.getCellValue(adr('B1', sheet1Id))).toBe(100) + + engine.undo() + + expect(engine.getCellFormula(adr('A1', sheet1Id))).toBe('=SUM(OldName!A1:B2)') + expect(engine.getCellFormula(adr('B1', sheet1Id))).toBe('=SUM(NewName!A1:B2)') + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(100) + expect(engine.getCellValue(adr('B1', sheet1Id))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.SheetRef)) + }) + + it('restores the dependency graph structure on undo', () => { + const engine = HyperFormula.buildFromSheets({ + 'Sheet1': [ + ['=OldName!A1', '=NewName!A1', '=SUM(OldName!A1:B2)', '=SUM(NewName!A1:B2)'], + ['=A1*2', '=B1+10', '=C1+A1', '=D1+B1'], + ], + 'OldName': [[1, 2], [3, 4]], + }) + const sheet1Id = engine.getSheetId('Sheet1')! + const oldNameId = engine.getSheetId('OldName')! + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(1) + expect(engine.getCellValue(adr('B1', sheet1Id))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.SheetRef)) + expect(engine.getCellValue(adr('C1', sheet1Id))).toBe(10) + expect(engine.getCellValue(adr('D1', sheet1Id))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.SheetRef)) + expect(engine.getCellValue(adr('A2', sheet1Id))).toBe(2) + expect(engine.getCellValue(adr('B2', sheet1Id))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.SheetRef)) + expect(engine.getCellValue(adr('C2', sheet1Id))).toBe(11) + expect(engine.getCellValue(adr('D2', sheet1Id))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.SheetRef)) + + engine.renameSheet(oldNameId, 'NewName') + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(1) + expect(engine.getCellValue(adr('B1', sheet1Id))).toBe(1) + expect(engine.getCellValue(adr('C1', sheet1Id))).toBe(10) + expect(engine.getCellValue(adr('D1', sheet1Id))).toBe(10) + expect(engine.getCellValue(adr('A2', sheet1Id))).toBe(2) + expect(engine.getCellValue(adr('B2', sheet1Id))).toBe(11) + expect(engine.getCellValue(adr('C2', sheet1Id))).toBe(11) + expect(engine.getCellValue(adr('D2', sheet1Id))).toBe(11) + + engine.undo() + + expect(engine.getCellFormula(adr('A1', sheet1Id))).toBe('=OldName!A1') + expect(engine.getCellFormula(adr('B1', sheet1Id))).toBe('=NewName!A1') + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(1) + expect(engine.getCellValue(adr('B1', sheet1Id))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.SheetRef)) + expect(engine.getCellValue(adr('C1', sheet1Id))).toBe(10) + expect(engine.getCellValue(adr('D1', sheet1Id))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.SheetRef)) + expect(engine.getCellValue(adr('A2', sheet1Id))).toBe(2) + expect(engine.getCellValue(adr('B2', sheet1Id))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.SheetRef)) + expect(engine.getCellValue(adr('C2', sheet1Id))).toBe(11) + expect(engine.getCellValue(adr('D2', sheet1Id))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.SheetRef)) + + engine.setCellContents(adr('A1', oldNameId), 100) + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(100) + expect(engine.getCellValue(adr('C1', sheet1Id))).toBe(109) + expect(engine.getCellValue(adr('A2', sheet1Id))).toBe(200) + expect(engine.getCellValue(adr('C2', sheet1Id))).toBe(209) + expect(engine.getCellValue(adr('B1', sheet1Id))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.SheetRef)) + expect(engine.getCellValue(adr('D1', sheet1Id))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.SheetRef)) + expect(engine.getCellValue(adr('B2', sheet1Id))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.SheetRef)) + expect(engine.getCellValue(adr('D2', sheet1Id))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.SheetRef)) + }) + + it('multiple undo/redo cycles for rename', () => { + const engine = HyperFormula.buildFromSheets({'Sheet1': [[1]]}) + engine.renameSheet(0, 'Renamed') + engine.undo() + + expect(engine.getSheetName(0)).toBe('Sheet1') + + engine.redo() + + expect(engine.getSheetName(0)).toBe('Renamed') + + engine.undo() + + expect(engine.getSheetName(0)).toBe('Sheet1') + + engine.redo() + + expect(engine.getSheetName(0)).toBe('Renamed') + }) + + it('undo multiple sequential renames', () => { + const engine = HyperFormula.buildFromSheets({'Sheet1': [[1]]}) + engine.renameSheet(0, 'Name1') + engine.renameSheet(0, 'Name2') + engine.renameSheet(0, 'Name3') + + engine.undo() + + expect(engine.getSheetName(0)).toBe('Name2') + + engine.undo() + + expect(engine.getSheetName(0)).toBe('Name1') + + engine.undo() + + expect(engine.getSheetName(0)).toBe('Sheet1') + }) + + it('undo rename combined with cell content changes', () => { + const engine = HyperFormula.buildFromSheets({'Sheet1': [[1]]}) + engine.setCellContents(adr('A1'), 10) + engine.renameSheet(0, 'NewName') + engine.setCellContents(adr('A1'), 100) + engine.undo() + + expect(engine.getCellValue(adr('A1'))).toBe(10) + expect(engine.getSheetName(0)).toBe('NewName') + + engine.undo() + + expect(engine.getSheetName(0)).toBe('Sheet1') + + engine.undo() + + expect(engine.getCellValue(adr('A1'))).toBe(1) + }) + + it('undo rename in batch mode', () => { + const engine = HyperFormula.buildFromSheets({'Sheet1': [[1]], 'Sheet2': [[2]]}) + engine.batch(() => { + engine.renameSheet(0, 'NewName1') + engine.renameSheet(1, 'NewName2') + }) + + engine.undo() + + expect(engine.getSheetName(0)).toBe('Sheet1') + expect(engine.getSheetName(1)).toBe('Sheet2') + }) + + it('undo rename with chained dependencies across sheets', () => { + const engine = HyperFormula.buildFromSheets({ + 'Sheet1': [['=Sheet2!A1+2']], + 'Sheet2': [['=OldName!A1*2']], + 'OldName': [[42]], + }) + const sheet1Id = engine.getSheetId('Sheet1')! + const sheet2Id = engine.getSheetId('Sheet2')! + const oldNameId = engine.getSheetId('OldName')! + + expect(engine.getCellValue(adr('A1', sheet2Id))).toBe(84) + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(86) + + engine.renameSheet(oldNameId, 'NewName') + + expect(engine.getCellFormula(adr('A1', sheet2Id))).toBe('=NewName!A1*2') + + engine.undo() + + expect(engine.getCellFormula(adr('A1', sheet2Id))).toBe('=OldName!A1*2') + expect(engine.getCellValue(adr('A1', sheet2Id))).toBe(84) + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(86) + }) + + it('undo rename with named expressions', () => { + const engine = HyperFormula.buildFromSheets({ + 'Sheet1': [['=MyValue']], + 'OldName': [[99]], + }, {}, [ + { name: 'MyValue', expression: '=OldName!$A$1' } + ]) + const sheet1Id = engine.getSheetId('Sheet1')! + const oldNameId = engine.getSheetId('OldName')! + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(99) + + engine.renameSheet(oldNameId, 'NewName') + + expect(engine.getNamedExpressionFormula('MyValue')).toBe('=NewName!$A$1') + + engine.undo() + + expect(engine.getNamedExpressionFormula('MyValue')).toBe('=OldName!$A$1') + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(99) + }) + + it('undo rename after row removal', () => { + const engine = HyperFormula.buildFromSheets({ + 'Sheet1': [[1], [2], ['=OldName!A1']], + 'OldName': [[42]], + }) + const sheet1Id = engine.getSheetId('Sheet1')! + const oldNameId = engine.getSheetId('OldName')! + + engine.removeRows(sheet1Id, [0, 1]) + engine.renameSheet(oldNameId, 'NewName') + + expect(engine.getCellValue(adr('A2', sheet1Id))).toBe(42) + expect(engine.getCellFormula(adr('A2', sheet1Id))).toBe('=NewName!A1') + + engine.undo() + + expect(engine.getCellFormula(adr('A2', sheet1Id))).toBe('=OldName!A1') + expect(engine.getCellValue(adr('A2', sheet1Id))).toBe(42) + + engine.undo() + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(1) + expect(engine.getCellValue(adr('A2', sheet1Id))).toBe(2) + expect(engine.getCellValue(adr('A3', sheet1Id))).toBe(42) + }) + + it('undo rename sheet that merged with placeholder restores placeholder', () => { + const engine = HyperFormula.buildFromSheets({ + 'Sheet1': [['=PlaceholderName!A1']], + 'OldName': [[42]], + }) + const sheet1Id = engine.getSheetId('Sheet1')! + const oldNameId = engine.getSheetId('OldName')! + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.SheetRef)) + + engine.renameSheet(oldNameId, 'PlaceholderName') + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(42) + expect(engine.getSheetName(oldNameId)).toBe('PlaceholderName') + + engine.undo() + + expect(engine.getSheetName(oldNameId)).toBe('OldName') + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.SheetRef)) + }) + + it('redo rename sheet that merged with placeholder works correctly', () => { + const engine = HyperFormula.buildFromSheets({ + 'Sheet1': [['=PlaceholderName!A1']], + 'OldName': [[42]], + }) + const sheet1Id = engine.getSheetId('Sheet1')! + const oldNameId = engine.getSheetId('OldName')! + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.SheetRef)) + + engine.renameSheet(oldNameId, 'PlaceholderName') + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(42) + + engine.undo() + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.SheetRef)) + + engine.redo() + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(42) + expect(engine.getSheetName(oldNameId)).toBe('PlaceholderName') + }) + + it('multiple undo/redo cycles with placeholder sheet merge', () => { + const engine = HyperFormula.buildFromSheets({ + 'Sheet1': [['=GhostSheet!A1']], + 'RealSheet': [[100]], + }) + const sheet1Id = engine.getSheetId('Sheet1')! + const realSheetId = engine.getSheetId('RealSheet')! + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.SheetRef)) + + engine.renameSheet(realSheetId, 'GhostSheet') + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(100) + + engine.undo() + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.SheetRef)) + expect(engine.getSheetName(realSheetId)).toBe('RealSheet') + + engine.redo() + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(100) + expect(engine.getSheetName(realSheetId)).toBe('GhostSheet') + + engine.undo() + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.SheetRef)) + expect(engine.getSheetName(realSheetId)).toBe('RealSheet') + + engine.redo() + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(100) + expect(engine.getSheetName(realSheetId)).toBe('GhostSheet') + + engine.undo() + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.SheetRef)) + + engine.redo() + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(100) + }) + + it('undo rename with range reference to placeholder sheet', () => { + const engine = HyperFormula.buildFromSheets({ + 'Sheet1': [['=SUM(PlaceholderName!A1:B2)']], + 'OldName': [[1, 2], [3, 4]], + }) + const sheet1Id = engine.getSheetId('Sheet1')! + const oldNameId = engine.getSheetId('OldName')! + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.SheetRef)) + + engine.renameSheet(oldNameId, 'PlaceholderName') + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(10) + + engine.undo() + + expect(engine.getCellValue(adr('A1', sheet1Id))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.SheetRef)) + expect(engine.getSheetName(oldNameId)).toBe('OldName') + + engine.redo() + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(10) }) }) describe('Undo - setting cell content', () => { - it('works for simple values', () => { + it('restores simple numeric values', () => { const sheet = [ ['3'], ] @@ -503,7 +961,7 @@ describe('Undo - setting cell content', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray(sheet)) }) - it('works for empty values', () => { + it('restores empty cell state', () => { const sheet = [ [null], ] @@ -515,7 +973,7 @@ describe('Undo - setting cell content', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray(sheet)) }) - it('works for formula values', () => { + it('restores formula cell content', () => { const sheet = [ ['=42'], ] @@ -527,7 +985,7 @@ describe('Undo - setting cell content', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray(sheet)) }) - it('setting multiple cell contents is one operation', () => { + it('undoes multiple cell contents as one operation', () => { const sheet = [ ['3', '4'], ] @@ -541,7 +999,7 @@ describe('Undo - setting cell content', () => { }) describe('Undo - adding sheet', () => { - it('works for basic case', () => { + it('removes named sheet on undo', () => { const engine = HyperFormula.buildFromArray([]) engine.addSheet('SomeSheet') @@ -549,10 +1007,45 @@ describe('Undo - adding sheet', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([])) }) + + it('removes auto-generated sheet on undo', () => { + const engine = HyperFormula.buildFromArray([]) + engine.addSheet() + engine.undo() + + expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([])) + }) + + it('restores cross-sheet reference error after undo', () => { + const engine = HyperFormula.buildFromArray([['=NewSheet!A1']]) + engine.addSheet('NewSheet') + engine.setCellContents({sheet: 1, col: 0, row: 0}, '42') + + expect(engine.getCellValue(adr('A1'))).toBe(42) + expect(engine.getCellValue(adr('A1', 1))).toBe(42) + + engine.undo() + engine.undo() + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.SheetRef)) + }) + + it('builds formulas correctly with suspended evaluation during sheet addition', () => { + const engine = HyperFormula.buildFromArray([['=NewSheet!A1']]) + engine.suspendEvaluation() + engine.addSheet('NewSheet') + engine.setCellContents({sheet: 1, col: 0, row: 0}, '42') + + engine.undo() + engine.undo() + engine.resumeEvaluation() + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.SheetRef)) + }) }) describe('Undo - clearing sheet', () => { - it('works for empty sheet', () => { + it('handles undo on already empty sheet', () => { const engine = HyperFormula.buildFromArray([]) engine.clearSheet(0) @@ -561,7 +1054,7 @@ describe('Undo - clearing sheet', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray([])) }) - it('works with restoring simple values', () => { + it('restores simple values after clearing', () => { const sheet = [ ['1'], ] @@ -573,7 +1066,7 @@ describe('Undo - clearing sheet', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray(sheet)) }) - it('works with restoring formulas', () => { + it('restores formulas after clearing', () => { const sheet = [ ['=42'], ] @@ -587,7 +1080,7 @@ describe('Undo - clearing sheet', () => { }) describe('Undo - setting sheet contents', () => { - it('works for basic case', () => { + it('restores original sheet content', () => { const sheet = [['13']] const engine = HyperFormula.buildFromArray(sheet) engine.setSheetContent(0, [['42']]) @@ -611,7 +1104,7 @@ describe('Undo - setting sheet contents', () => { }) describe('Undo - moving cells', () => { - it('works for simple case', () => { + it('restores cell to original position', () => { const sheet = [ ['foo'], [null], @@ -624,7 +1117,7 @@ describe('Undo - moving cells', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray(sheet)) }) - it('restores data', () => { + it('restores overwritten data at target location', () => { const sheet = [ ['foo'], ['42'], @@ -637,7 +1130,7 @@ describe('Undo - moving cells', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray(sheet)) }) - it('restores dependent cell formulas', () => { + it('restores dependent cell formulas after cell move', () => { const sheet = [ ['=A2'], ['42'], @@ -650,7 +1143,7 @@ describe('Undo - moving cells', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray(sheet)) }) - it('formulas are built correctly when there was a pause in computation', () => { + it('builds formulas correctly with suspended evaluation during cell move', () => { const sheet = [ ['=A2'], ['3'], @@ -665,7 +1158,7 @@ describe('Undo - moving cells', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray(sheet)) }) - it('removed added global named expression', () => { + it('removes global named expression promoted during move', () => { const engine = HyperFormula.buildFromSheets({ 'Sheet1': [], 'Sheet2': [] @@ -676,7 +1169,7 @@ describe('Undo - moving cells', () => { engine.undo() - expect(engine.getNamedExpressionValue('foo')).toEqual(undefined) + expect(engine.getNamedExpressionValue('foo')).toBeUndefined() }) it('remove global named expression even if it was added after formula', () => { @@ -689,13 +1182,13 @@ describe('Undo - moving cells', () => { engine.undo() - expect(engine.getNamedExpressionValue('foo', 0)).toEqual('bar') - expect(engine.getNamedExpressionValue('foo')).toEqual(undefined) + expect(engine.getNamedExpressionValue('foo', 0)).toBe('bar') + expect(engine.getNamedExpressionValue('foo')).toBeUndefined() }) }) describe('Undo - cut-paste', () => { - it('works for static content', () => { + it('restores source and target cells after cut-paste', () => { const sheet = [ ['foo'], ['bar'], @@ -709,7 +1202,7 @@ describe('Undo - cut-paste', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray(sheet)) }) - it('undoing doesnt roll back clipboard', () => { + it('does not roll back clipboard on undo', () => { const sheet = [ ['foo'], ['bar'], @@ -722,7 +1215,7 @@ describe('Undo - cut-paste', () => { expect(engine.isClipboardEmpty()).toBe(true) }) - it('removed added global named expression', () => { + it('removes global named expression promoted during cut-paste', () => { const engine = HyperFormula.buildFromSheets({ 'Sheet1': [], 'Sheet2': [] @@ -734,13 +1227,13 @@ describe('Undo - cut-paste', () => { engine.undo() - expect(engine.getNamedExpressionValue('foo', 0)).toEqual('bar') - expect(engine.getNamedExpressionValue('foo')).toEqual(undefined) + expect(engine.getNamedExpressionValue('foo', 0)).toBe('bar') + expect(engine.getNamedExpressionValue('foo')).toBeUndefined() }) }) describe('Undo - copy-paste', () => { - it('works', () => { + it('restores original content after copy-paste', () => { const sheet = [ ['foo'], ['bar'], @@ -754,7 +1247,7 @@ describe('Undo - copy-paste', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray(sheet)) }) - it('removed added global named expression', () => { + it('removes global named expression promoted during copy-paste', () => { const engine = HyperFormula.buildFromSheets({ 'Sheet1': [], 'Sheet2': [] @@ -766,13 +1259,13 @@ describe('Undo - copy-paste', () => { engine.undo() - expect(engine.getNamedExpressionValue('foo', 0)).toEqual('bar') - expect(engine.getNamedExpressionValue('foo')).toEqual(undefined) + expect(engine.getNamedExpressionValue('foo', 0)).toBe('bar') + expect(engine.getNamedExpressionValue('foo')).toBeUndefined() }) }) describe('Undo - add named expression', () => { - it('works', () => { + it('removes named expression and restores error', () => { const engine = HyperFormula.buildFromArray([ ['=foo'] ]) @@ -781,13 +1274,13 @@ describe('Undo - add named expression', () => { engine.undo() - expect(engine.listNamedExpressions().length).toEqual(0) + expect(engine.listNamedExpressions().length).toBe(0) expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.NAME, ErrorMessage.NamedExpressionName('foo'))) }) }) describe('Undo - remove named expression', () => { - it('works', () => { + it('restores removed named expression', () => { const engine = HyperFormula.buildFromArray([ ['=foo'] ]) @@ -797,13 +1290,13 @@ describe('Undo - remove named expression', () => { engine.undo() - expect(engine.listNamedExpressions().length).toEqual(1) - expect(engine.getCellValue(adr('A1'))).toEqual('foo') + expect(engine.listNamedExpressions().length).toBe(1) + expect(engine.getCellValue(adr('A1'))).toBe('foo') }) }) describe('Undo - change named expression', () => { - it('works', () => { + it('restores original expression value', () => { const engine = HyperFormula.buildFromArray([ ['=foo'] ]) @@ -813,13 +1306,13 @@ describe('Undo - change named expression', () => { engine.undo() - expect(engine.listNamedExpressions().length).toEqual(1) - expect(engine.getCellValue(adr('A1'))).toEqual('foo') + expect(engine.listNamedExpressions().length).toBe(1) + expect(engine.getCellValue(adr('A1'))).toBe('foo') }) }) describe('Undo', () => { - it('when there is no operation to undo', () => { + it('throws error when undo stack is empty', () => { const engine = HyperFormula.buildEmpty() expect(() => { @@ -835,7 +1328,7 @@ describe('Undo', () => { const changes = engine.undo() - expect(engine.getCellValue(adr('B1'))).toEqual(3) + expect(engine.getCellValue(adr('B1'))).toBe(3) expect(changes.length).toBe(2) }) @@ -852,6 +1345,7 @@ describe('Undo', () => { engine.undo() expectEngineToBeTheSameAs(engine, HyperFormula.buildFromArray(sheet)) + expect(engine.isThereSomethingToUndo()).toBe(false) }) @@ -904,7 +1398,7 @@ describe('Undo', () => { engine.removeColumns(0, [0, 1]) expect(() => engine.undo()).not.toThrowError() - expect(engine.getCellFormula(adr('F1'))).toEqual('=SUM(A1:C1)') + expect(engine.getCellFormula(adr('F1'))).toBe('=SUM(A1:C1)') }) }) @@ -927,7 +1421,7 @@ describe('UndoRedo', () => { }) describe('UndoRedo - #isThereSomethingToUndo', () => { - it('when there is no operation to undo', () => { + it('returns false when undo stack is empty', () => { const engine = HyperFormula.buildEmpty() expect(engine.isThereSomethingToUndo()).toBe(false) @@ -943,8 +1437,10 @@ describe('UndoRedo - #isThereSomethingToUndo', () => { it('when the undo stack has been cleared', () => { const engine = HyperFormula.buildFromArray([]) engine.removeRows(0, [1, 1]) + expect(engine.isThereSomethingToUndo()).toBe(true) engine.clearUndoStack() + expect(engine.isThereSomethingToUndo()).toBe(false) }) }) @@ -968,8 +1464,10 @@ describe('UndoRedo - #isThereSomethingToRedo', () => { const engine = HyperFormula.buildFromArray([]) engine.removeRows(0, [1, 1]) engine.undo() + expect(engine.isThereSomethingToRedo()).toBe(true) engine.clearRedoStack() + expect(engine.isThereSomethingToRedo()).toBe(false) }) }) @@ -985,11 +1483,14 @@ describe('UndoRedo - at the Operations layer', () => { const lazilyTransformingAstService = new LazilyTransformingAstService(stats) const dependencyGraph = DependencyGraph.buildEmpty(lazilyTransformingAstService, config, functionRegistry, namedExpressions, stats) const columnSearch = buildColumnSearchStrategy(dependencyGraph, config, stats) - const sheetMapping = dependencyGraph.sheetMapping const dateTimeHelper = new DateTimeHelper(config) const numberLiteralHelper = new NumberLiteralHelper(config) const cellContentParser = new CellContentParser(config, dateTimeHelper, numberLiteralHelper) - const parser = new ParserWithCaching(config, functionRegistry, sheetMapping.get) + const parser = new ParserWithCaching( + config, + functionRegistry, + dependencyGraph.sheetReferenceRegistrar.ensureSheetRegistered.bind(dependencyGraph.sheetReferenceRegistrar) + ) const arraySizePredictor = new ArraySizePredictor(config, functionRegistry) const operations = new Operations(config, dependencyGraph, columnSearch, cellContentParser, parser, stats, lazilyTransformingAstService, namedExpressions, arraySizePredictor) undoRedo = new UndoRedo(config, operations) @@ -1003,10 +1504,13 @@ describe('UndoRedo - at the Operations layer', () => { it('clearUndoStack should clear out all undo entries', () => { expect(undoRedo.isUndoStackEmpty()).toBe(true) - undoRedo.saveOperation(new AddSheetUndoEntry('Sheet 1')) - undoRedo.saveOperation(new AddSheetUndoEntry('Sheet 2')) + undoRedo.saveOperation(new AddSheetUndoEntry('Sheet 1', 1)) + undoRedo.saveOperation(new AddSheetUndoEntry('Sheet 2', 2)) + expect(undoRedo.isUndoStackEmpty()).toBe(false) + undoRedo.clearUndoStack() + expect(undoRedo.isUndoStackEmpty()).toBe(true) }) @@ -1024,7 +1528,7 @@ describe('UndoRedo - at the Operations layer', () => { }) describe('Redo - removing rows', () => { - it('works for empty row', () => { + it('re-removes empty row after undo', () => { const engine = HyperFormula.buildFromArray([ ['1'], [null], // remove @@ -1039,7 +1543,7 @@ describe('Redo - removing rows', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(snapshot)) }) - it('works for other values', () => { + it('re-removes row with values and formulas after undo', () => { const engine = HyperFormula.buildFromArray([ ['1'], ['2', '=A1'], // remove @@ -1054,7 +1558,7 @@ describe('Redo - removing rows', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(snapshot)) }) - it('works for more removal segments', () => { + it('re-removes multiple row segments after undo', () => { const engine = HyperFormula.buildFromArray([ ['1'], ['2'], @@ -1070,7 +1574,7 @@ describe('Redo - removing rows', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(snapshot)) }) - it('dummy operation should also be redoable', () => { + it('dummy removeRows operation is redoable', () => { const engine = HyperFormula.buildFromArray([ ['1'] ]) @@ -1083,7 +1587,7 @@ describe('Redo - removing rows', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(snapshot)) }) - it('clears redo stack', () => { + it('removeRows clears redo stack', () => { const engine = HyperFormula.buildFromArray([]) engine.setCellContents(adr('A1'), 42) engine.undo() @@ -1095,7 +1599,7 @@ describe('Redo - removing rows', () => { }) describe('Redo - adding rows', () => { - it('works', () => { + it('re-adds row after undo', () => { const engine = HyperFormula.buildFromArray([ ['1'], // add after that ['3'], @@ -1109,7 +1613,7 @@ describe('Redo - adding rows', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(snapshot)) }) - it('dummy operation should also be redoable', () => { + it('dummy addRows operation is redoable', () => { const engine = HyperFormula.buildFromArray([ ['1'], ]) @@ -1122,7 +1626,7 @@ describe('Redo - adding rows', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(snapshot)) }) - it('works for more addition segments', () => { + it('re-adds multiple row segments after undo', () => { const engine = HyperFormula.buildFromArray([ ['1'], ['2'], @@ -1137,7 +1641,7 @@ describe('Redo - adding rows', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(snapshot)) }) - it('clears redo stack', () => { + it('addRows clears redo stack', () => { const engine = HyperFormula.buildFromArray([]) engine.setCellContents(adr('A1'), 42) engine.undo() @@ -1149,7 +1653,7 @@ describe('Redo - adding rows', () => { }) describe('Redo - moving rows', () => { - it('works', () => { + it('re-applies row move after undo', () => { const engine = HyperFormula.buildFromArray([ ['1'], ['2'], @@ -1164,7 +1668,7 @@ describe('Redo - moving rows', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(snapshot)) }) - it('clears redo stack', () => { + it('moveRows clears redo stack', () => { const engine = HyperFormula.buildFromArray([]) engine.setCellContents(adr('A1'), 42) engine.undo() @@ -1176,7 +1680,7 @@ describe('Redo - moving rows', () => { }) describe('Redo - moving columns', () => { - it('works', () => { + it('re-applies column move after undo', () => { const engine = HyperFormula.buildFromArray([ ['1', '2', '3'], ]) @@ -1189,7 +1693,7 @@ describe('Redo - moving columns', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(snapshot)) }) - it('clears redo stack', () => { + it('moveColumns clears redo stack', () => { const engine = HyperFormula.buildFromArray([]) engine.setCellContents(adr('A1'), 42) engine.undo() @@ -1201,7 +1705,7 @@ describe('Redo - moving columns', () => { }) describe('Redo - moving cells', () => { - it('works', () => { + it('re-applies cell move after undo', () => { const engine = HyperFormula.buildFromArray([ ['42'], ['45'], @@ -1215,7 +1719,7 @@ describe('Redo - moving cells', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(snapshot)) }) - it('clears redo stack', () => { + it('moveCells clears redo stack', () => { const engine = HyperFormula.buildFromArray([]) engine.setCellContents(adr('A1'), 42) engine.undo() @@ -1227,7 +1731,7 @@ describe('Redo - moving cells', () => { }) describe('Redo - setting cell content', () => { - it('works for simple values', () => { + it('re-applies simple value change after undo', () => { const engine = HyperFormula.buildFromArray([ ['3'], ]) @@ -1240,7 +1744,7 @@ describe('Redo - setting cell content', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(snapshot)) }) - it('works for empty values', () => { + it('re-applies cell clearing after undo', () => { const engine = HyperFormula.buildFromArray([ ['3'], ]) @@ -1253,7 +1757,7 @@ describe('Redo - setting cell content', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(snapshot)) }) - it('works for formula values', () => { + it('re-applies formula value change after undo', () => { const engine = HyperFormula.buildFromArray([ ['3'], ]) @@ -1266,7 +1770,7 @@ describe('Redo - setting cell content', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(snapshot)) }) - it('setting multiple cell contents is one operation', () => { + it('redoes multiple cell contents as one operation', () => { const engine = HyperFormula.buildFromArray([ ['3', '4'], ]) @@ -1279,7 +1783,7 @@ describe('Redo - setting cell content', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(snapshot)) }) - it('clears redo stack', () => { + it('setCellContents clears redo stack', () => { const engine = HyperFormula.buildFromArray([]) engine.setCellContents(adr('A1'), 42) engine.undo() @@ -1291,7 +1795,7 @@ describe('Redo - setting cell content', () => { }) describe('Redo - removing sheet', () => { - it('works', () => { + it('re-removes sheet after undo', () => { const engine = HyperFormula.buildFromArray([ ['1'] ]) @@ -1304,7 +1808,7 @@ describe('Redo - removing sheet', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(snapshot)) }) - it('clears redo stack', () => { + it('removeSheet clears redo stack', () => { const engine = HyperFormula.buildFromArray([]) engine.setCellContents(adr('A1'), 42) engine.undo() @@ -1313,10 +1817,55 @@ describe('Redo - removing sheet', () => { expect(engine.isThereSomethingToRedo()).toBe(false) }) + + it('redo with cross-sheet formula dependencies', () => { + const engine = HyperFormula.buildFromSheets({ + Sheet1: [['=Sheet2!A1']], + Sheet2: [['42']], + }) + engine.removeSheet(1) + engine.undo() + + expect(engine.getSheetNames()).toEqual(['Sheet1', 'Sheet2']) + + engine.redo() + + expect(engine.getSheetNames()).toEqual(['Sheet1']) + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.SheetRef)) + }) + + it('removes sheet correctly after multiple undo/redo cycles', () => { + const engine = HyperFormula.buildFromSheets({ + Sheet1: [['1']], + Sheet2: [['2']], + }) + engine.removeSheet(1) + + engine.undo() + engine.redo() + engine.undo() + engine.redo() + engine.undo() + engine.redo() + + expect(engine.getSheetNames()).toEqual(['Sheet1']) + }) + + it('redo removes scoped named expressions', () => { + const engine = HyperFormula.buildFromSheets({ + Sheet1: [['=MyName']], + }) + engine.addNamedExpression('MyName', '=42', 0) + engine.removeSheet(0) + engine.undo() + engine.redo() + + expect(engine.listNamedExpressions().length).toBe(0) + }) }) describe('Redo - adding sheet', () => { - it('works for basic case', () => { + it('re-adds named sheet after undo', () => { const engine = HyperFormula.buildFromArray([]) engine.addSheet('SomeSheet') const snapshot = engine.getAllSheetsSerialized() @@ -1324,11 +1873,11 @@ describe('Redo - adding sheet', () => { engine.redo() - expect(engine.getSheetName(1)).toEqual('SomeSheet') + expect(engine.getSheetName(1)).toBe('SomeSheet') expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(snapshot)) }) - it('works for automatic naming', () => { + it('re-adds auto-named sheet after undo', () => { const engine = HyperFormula.buildFromArray([]) engine.addSheet() const snapshot = engine.getAllSheetsSerialized() @@ -1336,11 +1885,11 @@ describe('Redo - adding sheet', () => { engine.redo() - expect(engine.getSheetName(1)).toEqual('Sheet2') + expect(engine.getSheetName(1)).toBe('Sheet2') expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(snapshot)) }) - it('clears redo stack', () => { + it('addSheet clears redo stack', () => { const engine = HyperFormula.buildFromArray([]) engine.setCellContents(adr('A1'), 42) engine.undo() @@ -1349,20 +1898,42 @@ describe('Redo - adding sheet', () => { expect(engine.isThereSomethingToRedo()).toBe(false) }) + + it('restores cross-sheet reference after redo', () => { + const engine = HyperFormula.buildFromArray([['=NewSheet!A1']]) + engine.addSheet('NewSheet') + engine.undo() + engine.redo() + + expect(engine.getSheetName(1)).toBe('NewSheet') + }) + + it('builds formulas correctly with suspended evaluation during redo addSheet', () => { + const engine = HyperFormula.buildFromArray([['=NewSheet!A1']]) + engine.addSheet('NewSheet') + const snapshot = engine.getAllSheetsSerialized() + + engine.undo() + engine.suspendEvaluation() + engine.redo() + engine.resumeEvaluation() + + expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(snapshot)) + }) }) describe('Redo - renaming sheet', () => { - it('redo rename sheet', () => { + it('re-applies sheet rename after undo', () => { const engine = HyperFormula.buildFromSheets({'Sheet1': [[1]]}) engine.renameSheet(0, 'Foo') engine.undo() engine.redo() - expect(engine.getSheetName(0)).toEqual('Foo') + expect(engine.getSheetName(0)).toBe('Foo') }) - it('clears redo stack', () => { + it('renameSheet clears redo stack', () => { const engine = HyperFormula.buildFromArray([]) engine.setCellContents(adr('A1'), 42) engine.undo() @@ -1371,23 +1942,261 @@ describe('Redo - renaming sheet', () => { expect(engine.isThereSomethingToRedo()).toBe(false) }) + + it('redo rename with case change only', () => { + const engine = HyperFormula.buildFromSheets({'Sheet1': [[1]]}) + engine.renameSheet(0, 'SHEET1') + engine.undo() + engine.redo() + + expect(engine.getSheetName(0)).toBe('SHEET1') + }) + + it('redo rename preserves cell values', () => { + const engine = HyperFormula.buildFromSheets({'Sheet1': [[42], ['=A1*2']]}) + engine.renameSheet(0, 'NewName') + engine.undo() + engine.redo() + + expect(engine.getSheetName(0)).toBe('NewName') + expect(engine.getCellValue(adr('A1'))).toBe(42) + expect(engine.getCellValue(adr('A2'))).toBe(84) + }) + + it('redo rename with suspended evaluation', () => { + const engine = HyperFormula.buildFromSheets({'Sheet1': [[1]]}) + engine.renameSheet(0, 'Foo') + engine.undo() + engine.suspendEvaluation() + engine.redo() + engine.resumeEvaluation() + + expect(engine.getSheetName(0)).toBe('Foo') + }) + + it('redo rename that merged with placeholder sheet', () => { + const engine = HyperFormula.buildFromSheets({ + 'Sheet1': [['=OldName!A1', '=NewName!A1']], + 'OldName': [[42]], + }) + const sheet1Id = engine.getSheetId('Sheet1')! + const oldNameId = engine.getSheetId('OldName')! + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(42) + expect(engine.getCellValue(adr('B1', sheet1Id))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.SheetRef)) + + engine.renameSheet(oldNameId, 'NewName') + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(42) + expect(engine.getCellValue(adr('B1', sheet1Id))).toBe(42) + + engine.undo() + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(42) + expect(engine.getCellValue(adr('B1', sheet1Id))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.SheetRef)) + + engine.redo() + + expect(engine.getSheetName(oldNameId)).toBe('NewName') + expect(engine.getCellFormula(adr('A1', sheet1Id))).toBe('=NewName!A1') + expect(engine.getCellFormula(adr('B1', sheet1Id))).toBe('=NewName!A1') + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(42) + expect(engine.getCellValue(adr('B1', sheet1Id))).toBe(42) + }) + + it('redo rename with range reference updates formula (merged with placeholder sheet)', () => { + const engine = HyperFormula.buildFromSheets({ + 'Sheet1': [['=SUM(OldName!A1:B2)', '=SUM(NewName!A1:B2)']], + 'OldName': [[10, 20], [30, 40]], + }) + const sheet1Id = engine.getSheetId('Sheet1')! + const oldNameId = engine.getSheetId('OldName')! + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(100) + expect(engine.getCellValue(adr('B1', sheet1Id))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.SheetRef)) + + engine.renameSheet(oldNameId, 'NewName') + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(100) + expect(engine.getCellValue(adr('B1', sheet1Id))).toBe(100) + + engine.undo() + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(100) + expect(engine.getCellValue(adr('B1', sheet1Id))).toEqualError(detailedError(ErrorType.REF, ErrorMessage.SheetRef)) + + engine.redo() + + expect(engine.getCellFormula(adr('A1', sheet1Id))).toBe('=SUM(NewName!A1:B2)') + expect(engine.getCellFormula(adr('B1', sheet1Id))).toBe('=SUM(NewName!A1:B2)') + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(100) + expect(engine.getCellValue(adr('B1', sheet1Id))).toBe(100) + }) + + it('redo multiple sequential renames', () => { + const engine = HyperFormula.buildFromSheets({'Sheet1': [[1]]}) + engine.renameSheet(0, 'Name1') + engine.renameSheet(0, 'Name2') + engine.renameSheet(0, 'Name3') + engine.undo() + engine.undo() + engine.undo() + engine.redo() + + expect(engine.getSheetName(0)).toBe('Name1') + + engine.redo() + + expect(engine.getSheetName(0)).toBe('Name2') + + engine.redo() + + expect(engine.getSheetName(0)).toBe('Name3') + }) + + it('redo rename combined with cell content changes', () => { + const engine = HyperFormula.buildFromSheets({'Sheet1': [[1]]}) + engine.setCellContents(adr('A1'), 10) + engine.renameSheet(0, 'NewName') + engine.setCellContents(adr('A1'), 100) + engine.undo() + engine.undo() + engine.undo() + engine.redo() + + expect(engine.getCellValue(adr('A1'))).toBe(10) + + engine.redo() + + expect(engine.getSheetName(0)).toBe('NewName') + + engine.redo() + + expect(engine.getCellValue(adr('A1'))).toBe(100) + }) + + it('redo rename in batch mode', () => { + const engine = HyperFormula.buildFromSheets({'Sheet1': [[1]], 'Sheet2': [[2]]}) + engine.batch(() => { + engine.renameSheet(0, 'NewName1') + engine.renameSheet(1, 'NewName2') + }) + engine.undo() + engine.redo() + + expect(engine.getSheetName(0)).toBe('NewName1') + expect(engine.getSheetName(1)).toBe('NewName2') + }) + + it('redo rename with chained dependencies across sheets', () => { + const engine = HyperFormula.buildFromSheets({ + 'Sheet1': [['=Sheet2!A1+2']], + 'Sheet2': [['=NewName!A1*2']], + 'OldName': [[42]], + }) + const sheet1Id = engine.getSheetId('Sheet1')! + const sheet2Id = engine.getSheetId('Sheet2')! + const oldNameId = engine.getSheetId('OldName')! + + engine.renameSheet(oldNameId, 'NewName') + engine.undo() + engine.redo() + + expect(engine.getCellValue(adr('A1', sheet2Id))).toBe(84) + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(86) + }) + + it('redo rename with named expressions referencing placeholder', () => { + const engine = HyperFormula.buildFromSheets({ + 'Sheet1': [['=MyValue']], + 'OldName': [[99]], + }, {}, [ + { name: 'MyValue', expression: '=NewName!$A$1' } + ]) + const sheet1Id = engine.getSheetId('Sheet1')! + const oldNameId = engine.getSheetId('OldName')! + engine.renameSheet(oldNameId, 'NewName') + engine.undo() + engine.redo() + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(99) + }) + + it('redo rename after undo of combined operations', () => { + const engine = HyperFormula.buildFromSheets({ + 'Sheet1': [['=NewName!A1']], + 'OldName': [[42]], + }) + const sheet1Id = engine.getSheetId('Sheet1')! + const oldNameId = engine.getSheetId('OldName')! + engine.renameSheet(oldNameId, 'NewName') + engine.setCellContents(adr('A1', oldNameId), 100) + engine.undo() + engine.undo() + engine.redo() + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(42) + + engine.redo() + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(100) + }) + + it('redo rename with multiple cells referencing placeholder', () => { + const engine = HyperFormula.buildFromSheets({ + 'Sheet1': [['=NewName!A1', '=NewName!B1']], + 'Sheet2': [['=NewName!A1+10', '=NewName!B1+20']], + 'OldName': [[5, 7]], + }) + const sheet1Id = engine.getSheetId('Sheet1')! + const sheet2Id = engine.getSheetId('Sheet2')! + const oldNameId = engine.getSheetId('OldName')! + engine.renameSheet(oldNameId, 'NewName') + engine.undo() + engine.redo() + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(5) + expect(engine.getCellValue(adr('B1', sheet1Id))).toBe(7) + expect(engine.getCellValue(adr('A1', sheet2Id))).toBe(15) + expect(engine.getCellValue(adr('B1', sheet2Id))).toBe(27) + }) + + it('redo rename with column and row ranges', () => { + const engine = HyperFormula.buildFromSheets({ + 'Sheet1': [ + ['=SUM(NewName!A:A)'], + ['=SUM(NewName!1:2)'], + ], + 'OldName': [ + [1, 2], + [3, 4], + ], + }) + const sheet1Id = engine.getSheetId('Sheet1')! + const oldNameId = engine.getSheetId('OldName')! + engine.renameSheet(oldNameId, 'NewName') + engine.undo() + engine.redo() + + expect(engine.getCellValue(adr('A1', sheet1Id))).toBe(4) + expect(engine.getCellValue(adr('A2', sheet1Id))).toBe(10) + }) }) describe('Redo - clearing sheet', () => { - it('works', () => { + it('re-clears sheet after undo', () => { const engine = HyperFormula.buildFromArray([ ['1'] ]) engine.clearSheet(0) const snapshot = engine.getAllSheetsSerialized() engine.undo() - engine.redo() expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(snapshot)) }) - it('clears redo stack', () => { + it('clearSheet clears redo stack', () => { const engine = HyperFormula.buildFromArray([]) engine.setCellContents(adr('A1'), 42) engine.undo() @@ -1399,7 +2208,7 @@ describe('Redo - clearing sheet', () => { }) describe('Redo - adding columns', () => { - it('works', () => { + it('re-adds column after undo', () => { const engine = HyperFormula.buildFromArray([ ['1', '3'], ]) @@ -1412,7 +2221,7 @@ describe('Redo - adding columns', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(snapshot)) }) - it('dummy operation should also be redoable', () => { + it('dummy addColumns operation is redoable', () => { const engine = HyperFormula.buildFromArray([ ['1'], ]) @@ -1425,7 +2234,7 @@ describe('Redo - adding columns', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(snapshot)) }) - it('works for more addition segments', () => { + it('re-adds multiple column segments after undo', () => { const engine = HyperFormula.buildFromArray([ ['1', '2', '3'], ]) @@ -1438,7 +2247,7 @@ describe('Redo - adding columns', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(snapshot)) }) - it('clears redo stack', () => { + it('addColumns clears redo stack', () => { const engine = HyperFormula.buildFromArray([]) engine.setCellContents(adr('A1'), 42) engine.undo() @@ -1450,7 +2259,7 @@ describe('Redo - adding columns', () => { }) describe('Redo - removing column', () => { - it('works for empty column', () => { + it('re-removes empty column after undo', () => { const engine = HyperFormula.buildFromArray([ ['1', null, '3'], ]) @@ -1463,7 +2272,7 @@ describe('Redo - removing column', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(snapshot)) }) - it('works for other values', () => { + it('re-removes column with values and formulas after undo', () => { const engine = HyperFormula.buildFromArray([ ['1', '2'], ['=B1'] @@ -1477,7 +2286,7 @@ describe('Redo - removing column', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(snapshot)) }) - it('works for more removal segments', () => { + it('re-removes multiple column segments after undo', () => { const engine = HyperFormula.buildFromArray([ ['1', '2', '3', '4'], ]) @@ -1490,7 +2299,7 @@ describe('Redo - removing column', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(snapshot)) }) - it('dummy operation should also be redoable', () => { + it('dummy removeColumns operation is redoable', () => { const engine = HyperFormula.buildFromArray([ ['1'] ]) @@ -1503,7 +2312,7 @@ describe('Redo - removing column', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(snapshot)) }) - it('clears redo stack', () => { + it('removeColumns clears redo stack', () => { const engine = HyperFormula.buildFromArray([]) engine.setCellContents(adr('A1'), 42) engine.undo() @@ -1515,7 +2324,7 @@ describe('Redo - removing column', () => { }) describe('Redo - cut-paste', () => { - it('works', () => { + it('re-applies cut-paste after undo', () => { const engine = HyperFormula.buildFromArray([ ['foo'], ['bar'], @@ -1553,7 +2362,7 @@ describe('Redo - cut-paste', () => { }) describe('Redo - copy-paste', () => { - it('works', () => { + it('re-applies copy-paste after undo', () => { const engine = HyperFormula.buildFromArray([ ['foo', 'baz'], ['bar', 'faz'], @@ -1591,7 +2400,7 @@ describe('Redo - copy-paste', () => { }) describe('Redo - setting sheet contents', () => { - it('works for basic case', () => { + it('re-applies sheet content change after undo', () => { const engine = HyperFormula.buildFromArray([['13']]) engine.setSheetContent(0, [['42']]) const snapshot = engine.getAllSheetsSerialized() @@ -1602,7 +2411,7 @@ describe('Redo - setting sheet contents', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(snapshot)) }) - it('also clears sheet when redoing', () => { + it('clears extra cells when redoing setSheetContent', () => { const engine = HyperFormula.buildFromArray([['13', '14']]) engine.setSheetContent(0, [['42']]) const snapshot = engine.getAllSheetsSerialized() @@ -1613,7 +2422,7 @@ describe('Redo - setting sheet contents', () => { expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(snapshot)) }) - it('clears redo stack', () => { + it('setSheetContent clears redo stack', () => { const engine = HyperFormula.buildFromArray([]) engine.setCellContents(adr('A1'), 42) engine.undo() @@ -1625,7 +2434,7 @@ describe('Redo - setting sheet contents', () => { }) describe('Redo - add named expression', () => { - it('works', () => { + it('re-adds named expression after undo', () => { const engine = HyperFormula.buildFromArray([ ['=foo'] ]) @@ -1635,11 +2444,11 @@ describe('Redo - add named expression', () => { engine.redo() - expect(engine.listNamedExpressions().length).toEqual(1) - expect(engine.getCellValue(adr('A1'))).toEqual('foo') + expect(engine.listNamedExpressions().length).toBe(1) + expect(engine.getCellValue(adr('A1'))).toBe('foo') }) - it('clears redo stack', () => { + it('addNamedExpression clears redo stack', () => { const engine = HyperFormula.buildFromArray([]) engine.setCellContents(adr('A1'), 42) engine.undo() @@ -1651,7 +2460,7 @@ describe('Redo - add named expression', () => { }) describe('Redo - remove named expression', () => { - it('works', () => { + it('re-removes named expression after undo', () => { const engine = HyperFormula.buildFromArray([ ['=foo'] ]) @@ -1662,11 +2471,11 @@ describe('Redo - remove named expression', () => { engine.redo() - expect(engine.listNamedExpressions().length).toEqual(0) + expect(engine.listNamedExpressions().length).toBe(0) expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.NAME, ErrorMessage.NamedExpressionName('foo'))) }) - it('clears redo stack', () => { + it('removeNamedExpression clears redo stack', () => { const engine = HyperFormula.buildFromArray([]) engine.addNamedExpression('foo', 'foo') engine.setCellContents(adr('A1'), 42) @@ -1679,7 +2488,7 @@ describe('Redo - remove named expression', () => { }) describe('Redo - change named expression', () => { - it('works', () => { + it('re-applies expression change after undo', () => { const engine = HyperFormula.buildFromArray([ ['=foo'] ]) @@ -1690,11 +2499,11 @@ describe('Redo - change named expression', () => { engine.redo() - expect(engine.listNamedExpressions().length).toEqual(1) - expect(engine.getCellValue(adr('A1'))).toEqual('bar') + expect(engine.listNamedExpressions().length).toBe(1) + expect(engine.getCellValue(adr('A1'))).toBe('bar') }) - it('clears redo stack', () => { + it('changeNamedExpression clears redo stack', () => { const engine = HyperFormula.buildFromArray([]) engine.addNamedExpression('foo', 'foo') engine.setCellContents(adr('A1'), 42) @@ -1721,6 +2530,7 @@ describe('Redo - batch mode', () => { engine.redo() expectEngineToBeTheSameAs(engine, HyperFormula.buildFromSheets(snapshot)) + expect(engine.isThereSomethingToRedo()).toBe(false) }) @@ -1742,7 +2552,7 @@ describe('Redo - batch mode', () => { }) describe('Redo', () => { - it('when there is no operation to redo', () => { + it('throws error when redo stack is empty', () => { const engine = HyperFormula.buildEmpty() expect(() => { @@ -1759,7 +2569,7 @@ describe('Redo', () => { const changes = engine.redo() - expect(engine.getCellValue(adr('B1'))).toEqual(100) + expect(engine.getCellValue(adr('B1'))).toBe(100) expect(changes.length).toBe(2) }) }) diff --git a/test/unit/update-config.spec.ts b/test/unit/update-config.spec.ts index 3ae075155..15e0925e2 100644 --- a/test/unit/update-config.spec.ts +++ b/test/unit/update-config.spec.ts @@ -112,4 +112,14 @@ describe('update config', () => { engine.updateConfig({ useColumnIndex: false, functionArgSeparator: ',', undoLimit: 20 }) expect(rebuildEngineSpy).not.toHaveBeenCalled() }) + + it('doesn\'t throw after adding named expression (#1194)', () => { + const hf = HyperFormula.buildFromArray([['=42']], { + licenseKey: 'gpl-v3' + }) + + + hf.addNamedExpression('ABC', '=Sheet1!$A$1') + expect(() => hf.updateConfig({ maxRows: 101 })).not.toThrow() + }) })