Created
December 6, 2024 18:53
-
-
Save WomB0ComB0/882dd1981e7dd799ef41661f10fd2c48 to your computer and use it in GitHub Desktop.
Utilities for https://github.com/WomB0ComB0/leetcode-solutions, utilizing the LeetCode GraphQL endpoint
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { promises as fs } from 'fs'; | |
import path from 'path'; | |
/** | |
* Utility class for cleaning and standardizing filenames across programming language directories. | |
* Handles conversion to kebab-case, roman numeral suffixes, and ensures unique filenames. | |
*/ | |
class FilenameCleaner { | |
/** List of programming language directories to process */ | |
private static readonly PROGRAMMING_DIRECTORIES = [ | |
'python', 'typescript', 'javascript', 'java', 'cpp', 'c', | |
'csharp', 'dart', 'php', 'go', 'rust', | |
'ruby', 'swift', 'kotlin' | |
]; | |
/** | |
* Converts a string to kebab-case format. | |
* @param str - The input string to convert | |
* @returns The kebab-cased string with only alphanumeric characters and hyphens | |
*/ | |
private static toKebabCase(str: string): string { | |
return str | |
.replace(/([a-z])([A-Z])/g, '$1-$2') // Convert camelCase to kebab-case | |
.replace(/\s+/g, '-') // Replace spaces with hyphens | |
.replace(/[^a-zA-Z0-9-]/g, '') // Remove non-alphanumeric characters except hyphens | |
.toLowerCase(); | |
} | |
/** | |
* Checks if a string is already in valid kebab-case format. | |
* @param str - The string to validate | |
* @returns True if the string is valid kebab-case, false otherwise | |
*/ | |
private static isKebabCase(str: string): boolean { | |
return /^[a-z0-9]+(-[a-z0-9]+)*$/.test(str); | |
} | |
/** | |
* Converts a number to its Roman numeral representation. | |
* @param num - The number to convert (must be positive) | |
* @returns The Roman numeral string, or empty string if num <= 0 | |
*/ | |
private static toRoman(num: number): string { | |
if (num <= 0) return ''; | |
const romanNumerals: [string, number][] = [ | |
['M', 1000], ['CM', 900], ['D', 500], ['CD', 400], | |
['C', 100], ['XC', 90], ['L', 50], ['XL', 40], | |
['X', 10], ['IX', 9], ['V', 5], ['IV', 4], ['I', 1] | |
]; | |
return romanNumerals.reduce((result, [roman, value]) => { | |
while (num >= value) { | |
result += roman; | |
num -= value; | |
} | |
return result; | |
}, ''); | |
} | |
/** | |
* Processes a single directory, cleaning and standardizing all filenames within it. | |
* - Converts filenames to kebab-case | |
* - Converts numeric suffixes to Roman numerals | |
* - Ensures unique filenames by adding numbered suffixes if needed | |
* - Preserves file extensions | |
* | |
* @param directory - The full path to the directory to process | |
* @throws Error if directory operations fail | |
*/ | |
static async cleanFilenames(directory: string): Promise<void> { | |
try { | |
const entries = await fs.readdir(directory, { withFileTypes: true }); | |
const seenNames = new Set<string>(); | |
for (const entry of entries) { | |
if (!entry.isFile()) continue; | |
const file = entry.name; | |
const ext = path.extname(file); | |
const baseName = path.basename(file, ext); | |
const parts = baseName.split('.'); | |
const namePart = parts.slice(1).join('.'); | |
const kebabName = this.toKebabCase(namePart); | |
if (this.isKebabCase(baseName)) { | |
console.log(`Skipping already kebab-case file: ${file}`); | |
continue; | |
} | |
const postfixMatch = kebabName.match(/-(\d+)$/); | |
let finalName = kebabName; | |
if (postfixMatch) { | |
const number = parseInt(postfixMatch[1], 10); | |
if (number > 0) { | |
const roman = this.toRoman(number); | |
finalName = kebabName.replace(/-\d+$/, `-${roman}`); | |
} | |
} | |
let uniqueName = finalName; | |
let counter = 1; | |
while (seenNames.has(uniqueName) || await fs.access(path.join(directory, `${uniqueName}${ext}`)).then(() => true).catch(() => false)) { | |
uniqueName = `${finalName}-duplicate-${counter}`; | |
counter++; | |
} | |
seenNames.add(uniqueName); | |
const oldFilePath = path.join(directory, file); | |
const newFilePath = path.join(directory, `${uniqueName}${ext}`); | |
await fs.rename(oldFilePath, newFilePath); | |
console.log(`Renamed: ${file} → ${uniqueName}${ext}`); | |
} | |
} catch (error) { | |
console.error(`Error processing directory ${directory}:`, error); | |
} | |
} | |
/** | |
* Main entry point for the filename cleaning process. | |
* Processes all configured programming language directories in parallel. | |
* | |
* @param baseDirectory - The root directory containing language subdirectories (defaults to current working directory) | |
* @throws Error if the overall process fails | |
*/ | |
static async run(baseDirectory: string = process.cwd()): Promise<void> { | |
console.time('Filename Cleaning Duration'); | |
try { | |
await Promise.all( | |
this.PROGRAMMING_DIRECTORIES.map(async (dir) => { | |
const fullPath = path.join(baseDirectory, dir); | |
console.log(`Processing directory: ${dir}`); | |
await this.cleanFilenames(fullPath); | |
}) | |
); | |
} catch (error) { | |
console.error('Unexpected error in filename cleaning process:', error); | |
} finally { | |
console.timeEnd('Filename Cleaning Duration'); | |
} | |
} | |
} | |
if (require.main === module) { | |
FilenameCleaner.run().catch(console.error); | |
} | |
export default FilenameCleaner; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @fileoverview Fetches and processes the daily LeetCode challenge, creating solution files across multiple languages | |
*/ | |
import { promises as fs } from 'fs'; | |
import axios from 'axios'; | |
import path from 'path'; | |
/** | |
* Converts a string to kebab-case format | |
* @param {string} str - The input string to convert | |
* @returns {Promise<string>} The kebab-cased string with only lowercase letters, numbers and hyphens | |
*/ | |
async function toKebabCase(str: string): Promise<string> { | |
return str.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''); | |
} | |
/** | |
* Fetches the daily LeetCode challenge and creates solution files in multiple languages | |
* @throws {Error} If the API request fails or file operations fail | |
*/ | |
async function getDailyLeetcodeChallenge() { | |
const url = 'https://leetcode.com/graphql'; | |
const query = { | |
query: ` | |
query questionOfToday { | |
activeDailyCodingChallengeQuestion { | |
question { | |
title | |
difficulty | |
topicTags { | |
name | |
} | |
} | |
} | |
} | |
`, | |
}; | |
console.log('Fetching daily LeetCode challenge...'); | |
try { | |
const response = await axios.post(url, query, { | |
headers: { | |
'Content-Type': 'application/json', | |
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', | |
}, | |
}); | |
const question = response.data.data.activeDailyCodingChallengeQuestion.question; | |
const title = question.title; | |
const difficulty = question.difficulty; | |
const language = question.topicTags[0]?.name || 'unknown'; | |
console.log('Challenge details:'); | |
console.log(`Title: ${title}`); | |
console.log(`Difficulty: ${difficulty}`); | |
console.log(`Primary topic: ${language}`); | |
const kebabTitle = await toKebabCase(title); | |
/** | |
* Mapping of programming languages to their file extensions | |
* @type {Object.<string, string>} | |
*/ | |
const extensions: { [key: string]: string } = { | |
python: 'py', | |
typescript: 'ts', | |
javascript: 'js', | |
java: 'java', | |
cpp: 'cpp', | |
c: 'c', | |
csharp: 'cs', | |
dart: 'dart', | |
php: 'php', | |
go: 'go', | |
rust: 'rs', | |
ruby: 'rb', | |
swift: 'swift', | |
kotlin: 'kt', | |
}; | |
const existingFiles: string[] = []; | |
const createdFiles: string[] = []; | |
for (const [key, value] of Object.entries(extensions)) { | |
const dirPath = path.join(key, difficulty.toLowerCase()); | |
const filePath = path.join(dirPath, `${kebabTitle}.${value}`); | |
try { | |
await fs.access(filePath); | |
existingFiles.push(filePath); | |
console.log(`File already exists: ${filePath}`); | |
} catch { | |
await fs.mkdir(dirPath, { recursive: true }); | |
await fs.writeFile(filePath, ''); | |
createdFiles.push(filePath); | |
console.log(`Created file: ${filePath}`); | |
} | |
} | |
if (existingFiles.length > 0) { | |
console.log('\nExisting Challenge Files:'); | |
existingFiles.forEach(file => console.log(file)); | |
} | |
if (createdFiles.length > 0) { | |
console.log('\nNewly Created Challenge Files:'); | |
createdFiles.forEach(file => console.log(file)); | |
} | |
console.log(`Successfully processed challenge files`); | |
} catch (error) { | |
console.error('Error: Failed to fetch daily LeetCode challenge', error); | |
} | |
} | |
/** | |
* Main class to handle daily LeetCode challenge operations | |
* @class | |
*/ | |
class Daily { | |
/** | |
* Executes the daily LeetCode challenge processing | |
* @static | |
* @async | |
* @returns {Promise<void>} | |
*/ | |
static async run() { | |
await getDailyLeetcodeChallenge(); | |
} | |
} | |
// Execute if this is the main module | |
if (require.main === module) { | |
Daily.run().catch(console.error); | |
} | |
export default Daily; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @fileoverview Utility for sorting LeetCode problem files into difficulty-based directories. | |
* Fetches problem metadata from LeetCode's GraphQL API and organizes local solution files. | |
*/ | |
import axios from 'axios'; | |
import { promises as fs } from 'fs'; | |
import path from 'path'; | |
/** | |
* Represents a LeetCode problem with essential metadata. | |
* @interface LeetCodeProblem | |
* @property {string} titleSlug - The URL-friendly title of the problem | |
* @property {string} difficulty - The difficulty level of the problem (easy/medium/hard) | |
*/ | |
interface LeetCodeProblem { | |
titleSlug: string; | |
difficulty: string; | |
} | |
/** | |
* Handles fetching LeetCode problem metadata and sorting solution files by difficulty. | |
* @class ProblemSorter | |
*/ | |
class ProblemSorter { | |
/** GraphQL endpoint for LeetCode's API */ | |
private static readonly LEETCODE_GRAPHQL_URL = 'https://leetcode.com/graphql'; | |
/** Supported programming language directories to process */ | |
private static readonly PROGRAMMING_DIRECTORIES = [ | |
'python', 'typescript', 'javascript', 'java', 'cpp', 'c', 'c#', | |
'c++', 'dart', 'php', 'csharp', 'go', 'rust', 'ruby', 'swift', 'kotlin' | |
]; | |
/** | |
* Fetches all problems from LeetCode's GraphQL API. | |
* @returns {Promise<LeetCodeProblem[]>} Array of problem metadata | |
* @throws {Error} If the API request fails | |
*/ | |
static async fetchAllProblems(): Promise<LeetCodeProblem[]> { | |
const query = { | |
query: ` | |
query problemsetQuestionList($categorySlug: String, $limit: Int, $skip: Int, $filters: QuestionListFilterInput) { | |
problemsetQuestionList: questionList( | |
categorySlug: $categorySlug | |
limit: $limit | |
skip: $skip | |
filters: $filters | |
) { | |
questions: data { | |
titleSlug | |
difficulty | |
} | |
} | |
} | |
`, | |
variables: { | |
categorySlug: '', | |
limit: 3500, // Increased slightly for buffer | |
skip: 0, | |
filters: {}, | |
}, | |
}; | |
try { | |
const response = await axios.post(this.LEETCODE_GRAPHQL_URL, query, { | |
headers: { | |
'Content-Type': 'application/json', | |
'User-Agent': 'Mozilla/5.0', | |
}, | |
timeout: 10000, // 10-second timeout | |
}); | |
return response.data.data.problemsetQuestionList.questions || []; | |
} catch (error) { | |
console.error('Failed to fetch LeetCode problems:', error instanceof Error ? error.message : error); | |
return []; | |
} | |
} | |
/** | |
* Sorts problem solution files into difficulty-based subdirectories. | |
* @param {string} baseDirectory - Root directory containing language folders | |
* @param {LeetCodeProblem[]} problems - Array of problem metadata from LeetCode | |
* @returns {Promise<void>} | |
* @throws {Error} If file operations fail | |
*/ | |
static async sortProblemFiles(baseDirectory: string, problems: LeetCodeProblem[]): Promise<void> { | |
// Create difficulty map for efficient lookup | |
const difficultyMap = new Map( | |
problems.map((problem) => [problem.titleSlug, problem.difficulty.toLowerCase()]) | |
); | |
try { | |
const directories = await Promise.all( | |
this.PROGRAMMING_DIRECTORIES.map(async (dir) => { | |
const fullPath = path.join(baseDirectory, dir); | |
try { | |
return { dir, files: await fs.readdir(fullPath), path: fullPath }; | |
} catch (error) { | |
console.warn(`Skipping directory ${dir}: ${error instanceof Error ? error.message : error}`); | |
return null; | |
} | |
}) | |
); | |
// Filter out null directories and process concurrently | |
await Promise.all( | |
directories | |
.filter((dirInfo): dirInfo is NonNullable<typeof dirInfo> => dirInfo !== null) | |
.map(async ({ dir, files, path: dirPath }) => { | |
for (const file of files) { | |
const ext = path.extname(file); | |
const baseName = path.basename(file, ext); | |
const slug = baseName.split('.')[0]; | |
const difficulty = difficultyMap.get(slug); | |
if (difficulty) { | |
const difficultyDirPath = path.join(dirPath, difficulty); | |
await fs.mkdir(difficultyDirPath, { recursive: true }); | |
const sourcePath = path.join(dirPath, file); | |
const destPath = path.join(difficultyDirPath, file); | |
try { | |
await fs.rename(sourcePath, destPath); | |
console.log(`Moved ${file} to ${difficulty} difficulty`); | |
} catch (moveError) { | |
console.error(`Failed to move ${file}:`, moveError); | |
} | |
} | |
} | |
}) | |
); | |
} catch (error) { | |
console.error('Error processing problem directories:', error); | |
} | |
} | |
/** | |
* Main execution method that orchestrates the problem sorting process. | |
* @param {string} [baseDirectory=process.cwd()] - Root directory to process | |
* @returns {Promise<void>} | |
* @throws {Error} If the sorting process fails | |
*/ | |
static async run(baseDirectory: string = process.cwd()): Promise<void> { | |
console.time('Problem Sorting Duration'); | |
try { | |
const problems = await this.fetchAllProblems(); | |
if (problems.length === 0) { | |
console.warn('No problems retrieved. Exiting.'); | |
return; | |
} | |
await this.sortProblemFiles(baseDirectory, problems); | |
} catch (error) { | |
console.error('Unexpected error in problem sorting process:', error); | |
} finally { | |
console.timeEnd('Problem Sorting Duration'); | |
} | |
} | |
} | |
// Allow direct execution or import | |
if (require.main === module) { | |
ProblemSorter.run().catch(console.error); | |
} | |
export default ProblemSorter; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import axios from 'axios'; | |
import { promises as fs } from 'fs'; | |
import path from 'path'; | |
/** | |
* Interface representing a LeetCode problem returned from the API | |
* @interface | |
*/ | |
interface LeetCodeProblem { | |
/** The title of the problem */ | |
title: string; | |
/** The difficulty level (Easy, Medium, Hard) */ | |
difficulty: string; | |
/** The URL-friendly slug version of the title */ | |
titleSlug: string; | |
} | |
/** | |
* Fetches the list of problems from LeetCode's GraphQL API | |
* @returns {Promise<LeetCodeProblem[]>} Array of LeetCode problems with title, difficulty and slug | |
* @throws {Error} If the API request fails | |
*/ | |
async function fetchProblems(): Promise<LeetCodeProblem[]> { | |
const url = 'https://leetcode.com/graphql'; | |
const query = { | |
query: ` | |
query problemsetQuestionList($categorySlug: String, $limit: Int, $skip: Int, $filters: QuestionListFilterInput) { | |
problemsetQuestionList: questionList( | |
categorySlug: $categorySlug | |
limit: $limit | |
skip: $skip | |
filters: $filters | |
) { | |
questions: data { | |
title | |
difficulty | |
titleSlug | |
} | |
} | |
} | |
`, | |
variables: { | |
categorySlug: '', | |
limit: 3374, | |
skip: 0, | |
filters: {}, | |
}, | |
}; | |
try { | |
const response = await axios.post(url, query, { | |
headers: { | |
'Content-Type': 'application/json', | |
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', | |
}, | |
}); | |
return response.data.data.problemsetQuestionList.questions; | |
} catch (error) { | |
console.error('Error fetching problems:', error); | |
return []; | |
} | |
} | |
/** | |
* Sorts an array of LeetCode problems by difficulty level | |
* @param {LeetCodeProblem[]} problems - Array of problems to sort | |
* @returns {Promise<void>} | |
*/ | |
async function sortProblemsByDifficulty(problems: LeetCodeProblem[]): Promise<void> { | |
const difficultyOrder = ['Easy', 'Medium', 'Hard']; | |
problems.sort((a, b) => difficultyOrder.indexOf(a.difficulty) - difficultyOrder.indexOf(b.difficulty)); | |
} | |
/** | |
* Interface mapping programming languages to their file extensions | |
* @interface | |
*/ | |
interface LanguageExtensions { | |
[key: string]: string; | |
} | |
/** | |
* Creates empty solution files for each problem across multiple programming languages | |
* @param {LeetCodeProblem[]} problems - Array of problems to create files for | |
* @returns {Promise<void>} | |
* @throws {Error} If file operations fail | |
*/ | |
async function createSortedFiles(problems: LeetCodeProblem[]): Promise<void> { | |
const languages: LanguageExtensions = { | |
python: 'py', | |
typescript: 'ts', | |
javascript: 'js', | |
java: 'java', | |
cpp: 'cpp', | |
c: 'c', | |
dart: 'dart', | |
php: 'php', | |
csharp: 'cs', | |
go: 'go', | |
rust: 'rs', | |
ruby: 'rb', | |
swift: 'swift', | |
kotlin: 'kt', | |
}; | |
for (const problem of problems) { | |
const kebabTitle = problem.titleSlug; | |
const difficulty = problem.difficulty.toLowerCase(); | |
for (const [language, extension] of Object.entries(languages)) { | |
const dirPath = `${language}/${difficulty}`; | |
const filePath = `${dirPath}/${kebabTitle}.${extension}`; | |
try { | |
await fs.access(filePath); | |
console.log(`File already exists, skipping: ${filePath}`); | |
} catch { | |
await fs.mkdir(dirPath, { recursive: true }); | |
await fs.writeFile(filePath, ''); | |
console.log(`Created file: ${filePath}`); | |
} | |
} | |
} | |
} | |
/** | |
* Main execution function that orchestrates the problem fetching, sorting and file creation | |
* @returns {Promise<void>} | |
*/ | |
async function main(): Promise<void> { | |
const problems = await fetchProblems(); | |
await sortProblemsByDifficulty(problems); | |
await createSortedFiles(problems); | |
} | |
/** | |
* Class that provides a static interface for running the problem processing pipeline | |
*/ | |
class Problems { | |
/** | |
* Executes the main problem processing pipeline | |
* @returns {Promise<void>} | |
* @throws {Error} If any step in the pipeline fails | |
*/ | |
static async run(): Promise<void> { | |
await main(); | |
} | |
} | |
if (require.main === module) { | |
Problems.run().catch(console.error); | |
} | |
export default Problems; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment