Skip to content

Commit b3b718c

Browse files
Merge pull request #241 from beakerandjake/20-240-allow-running-on-in-progress-aocs
20 240 allow running on in progress aocs
2 parents 7946515 + ccc9d0f commit b3b718c

File tree

9 files changed

+553
-67
lines changed

9 files changed

+553
-67
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## [Unreleased]
9+
### Added
10+
- Added `import` command which stores correct answers to problems submitted outside of the cli. ([#20](https://github.com/beakerandjake/advent-of-code-runner/issues/20) and [#240](https://github.com/beakerandjake/advent-of-code-runner/issues/240))
911

1012
## [1.6.1] - 2023-12-01
1113
### Fixed

README.md

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ A Node.Js CLI solution generator and runner for [advent of code](https://advento
88
[![codecov](https://codecov.io/gh/beakerandjake/advent-of-code-runner/branch/main/graph/badge.svg?token=MN3ZFKM7SX)](https://codecov.io/gh/beakerandjake/advent-of-code-runner)
99

1010
## :mrs_claus: Features
11-
- Quickly and easily scaffolds an empty directory, creating all required solution files (similar to create-react-app).
11+
- Quickly and easily scaffolds an empty directory, creating all required solution files.
1212
- Runs your solutions (both sync and async) and measures performance.
1313
- Downloads and caches puzzle input files.
1414
- Submits answers to advent of code.
@@ -26,6 +26,7 @@ A Node.Js CLI solution generator and runner for [advent of code](https://advento
2626
- [submit](#submit-day-level)
2727
- [stats](#stats---save)
2828
- [auth](#auth)
29+
- [import](#import-day-level-answer---no-confirm)
2930
- [Solution Files](#snowman-solution-files)
3031
- [Caching](#cloud_with_snow-caching)
3132
- [Misc File Information](#gift-misc-file-information)
@@ -168,6 +169,40 @@ Creates or updates the `.env` file with your advent of code authentication token
168169
npm run auth
169170
```
170171

172+
### `import <day> <level> <answer> [--no-confirm]`
173+
Stores the correct answer to a puzzle which was solved outside of advent-of-code-runner. This allows you to use advent-of-code-runner even if you already submitted answers with different tools. Before this command you couldn't let advent-of-code-runner know that you had solved a puzzle already. Once you've imported an answer you will probably want to update the corresponding solution file to add your existing code.
174+
175+
**Note**: All imported puzzles are set to a runtime of 999 seconds. After importing an answer, run the `solve` command to update the puzzles runtime to a real value.
176+
177+
Running without the `--no-confirm` flag is the default behavior. You will be asked to confirm when importing any puzzles which already exist in your `aocr-data.json` file.
178+
179+
```
180+
npm run import <day> <level> <answer>
181+
```
182+
183+
Running with the `--no-confirm` flag will skip any confirmation for overwriting existing data.
184+
185+
```
186+
npm run import --no-confirm <day> <level> <answer>
187+
```
188+
189+
#### Handling different answer types
190+
191+
Answers are stored as strings, so you usually don't need to wrap your answer with quotes. However there are certain answers which will need special care:
192+
193+
- An answer with whitespace: wrap in quotes ```npm run import 1 1 'an answer with spaces' ```
194+
- An answer which is a negative number: use `--` before the args ```npm run import -- 1 1 -12345 ```
195+
196+
Examples:
197+
- Import answer 'asdf' for day 10 level 1: `npm run import 10 1 asdf`
198+
- Import answer 999100 for day 7 level 2: `npm run import 7 2 999100`
199+
- Import answer -4000 for day 1 level 1 and skip confirmation: `npm run import --no-confirm -- 1 1 -4000`
200+
201+
#### Bulk importing
202+
203+
If you have already solved some puzzles, it would be tedious to manually run the `import` command a bunch of times. The wiki has a [guide](https://github.com/beakerandjake/advent-of-code-runner/wiki/Bulk-import-of-in-progress-advent-calendar) for bulk importing puzzle answers using basic linux command line tools.
204+
205+
171206
### `help`
172207
Outputs the help text for the cli
173208
```

src/arguments.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { Argument, InvalidArgumentError } from 'commander';
2+
import { getConfigValue } from './config.js';
3+
4+
/**
5+
* Returns a function which parses the string value as an integer.
6+
* Then compares the parsed integer value to an array of choices.
7+
* The returned function throws an InvalidArgumentError if the value is not included in the choices.
8+
* @private
9+
* @param {number[]} choices - The valid options to choose from
10+
* @throws {RangeError} - The parsed integer value was not included in the choices.
11+
*/
12+
export const intParser = (choices) => (value) => {
13+
const parsed = Number.parseInt(value, 10);
14+
if (Number.isNaN(parsed)) {
15+
throw new InvalidArgumentError('Value could not be parsed as an integer.');
16+
}
17+
if (!choices.includes(parsed)) {
18+
const min = Math.min(...choices);
19+
const max = Math.max(...choices);
20+
throw new InvalidArgumentError(`Value must be between ${min} and ${max}.`);
21+
}
22+
return parsed;
23+
};
24+
25+
/**
26+
* Wraps the argument name with brackets indicating if it's required or not.
27+
* @private
28+
* @param {string} name
29+
* @param {boolean} required
30+
*/
31+
export const decorateName = (name, required) =>
32+
required ? `<${name}>` : `[${name}]`;
33+
34+
/**
35+
* Returns a commander.js Argument for the day of a puzzle.
36+
* @param {boolean} required
37+
* @returns {Argument}
38+
*/
39+
export const getDayArg = (required) =>
40+
new Argument(
41+
decorateName('day', required),
42+
'The day of the puzzle to solve (1-25).'
43+
).argParser(intParser(getConfigValue('aoc.validation.days')));
44+
45+
/**
46+
* Returns a commander.js Argument for the level of a puzzle.
47+
*/
48+
export const getLevelArg = (required) =>
49+
new Argument(
50+
decorateName('level', required),
51+
`The the level of the puzzle to solve (1 or 2).`
52+
).argParser(intParser(getConfigValue('aoc.validation.levels')));

src/commands/import.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { confirm } from '@inquirer/prompts';
2+
import { DirectoryNotInitializedError } from '../errors/cliErrors.js';
3+
import {
4+
PuzzleInFutureError,
5+
PuzzleLevelInvalidError,
6+
} from '../errors/puzzleErrors.js';
7+
import { festiveStyle } from '../festive.js';
8+
import { logger } from '../logger.js';
9+
import { getYear } from '../persistence/metaRepository.js';
10+
import {
11+
addOrEditPuzzle,
12+
createPuzzle,
13+
findPuzzle,
14+
} from '../persistence/puzzleRepository.js';
15+
import { dataFileExists } from '../validation/userFilesExist.js';
16+
import {
17+
puzzleHasLevel,
18+
puzzleIsInFuture,
19+
} from '../validation/validatePuzzle.js';
20+
21+
/**
22+
* Attempts to confirm with the user if an entry for the puzzle exists in their data file.
23+
*/
24+
const userConfirmed = async (year, day, level) => {
25+
// nothing to confirm if puzzle does not exist.
26+
if (!(await findPuzzle(year, day, level))) {
27+
logger.debug('not confirming because no data for puzzle exists');
28+
return true;
29+
}
30+
31+
logger.debug('data for puzzle exists, confirming overwrite with user');
32+
33+
// confirm with user that they want to overwrite the existing puzzle data.
34+
return confirm({
35+
message: festiveStyle(
36+
'An entry exists for this puzzle in your data file, do you want to overwrite it?'
37+
),
38+
default: false,
39+
});
40+
};
41+
42+
/**
43+
* Stores the correct answer to the puzzle. Used to inform advent-of-code-runner about problems
44+
* which were solved outside of their advent-of-code-runner repository.
45+
*/
46+
export const importAction = async (day, level, answer, options) => {
47+
logger.debug('starting import action:', { day, level, answer, options });
48+
49+
// can't import answer if repository has not been initialized.
50+
if (!(await dataFileExists())) {
51+
throw new DirectoryNotInitializedError();
52+
}
53+
54+
const year = await getYear();
55+
56+
// can't import answer if puzzle doesn't have level.
57+
if (!puzzleHasLevel(year, day, level)) {
58+
throw new PuzzleLevelInvalidError(day, level);
59+
}
60+
61+
// can't import answer if puzzle isn't unlocked yet.
62+
if (puzzleIsInFuture(year, day)) {
63+
throw new PuzzleInFutureError(day);
64+
}
65+
66+
// bail if user does not confirm action.
67+
if (options.confirm && !(await userConfirmed(year, day, level))) {
68+
logger.debug('user did not confirm the import action');
69+
return;
70+
}
71+
72+
const puzzleData = {
73+
...createPuzzle(year, day, level),
74+
// set fastest runtime to a high value so it can be overwritten when user runs solve.
75+
fastestRuntimeNs: 99.9e10,
76+
correctAnswer: answer,
77+
};
78+
79+
logger.debug('created puzzle data for imported puzzle', puzzleData);
80+
81+
// save the puzzle data to the users data file.
82+
await addOrEditPuzzle(puzzleData);
83+
84+
logger.festive(
85+
"Successfully imported puzzle answer. Run the 'solve' command to update runtime statistics."
86+
);
87+
};

src/errors/puzzleErrors.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export class PuzzleInFutureError extends UserError {
1717
// ``
1818
constructor(day, ...args) {
1919
super(
20-
`You cannot attempt this puzzle because it is not unlocked yet, check back on December ${day} at midnight EST`,
20+
`This puzzle is not unlocked yet, check back on December ${day} at midnight EST`,
2121
...args
2222
);
2323
this.name = 'PuzzleInFutureError';

src/main.js

Lines changed: 31 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,16 @@
11
#!/usr/bin/env node
2-
import { Argument, Command, InvalidArgumentError } from 'commander';
2+
import { Command } from 'commander';
33
import { authAction } from './commands/auth.js';
44
import { initAction } from './commands/init.js';
55
import { solveAction } from './commands/solve.js';
66
import { statsAction } from './commands/stats.js';
77
import { submitAction } from './commands/submit.js';
8+
import { importAction } from './commands/import.js';
89
import { getConfigValue } from './config.js';
910
import { handleError } from './errorHandler.js';
11+
import { getDayArg, getLevelArg } from './arguments.js';
1012
import { printFestiveTitle } from './festive.js';
1113

12-
/**
13-
* Returns a function which parses the string value as an integer.
14-
* Then compares the parsed integer value to an array of choices.
15-
* The returned function throws an InvalidArgumentError if the value is not included in the choices.
16-
* @param {number[]} choices - The valid options to choose from
17-
* @throws {RangeError} - The parsed integer value was not included in the choices.
18-
*/
19-
export const intParser = (choices) => (value) => {
20-
const parsed = Number.parseInt(value, 10);
21-
if (Number.isNaN(parsed)) {
22-
throw new InvalidArgumentError('Value could not be parsed as an integer.');
23-
}
24-
if (!choices.includes(parsed)) {
25-
const min = Math.min(...choices);
26-
const max = Math.max(...choices);
27-
throw new InvalidArgumentError(`Value must be between ${min} and ${max}.`);
28-
}
29-
return parsed;
30-
};
31-
3214
try {
3315
const program = new Command();
3416

@@ -55,15 +37,30 @@ try {
5537
.description('Scaffold an empty directory.')
5638
.action(initAction);
5739

58-
const dayArgument = new Argument(
59-
'[day]',
60-
'The day of the puzzle to solve (1-25).'
61-
).argParser(intParser(getConfigValue('aoc.validation.days')));
62-
63-
const levelArgument = new Argument(
64-
'[level]',
65-
`The the level of the puzzle to solve (1 or 2).`
66-
).argParser(intParser(getConfigValue('aoc.validation.levels')));
40+
// add the import command
41+
program
42+
.command('import')
43+
.description(
44+
'Store the correct answer to a puzzle solved outside of this project.'
45+
)
46+
.option(
47+
'--no-confirm',
48+
'Does not ask for confirmation if puzzle already exists in data file'
49+
)
50+
.addArgument(getDayArg(true))
51+
.addArgument(getLevelArg(true))
52+
.argument('<answer>', 'The correct answer to the puzzle')
53+
.addHelpText(
54+
'after',
55+
[
56+
'',
57+
'Example Calls:',
58+
` import 10 1 123456 Stores correct answer "123456" for day 10 level 1`,
59+
` import 5 2 'hello world' Stores correct answer "hello world" for day 5 level 2`,
60+
` import -- 1 1 -123456 Stores correct answer "-123456" for day 1 level 1 (note the -- to add a negative number as an answer)`,
61+
].join('\n')
62+
)
63+
.action(importAction);
6764

6865
// add the solve command
6966
program
@@ -81,8 +78,8 @@ try {
8178
' solve [day] [level] Solves the puzzle for the specified day and level',
8279
].join('\n')
8380
)
84-
.addArgument(dayArgument)
85-
.addArgument(levelArgument)
81+
.addArgument(getDayArg(false))
82+
.addArgument(getLevelArg(false))
8683
.action(solveAction);
8784

8885
// add the submit command
@@ -101,8 +98,8 @@ try {
10198
' submit [day] [level] Submits the puzzle for the specified day and level',
10299
].join('\n')
103100
)
104-
.addArgument(dayArgument)
105-
.addArgument(levelArgument)
101+
.addArgument(getDayArg(false))
102+
.addArgument(getLevelArg(false))
106103
.action(submitAction);
107104

108105
// add the stats command

0 commit comments

Comments
 (0)