Skip to content

Instantly share code, notes, and snippets.

@WomB0ComB0
Created December 6, 2024 18:53
Show Gist options
  • Save WomB0ComB0/882dd1981e7dd799ef41661f10fd2c48 to your computer and use it in GitHub Desktop.
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
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;
/**
* @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;
/**
* @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;
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