import {
SyllogisticStatement,
SyllogisticArgument,
SyllogisticType
} from '../types.js';
/**
* Parser for syllogistic arguments
* Handles natural language parsing of categorical syllogisms
*/
export class SyllogisticParser {
/**
* Parse natural language input into a syllogistic argument
* @param input Natural language syllogism
* @returns Parsed syllogistic argument
*/
parseSyllogism(input: string): SyllogisticArgument {
// Split into sentences
const sentences = this.splitIntoSentences(input);
if (sentences.length < 3) {
throw new Error('A syllogism requires at least three statements (two premises and a conclusion)');
}
// Identify conclusion (usually last or after "therefore", "thus", "hence", "so")
let conclusionIndex = sentences.length - 1;
let premises: string[] = [];
for (let i = 0; i < sentences.length; i++) {
const lowerSentence = sentences[i].toLowerCase();
if (lowerSentence.includes('therefore') ||
lowerSentence.includes('thus') ||
lowerSentence.includes('hence') ||
lowerSentence.startsWith('so ') ||
lowerSentence.includes(', so ')) {
conclusionIndex = i;
// Remove conclusion markers and any following punctuation/whitespace
sentences[i] = sentences[i]
.replace(/^\s*(therefore|thus|hence|so)\s*[,:]?\s*/gi, '')
.replace(/\b(therefore|thus|hence)\b\s*[,:]?\s*/gi, '')
.replace(/,\s*so\b\s*/gi, ', ') // Replace ", so" with ", "
.trim();
break;
}
}
// Extract premises (all statements except conclusion)
premises = sentences.filter((_, index) => index !== conclusionIndex);
const conclusionText = sentences[conclusionIndex];
if (premises.length !== 2) {
throw new Error(`Expected 2 premises but found ${premises.length}`);
}
// Parse each statement
const premise1 = this.parseStatement(premises[0]);
const premise2 = this.parseStatement(premises[1]);
const conclusion = this.parseStatement(conclusionText);
// Identify major and minor premises
// The major premise contains the predicate of the conclusion
// The minor premise contains the subject of the conclusion
let majorPremise: SyllogisticStatement;
let minorPremise: SyllogisticStatement;
if (this.containsTerm(premise1, conclusion.predicate)) {
majorPremise = premise1;
minorPremise = premise2;
} else if (this.containsTerm(premise2, conclusion.predicate)) {
majorPremise = premise2;
minorPremise = premise1;
} else {
// Fallback: assume first is major
majorPremise = premise1;
minorPremise = premise2;
}
return {
majorPremise,
minorPremise,
conclusion
};
}
/**
* Split input into sentences
* @param input Input text
* @returns Array of sentences
*/
private splitIntoSentences(input: string): string[] {
// Split by periods, but preserve periods that are part of abbreviations
const sentences = input
.split(/\.(?!\w)|[;]/)
.map(s => s.trim())
.filter(s => s.length > 0);
return sentences;
}
/**
* Parse a single categorical statement
* @param statement Natural language statement
* @returns Parsed syllogistic statement
*/
private parseStatement(statement: string): SyllogisticStatement {
// Remove trailing periods before normalization
const cleanedStatement = statement.trim().replace(/\.$/, '');
// First check for singular propositions (which need case sensitivity)
// Patterns for singular propositions with proper names
const singularAffirmativePatterns = [
/^([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)\s+is\s+(?:a\s+|an\s+)?(.+)$/,
/^([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)\s+was\s+(?:a\s+|an\s+)?(.+)$/,
/^([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)\s+will\s+be\s+(?:a\s+|an\s+)?(.+)$/
];
const singularNegativePatterns = [
/^([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)\s+is\s+not\s+(?:a\s+|an\s+)?(.+)$/,
/^([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)\s+was\s+not\s+(?:a\s+|an\s+)?(.+)$/,
/^([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)\s+will\s+not\s+be\s+(?:a\s+|an\s+)?(.+)$/
];
// Check for singular affirmative propositions
for (const pattern of singularAffirmativePatterns) {
const match = cleanedStatement.match(pattern);
if (match) {
return {
type: 'A' as SyllogisticType,
subject: this.cleanTerm(match[1], true), // Preserve case for proper names
predicate: this.cleanTerm(match[2])
};
}
}
// Check for singular negative propositions
for (const pattern of singularNegativePatterns) {
const match = cleanedStatement.match(pattern);
if (match) {
return {
type: 'E' as SyllogisticType,
subject: this.cleanTerm(match[1], true), // Preserve case for proper names
predicate: this.cleanTerm(match[2])
};
}
}
// If not a singular proposition, proceed with normal parsing
const normalizedStatement = cleanedStatement.toLowerCase().trim();
// Patterns for categorical statement types
const patterns = {
// Type A: Universal Affirmative - "All S are P"
A: [
/^all\s+(.+?)\s+are\s+(.+)$/i,
/^every\s+(.+?)\s+is\s+(?:a\s+|an\s+)?(.+)$/i,
/^each\s+(.+?)\s+is\s+(?:a\s+|an\s+)?(.+)$/i,
/^(.+?)\s+are\s+all\s+(.+)$/i
],
// Type E: Universal Negative - "No S are P"
E: [
/^no\s+(.+?)\s+are\s+(.+)$/i,
/^no\s+(.+?)\s+is\s+(?:a\s+|an\s+)?(.+)$/i,
/^none\s+of\s+the\s+(.+?)\s+are\s+(.+)$/i,
/^not\s+any\s+(.+?)\s+are\s+(.+)$/i
],
// Type I: Particular Affirmative - "Some S are P"
I: [
/^some\s+(.+?)\s+are\s+(.+)$/i,
/^some\s+(.+?)\s+is\s+(?:a\s+|an\s+)?(.+)$/i,
/^at\s+least\s+one\s+(.+?)\s+is\s+(?:a\s+|an\s+)?(.+)$/i,
/^there\s+(?:is|are)\s+(.+?)\s+that\s+(?:is|are)\s+(.+)$/i
],
// Type O: Particular Negative - "Some S are not P"
O: [
/^some\s+(.+?)\s+are\s+not\s+(.+)$/i,
/^some\s+(.+?)\s+is\s+not\s+(?:a\s+|an\s+)?(.+)$/i,
/^not\s+all\s+(.+?)\s+are\s+(.+)$/i,
/^at\s+least\s+one\s+(.+?)\s+is\s+not\s+(?:a\s+|an\s+)?(.+)$/i
]
};
// Try to match each pattern type
for (const [type, typePatterns] of Object.entries(patterns)) {
for (const pattern of typePatterns) {
const match = normalizedStatement.match(pattern);
if (match) {
// Extract and clean terms
const subject = this.cleanTerm(match[1]);
const predicate = this.cleanTerm(match[2]);
return {
type: type as SyllogisticType,
subject,
predicate
};
}
}
}
// If no pattern matches, throw error
throw new Error(`Could not parse statement: "${cleanedStatement}"`);
}
/**
* Simple lemmatization to handle singular/plural forms
* @param term The term to lemmatize
* @returns Lemmatized term
*/
private lemmatize(term: string): string {
// Handle irregular plurals first
const irregularPlurals: { [key: string]: string } = {
'men': 'man',
'women': 'woman',
'children': 'child',
'people': 'person',
'feet': 'foot',
'teeth': 'tooth',
'geese': 'goose',
'mice': 'mouse',
'oxen': 'ox'
};
const lowerTerm = term.toLowerCase();
if (lowerTerm in irregularPlurals) {
return irregularPlurals[lowerTerm];
}
// Simple rules for regular plural forms
if (term.endsWith('s') && !term.endsWith('ss')) {
// Handle special cases
if (term.endsWith('ies')) {
return term.slice(0, -3) + 'y'; // berries -> berry
} else if (term.endsWith('es') && (term.endsWith('ches') || term.endsWith('shes') || term.endsWith('xes') || term.endsWith('zes'))) {
return term.slice(0, -2); // watches -> watch
}
// Remove 's' for regular plurals
return term.slice(0, -1);
}
return term;
}
/**
* Clean and normalize a term
* @param term Raw term from parsing
* @param preserveCase Whether to preserve the original case (for proper names)
* @returns Cleaned term
*/
private cleanTerm(term: string, preserveCase = false): string {
const cleaned = term
.trim()
.replace(/^(a|an|the)\s+/i, '') // Remove articles
.replace(/\s+/g, ' '); // Normalize whitespace
if (preserveCase) {
return cleaned;
}
// For non-proper names, convert to lowercase and lemmatize
const lowercased = cleaned.toLowerCase();
return this.lemmatize(lowercased);
}
/**
* Check if a term is a proper name (starts with capital letter)
* @param term The term to check
* @returns True if the term is a proper name
*/
private isProperName(term: string): boolean {
return /^[A-Z]/.test(term.trim());
}
/**
* Check if a statement contains a specific term
* @param statement The statement to check
* @param term The term to look for
* @returns True if the statement contains the term
*/
private containsTerm(statement: SyllogisticStatement, term: string): boolean {
// For proper names, preserve case when comparing
const isProper = this.isProperName(term) || this.isProperName(statement.subject);
const normalizedTerm = this.cleanTerm(term, isProper);
const normalizedSubject = this.cleanTerm(statement.subject, isProper);
const normalizedPredicate = this.cleanTerm(statement.predicate);
return normalizedSubject === normalizedTerm || normalizedPredicate === normalizedTerm;
}
/**
* Identify the middle term in a syllogism
* @param argument The syllogistic argument
* @returns The middle term
*/
identifyMiddleTerm(argument: SyllogisticArgument): string {
const majorTerms = [argument.majorPremise.subject, argument.majorPremise.predicate];
const minorTerms = [argument.minorPremise.subject, argument.minorPremise.predicate];
const conclusionTerms = [argument.conclusion.subject, argument.conclusion.predicate];
// The middle term appears in both premises but not in the conclusion
for (const majorTerm of majorTerms) {
for (const minorTerm of minorTerms) {
// Check if terms match (considering proper names and lemmatization)
const majorIsProper = this.isProperName(majorTerm);
const minorIsProper = this.isProperName(minorTerm);
// For proper names, preserve case; otherwise normalize and lemmatize
const majorNormalized = majorIsProper ? majorTerm : this.cleanTerm(majorTerm);
const minorNormalized = minorIsProper ? minorTerm : this.cleanTerm(minorTerm);
if (majorNormalized === minorNormalized) {
// Check it's not in the conclusion
const notInConclusion = !conclusionTerms.some(t => {
const tIsProper = this.isProperName(t);
const tNormalized = tIsProper ? t : this.cleanTerm(t);
return tNormalized === majorNormalized;
});
if (notInConclusion) {
return majorNormalized;
}
}
}
}
throw new Error('Could not identify middle term - syllogism may be malformed');
}
}