Pig Latin

This commit is contained in:
2025-04-20 15:23:43 +02:00
parent b7199d2a17
commit fba3e4794e
19 changed files with 26048 additions and 0 deletions
@@ -0,0 +1,31 @@
{
"authors": [
"CRivasGomez"
],
"contributors": [
"masters3d",
"SleeplessByte"
],
"files": {
"solution": [
"pig-latin.ts"
],
"test": [
"pig-latin.test.ts"
],
"example": [
".meta/proof.ci.ts"
]
},
"blurb": "Implement a program that translates from English to Pig Latin.",
"custom": {
"version.tests.compatibility": "jest-29",
"flag.tests.task-per-describe": false,
"flag.tests.may-run-long": false,
"flag.tests.includes-optional": false,
"flag.tests.jest": true,
"flag.tests.tstyche": false
},
"source": "The Pig Latin exercise at Test First Teaching by Ultrasaurus",
"source_url": "https://github.com/ultrasaurus/test-first-teaching/blob/master/learn_ruby/pig_latin/"
}
@@ -0,0 +1 @@
{"track":"typescript","exercise":"pig-latin","id":"59d2b57ce0d74a898e56a8d87b730009","url":"https://exercism.org/tracks/typescript/exercises/pig-latin","handle":"briemens","is_requester":true,"auto_approve":false}
Generated Executable
+16683
View File
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+7
View File
@@ -0,0 +1,7 @@
{
"recommendations": [
"arcanis.vscode-zipfs",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}
+7
View File
@@ -0,0 +1,7 @@
{
"cSpell.words": ["exercism"],
"search.exclude": {
"**/.yarn": true,
"**/.pnp.*": true
}
}
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
compressionLevel: mixed
enableGlobalCache: true
+50
View File
@@ -0,0 +1,50 @@
# Help
## Running the tests
Before trying to execute the tests, ensure the assignment folder is set-up correctly by following the installation steps, namely `corepack yarn install` and the Editor SDK setup.
Execute the tests with:
```bash
$ corepack yarn test
```
## Skipped tests
In the test suites all tests but the first have been skipped.
Once you get a test passing, you can enable the next one by changing `xit` to `it`.
Additionally tests may be grouped using `xdescribe`.
Enable the group by changing that to `describe`.
Finally, some exercises may have optional tests `it.skip`.
Remove `.skip` to execute the optional test.
## Submitting your solution
You can submit your solution using the `exercism submit pig-latin.ts` command.
This command will upload your solution to the Exercism website and print the solution page's URL.
It's possible to submit an incomplete solution which allows you to:
- See how others have completed the exercise
- Request help from a mentor
## Need to get help?
If you'd like help solving the exercise, check the following pages:
- The [TypeScript track's documentation](https://exercism.org/docs/tracks/typescript)
- The [TypeScript track's programming category on the forum](https://forum.exercism.org/c/programming/typescript)
- [Exercism's programming category on the forum](https://forum.exercism.org/c/programming/5)
- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)
Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.
To get help if you're having trouble, you can use one of the following resources:
- [TypeScript QuickStart](https://www.typescriptlang.org/docs/handbook/release-notes/overview.html)
- [ECMAScript 2015 Language Specification](https://www.ecma-international.org/wp-content/uploads/ECMA-262_6th_edition_june_2015.pdf) (pdf)
- [Mozilla JavaScript Reference](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference)
- [/r/typescript](https://www.reddit.com/r/typescript) is the TypeScript subreddit.
- [StackOverflow](https://stackoverflow.com/questions/tagged/typescript) can be used to search for your problem and see if it has been answered already. You can also ask and answer questions.
+75
View File
@@ -0,0 +1,75 @@
# Pig Latin
Welcome to Pig Latin on Exercism's TypeScript Track.
If you need help running the tests or submitting your code, check out `HELP.md`.
## Introduction
Your parents have challenged you and your sibling to a game of two-on-two basketball.
Confident they'll win, they let you score the first couple of points, but then start taking over the game.
Needing a little boost, you start speaking in [Pig Latin][pig-latin], which is a made-up children's language that's difficult for non-children to understand.
This will give you the edge to prevail over your parents!
[pig-latin]: https://en.wikipedia.org/wiki/Pig_latin
## Instructions
Your task is to translate text from English to Pig Latin.
The translation is defined using four rules, which look at the pattern of vowels and consonants at the beginning of a word.
These rules look at each word's use of vowels and consonants:
- vowels: the letters `a`, `e`, `i`, `o`, and `u`
- consonants: the other 21 letters of the English alphabet
## Rule 1
If a word begins with a vowel, or starts with `"xr"` or `"yt"`, add an `"ay"` sound to the end of the word.
For example:
- `"apple"` -> `"appleay"` (starts with vowel)
- `"xray"` -> `"xrayay"` (starts with `"xr"`)
- `"yttria"` -> `"yttriaay"` (starts with `"yt"`)
## Rule 2
If a word begins with a one or more consonants, first move those consonants to the end of the word and then add an `"ay"` sound to the end of the word.
For example:
- `"pig"` -> `"igp"` -> `"igpay"` (starts with single consonant)
- `"chair"` -> `"airch"` -> `"airchay"` (starts with multiple consonants)
- `"thrush"` -> `"ushthr"` -> `"ushthray"` (starts with multiple consonants)
## Rule 3
If a word starts with zero or more consonants followed by `"qu"`, first move those consonants (if any) and the `"qu"` part to the end of the word, and then add an `"ay"` sound to the end of the word.
For example:
- `"quick"` -> `"ickqu"` -> `"ay"` (starts with `"qu"`, no preceding consonants)
- `"square"` -> `"aresqu"` -> `"aresquay"` (starts with one consonant followed by `"qu`")
## Rule 4
If a word starts with one or more consonants followed by `"y"`, first move the consonants preceding the `"y"`to the end of the word, and then add an `"ay"` sound to the end of the word.
Some examples:
- `"my"` -> `"ym"` -> `"ymay"` (starts with single consonant followed by `"y"`)
- `"rhythm"` -> `"ythmrh"` -> `"ythmrhay"` (starts with multiple consonants followed by `"y"`)
## Source
### Created by
- @CRivasGomez
### Contributed to by
- @masters3d
- @SleeplessByte
### Based on
The Pig Latin exercise at Test First Teaching by Ultrasaurus - https://github.com/ultrasaurus/test-first-teaching/blob/master/learn_ruby/pig_latin/
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
// eslint-disable-next-line @typescript-eslint/no-require-imports
presets: [[require('@exercism/babel-preset-typescript'), { corejs: '3.38' }]],
plugins: [],
}
+26
View File
@@ -0,0 +1,26 @@
// @ts-check
import tsEslint from 'typescript-eslint'
import config from '@exercism/eslint-config-typescript'
import maintainersConfig from '@exercism/eslint-config-typescript/maintainers.mjs'
export default [
...tsEslint.config(...config, {
files: ['.meta/proof.ci.ts', '.meta/exemplar.ts', '*.test.ts'],
extends: maintainersConfig,
}),
{
ignores: [
// # Protected or generated
'.git/**/*',
'.vscode/**/*',
//# When using npm
'node_modules/**/*',
// # Configuration files
'babel.config.cjs',
'jest.config.cjs',
],
},
]
+22
View File
@@ -0,0 +1,22 @@
module.exports = {
verbose: true,
projects: ['<rootDir>'],
testMatch: [
'**/__tests__/**/*.[jt]s?(x)',
'**/test/**/*.[jt]s?(x)',
'**/?(*.)+(spec|test).[jt]s?(x)',
],
testPathIgnorePatterns: [
'/(?:production_)?node_modules/',
'.d.ts$',
'<rootDir>/test/fixtures',
'<rootDir>/test/helpers',
'__mocks__',
],
transform: {
'^.+\\.[jt]sx?$': 'babel-jest',
},
moduleNameMapper: {
'^(\\.\\/.+)\\.js$': '$1',
},
}
+38
View File
@@ -0,0 +1,38 @@
{
"name": "@exercism/typescript-pig-latin",
"version": "1.0.0",
"description": "Exercism exercises in Typescript.",
"private": true,
"repository": {
"type": "git",
"url": "https://github.com/exercism/typescript"
},
"type": "module",
"engines": {
"node": "^18.16.0 || >=20.0.0"
},
"devDependencies": {
"@exercism/babel-preset-typescript": "^0.6.0",
"@exercism/eslint-config-typescript": "^0.8.0",
"@jest/globals": "^29.7.0",
"@types/node": "~22.7.6",
"babel-jest": "^29.7.0",
"core-js": "~3.38.1",
"eslint": "^9.12.0",
"expect": "^29.7.0",
"jest": "^29.7.0",
"prettier": "^3.3.3",
"tstyche": "^2.1.1",
"typescript": "~5.6.3",
"typescript-eslint": "^8.10.0"
},
"scripts": {
"test": "corepack yarn node test-runner.mjs",
"test:types": "corepack yarn tstyche",
"test:implementation": "corepack yarn jest --no-cache --passWithNoTests",
"lint": "corepack yarn lint:types && corepack yarn lint:ci",
"lint:types": "corepack yarn tsc --noEmit -p .",
"lint:ci": "corepack yarn eslint . --ext .tsx,.ts"
},
"packageManager": "yarn@4.5.1"
}
+107
View File
@@ -0,0 +1,107 @@
import { describe, it, expect } from '@jest/globals'
import { translate } from './pig-latin.ts'
describe('ay is added to words that start with vowels', () => {
it('word beginning with a', () => {
const expected = 'appleay'
expect(translate('apple')).toEqual(expected)
})
it('word beginning with e', () => {
const expected = 'earay'
expect(translate('ear')).toEqual(expected)
})
it('word beginning with i', () => {
const expected = 'iglooay'
expect(translate('igloo')).toEqual(expected)
})
it('word beginning with o', () => {
const expected = 'objectay'
expect(translate('object')).toEqual(expected)
})
it('word beginning with u', () => {
const expected = 'underay'
expect(translate('under')).toEqual(expected)
})
it('word beginning with a vowel and followed by a qu', () => {
const expected = 'equalay'
expect(translate('equal')).toEqual(expected)
})
})
describe('first letter and ay are moved to the end of words that start with consonants', () => {
it('word beginning with p', () => {
const expected = 'igpay'
expect(translate('pig')).toEqual(expected)
})
it('word beginning with k', () => {
const expected = 'oalakay'
expect(translate('koala')).toEqual(expected)
})
it('word beginning with x', () => {
const expected = 'enonxay'
expect(translate('xenon')).toEqual(expected)
})
it('word beginning with q without a following u', () => {
const expected = 'atqay'
expect(translate('qat')).toEqual(expected)
})
})
describe('some letter clusters are treated like a single consonant', () => {
it('word beginning with ch', () => {
const expected = 'airchay'
expect(translate('chair')).toEqual(expected)
})
it('word beginning with qu', () => {
const expected = 'eenquay'
expect(translate('queen')).toEqual(expected)
})
it('word beginning with qu and a preceding consonant', () => {
const expected = 'aresquay'
expect(translate('square')).toEqual(expected)
})
it('word beginning with th', () => {
const expected = 'erapythay'
expect(translate('therapy')).toEqual(expected)
})
it('word beginning with thr', () => {
const expected = 'ushthray'
expect(translate('thrush')).toEqual(expected)
})
it('word beginning with sch', () => {
const expected = 'oolschay'
expect(translate('school')).toEqual(expected)
})
})
describe('position of y in a word determines if it is a consonant or a vowel', () => {
it('y is treated like a consonant at the beginning of a word', () => {
const expected = 'ellowyay'
expect(translate('yellow')).toEqual(expected)
})
it('y as second letter in two letter word', () => {
const expected = 'ymay'
expect(translate('my')).toEqual(expected)
})
})
describe('phrases are translated', () => {
it('a whole phrase', () => {
const expected = 'ickquay astfay unray'
expect(translate('quick fast run')).toEqual(expected)
})
})
+93
View File
@@ -0,0 +1,93 @@
const vowels = ['A', 'E', 'I', 'O', 'U']
const consonants = [
'B',
'C',
'D',
'F',
'G',
'H',
'J',
'K',
'L',
'M',
'N',
'P',
'Q',
'R',
'S',
'T',
'V',
'W',
'X',
'Y',
'Z',
]
const startsWiths = ['XR', 'YT', 'AY']
const rule1 = (word: string) =>
// If a word begins with a vowel, or starts with `"xr"` or `"yt"`,
// add an `"ay"` sound to the end of the word.
vowels.concat(startsWiths).some(s => word.toUpperCase().startsWith(s))
? `${word}ay`
: word
const rule2RegExp = new RegExp(`^([${consonants.join('')}]+)(.*)`, 'i')
const rule2 = (word: string): string =>
// If a word begins with a one or more consonants, first move
// those consonants to the end of the word and then add an `"ay"`
// sound to the end of the word.
word.replace(
rule2RegExp,
(_, match, rest) => `${rest}${match}${match ? 'ay' : ''}`,
)
const rule3RegExp = new RegExp(`^(qu|[${consonants.join('')}]*qu)(.*)`, 'i')
const rule3 = (word: string): string =>
// If a word starts with zero or more consonants followed by `"qu"`,
// first move those consonants (if any) and the `"qu"` part to the end
// of the word, and then add an `"ay"` sound to the end of the word.
word.replace(
rule3RegExp,
(_, match, rest) => `${rest}${match}${match ? 'ay' : ''}`,
)
const rule4RegExp = new RegExp(`^([${consonants.join('')}]+)(y.*)`, 'i')
const rule4 = (word: string): string =>
// If a word starts with one or more consonants followed by `"y"`,
// first move the consonants preceding the `"y"`to the end of the word,
// and then add an `"ay"` sound to the end of the word.
word.replace(
rule4RegExp,
(_, match, rest) => `${rest}${match}${match ? 'ay' : ''}`,
)
const applyRule =
(rule: (word: string) => string) =>
({
originalWord,
hasChanged,
}: {
originalWord: string
hasChanged: boolean
}) => {
if (hasChanged) return { originalWord, hasChanged }
const word = rule(originalWord)
return { originalWord: word, hasChanged: word !== originalWord }
}
export const translate = (text: string): string => {
return text
.split(' ')
.filter(Boolean)
.map(word => ({ originalWord: word, hasChanged: false }))
.map(applyRule(rule1))
.map(applyRule(rule3))
.map(applyRule(rule4))
.map(applyRule(rule2))
.map(({ originalWord }) => originalWord)
.join(' ')
}
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env node
/**
* 👋🏽 Hello there reader,
*
* It looks like you are working on this solution using the Exercism CLI and
* not the online editor. That's great! The file you are looking at executes
* the various steps the online test-runner also takes.
*
* @see https://github.com/exercism/typescript-test-runner
*
* TypeScript track exercises generally consist of at least two out of three
* types of tests to run.
*
* 1. tsc, the TypeScript compiler. This tests if the TypeScript code is valid
* 2. tstyche, static analysis tests to see if the types used are expected
* 3. jest, runtime implementation tests to see if the solution is correct
*
* If one of these three fails, this script terminates with -1, -2, or -3
* respectively. If it succeeds, it terminates with exit code 0.
*
* @note you need corepack (bundled with node LTS) enabled in order for this
* test runner to work as expected. Follow the installation and test
* instructions if you see errors about corepack or pnp.
*/
import { execSync } from 'node:child_process'
import { existsSync, readFileSync } from 'node:fs'
import { exit } from 'node:process'
import { URL } from 'node:url'
/**
* Before executing any tests, the test runner attempts to find the
* exercise config.json file which has metadata about which types of tests
* to run for this solution.
*/
const metaDirectory = new URL('./.meta/', import.meta.url)
const exercismDirectory = new URL('./.exercism/', import.meta.url)
const configDirectory = existsSync(metaDirectory)
? metaDirectory
: existsSync(exercismDirectory)
? exercismDirectory
: null
if (configDirectory === null) {
throw new Error(
'Expected .meta or .exercism directory to exist, but I cannot find it.'
)
}
const configFile = new URL('./config.json', configDirectory)
if (!existsSync(configFile)) {
throw new Error('Expected config.json to exist at ' + configFile.toString())
}
// Experimental: import config from './config.json' with { type: 'json' }
/** @type {import('./config.json') } */
const config = JSON.parse(readFileSync(configFile))
const jest = !config.custom || config.custom['flag.tests.jest']
const tstyche = config.custom?.['flag.tests.tstyche']
console.log(
`[tests] tsc: ✅, tstyche: ${tstyche ? '✅' : '❌'}, jest: ${jest ? '✅' : '❌'}, `
)
/**
* 1. tsc: the typescript compiler
*/
try {
console.log('[tests] tsc (compile)')
execSync('corepack yarn lint:types', {
stdio: 'inherit',
cwd: process.cwd(),
})
} catch {
exit(-1)
}
/**
* 2. tstyche: type tests
*/
if (tstyche) {
try {
console.log('[tests] tstyche (type tests)')
execSync('corepack yarn test:types', {
stdio: 'inherit',
cwd: process.cwd(),
})
} catch {
exit(-2)
}
}
/**
* 3. jest: implementation tests
*/
if (jest) {
try {
console.log('[tests] tstyche (implementation tests)')
execSync('corepack yarn test:implementation', {
stdio: 'inherit',
cwd: process.cwd(),
})
} catch {
exit(-3)
}
}
/**
* Done! 🥳
*/
+38
View File
@@ -0,0 +1,38 @@
{
"display": "Configuration for Exercism TypeScript Exercises",
"compilerOptions": {
// Allows you to use the newest syntax, and have access to console.log
// https://www.typescriptlang.org/tsconfig#lib
"lib": ["ES2020", "dom"],
// Make sure typescript is configured to output ESM
// https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c#how-can-i-make-my-typescript-project-output-esm
"module": "Node16",
// Since this project is using babel, TypeScript may target something very
// high, and babel will make sure it runs on your local Node version.
// https://babeljs.io/docs/en/
"target": "ES2020", // ESLint doesn't support this yet: "es2022",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
// Because jest-resolve isn't like node resolve, the absolute path must be .ts
"allowImportingTsExtensions": true,
"noEmit": true,
// Because we'll be using babel: ensure that Babel can safely transpile
// files in the TypeScript project.
//
// https://babeljs.io/docs/en/babel-plugin-transform-typescript/#caveats
"isolatedModules": true
},
"include": [
"*.ts",
"*.tsx",
".meta/*.ts",
".meta/*.tsx",
"__typetests__/*.tst.ts"
],
"exclude": ["node_modules"]
}
File diff suppressed because it is too large Load Diff