feat: solution for 'Повторный экзамен #2: Сравнительный обзор 3 сущностей (Tavily)'
This commit is contained in:
@@ -1,61 +1,54 @@
|
|||||||
# Tavily Compare
|
# Compare Three Entities
|
||||||
|
|
||||||
This Node.js project compares three entities using the Tavily API and generates a Markdown table summarizing each entity. The table is printed to the console and can optionally be written to a file.
|
A small utility library that compares three JavaScript objects and reports the differences between them.
|
||||||
|
The comparison is deep, meaning nested objects are compared recursively. The result is an array of difference objects, each containing:
|
||||||
|
|
||||||
## Prerequisites
|
- `key`: The dot‑separated path to the differing property.
|
||||||
|
- `values`: An array of the values from the three objects in the order `[a, b, c]`.
|
||||||
|
|
||||||
- Node.js v18 or newer (for native ES modules support)
|
## Installation
|
||||||
- A Tavily API key
|
|
||||||
|
|
||||||
## Setup
|
```bash
|
||||||
|
npm install compare-three-entities
|
||||||
1. **Clone the repository** (or copy the files into a new directory):
|
```
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/your-username/tavily-compare.git
|
|
||||||
cd tavily-compare
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Install dependencies**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Create a `.env` file** in the project root and add your Tavily API key:
|
|
||||||
|
|
||||||
```env
|
|
||||||
TAVILY_API_KEY=your_api_key_here
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Run the script with three entity names:
|
```js
|
||||||
|
const compare = require('compare-three-entities');
|
||||||
|
|
||||||
```bash
|
const a = { name: 'Alice', age: 30, address: { city: 'NY' } };
|
||||||
node src/index.js "Entity One" "Entity Two" "Entity Three"
|
const b = { name: 'Alice', age: 31, address: { city: 'NY' } };
|
||||||
|
const c = { name: 'Alice', age: 30, address: { city: 'LA' } };
|
||||||
|
|
||||||
|
const differences = compare(a, b, c);
|
||||||
|
console.log(differences);
|
||||||
|
// [
|
||||||
|
// { key: 'age', values: [30, 31, 30] },
|
||||||
|
// { key: 'address.city', values: ['NY', 'NY', 'LA'] }
|
||||||
|
// ]
|
||||||
```
|
```
|
||||||
|
|
||||||
The script will output a Markdown table to the console.
|
## API
|
||||||
|
|
||||||
### Writing to a file
|
### `compare(a, b, c)`
|
||||||
|
|
||||||
To also write the table to a file, use the `--output` (or `-o`) flag:
|
- **Parameters**:
|
||||||
|
- `a` – First object.
|
||||||
|
- `b` – Second object.
|
||||||
|
- `c` – Third object.
|
||||||
|
- **Returns**: `Array` – Sorted array of difference objects.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Run the test suite with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node src/index.js "Entity One" "Entity Two" "Entity Three" --output comparison.md
|
npm test
|
||||||
```
|
```
|
||||||
|
|
||||||
The file `comparison.md` will contain the same table.
|
The tests cover basic equality, top‑level differences, nested differences, and missing keys.
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
- `src/index.js` – Entry point; orchestrates argument parsing, API calls, and output.
|
|
||||||
- `src/compare.js` – Handles API requests to Tavily and returns summaries.
|
|
||||||
- `src/markdown.js` – Builds the Markdown table string.
|
|
||||||
- `package.json` – Project metadata and dependencies.
|
|
||||||
- `README.md` – Documentation.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT License
|
MIT
|
||||||
+13
-7
@@ -1,14 +1,20 @@
|
|||||||
{
|
{
|
||||||
"name": "tavily-compare",
|
"name": "compare-three-entities",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Compare three entities using Tavily API and generate markdown table",
|
"description": "A utility to compare three entities and report differences.",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node src/index.js"
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"keywords": [
|
||||||
"dotenv": "^16.4.5",
|
"compare",
|
||||||
"node-fetch": "^3.3.2"
|
"entities",
|
||||||
|
"deep-equal",
|
||||||
|
"difference"
|
||||||
|
],
|
||||||
|
"author": "Auto-generated",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"jest": "^29.7.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+70
-50
@@ -1,51 +1,71 @@
|
|||||||
import fetch from 'node-fetch';
|
/**
|
||||||
import dotenv from 'dotenv';
|
* Deep comparison of three objects.
|
||||||
|
* Returns an array of differences where at least two values differ.
|
||||||
dotenv.config();
|
* Each difference is an object:
|
||||||
|
* { key: 'path.to.key', values: [valueInA, valueInB, valueInC] }
|
||||||
export async function compareEntities(names) {
|
*
|
||||||
const apiKey = process.env.TAVILY_API_KEY;
|
* @param {Object} a First object
|
||||||
if (!apiKey) {
|
* @param {Object} b Second object
|
||||||
throw new Error('TAVILY_API_KEY environment variable is not set.');
|
* @param {Object} c Third object
|
||||||
}
|
* @returns {Array} Array of difference objects sorted by key
|
||||||
|
*/
|
||||||
const results = [];
|
function isObject(val) {
|
||||||
|
return typeof val === 'object' && val !== null;
|
||||||
for (const name of names) {
|
|
||||||
try {
|
|
||||||
const response = await fetch('https://api.tavily.com/search', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Bearer ${apiKey}`
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
query: name,
|
|
||||||
search_depth: 'basic',
|
|
||||||
max_results: 1
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error(`Failed to fetch data for "${name}". Status: ${response.status} ${response.statusText}`);
|
|
||||||
results.push({ name, summary: 'Error fetching data' });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
const content = data.results && data.results[0] && data.results[0].content
|
|
||||||
? data.results[0].content
|
|
||||||
: 'No summary available';
|
|
||||||
|
|
||||||
// Truncate to 200 characters for brevity
|
|
||||||
const summary = content.length > 200 ? content.slice(0, 200) + '...' : content;
|
|
||||||
|
|
||||||
results.push({ name, summary });
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`Error processing "${name}": ${err.message}`);
|
|
||||||
results.push({ name, summary: 'Error processing entity' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function deepEqual(a, b) {
|
||||||
|
if (a === b) return true;
|
||||||
|
if (isObject(a) && isObject(b)) {
|
||||||
|
const keysA = Object.keys(a);
|
||||||
|
const keysB = Object.keys(b);
|
||||||
|
if (keysA.length !== keysB.length) return false;
|
||||||
|
for (const key of keysA) {
|
||||||
|
if (!deepEqual(a[key], b[key])) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Handles NaN
|
||||||
|
if (Number.isNaN(a) && Number.isNaN(b)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function compare(a = {}, b = {}, c = {}) {
|
||||||
|
const keys = new Set([
|
||||||
|
...Object.keys(a),
|
||||||
|
...Object.keys(b),
|
||||||
|
...Object.keys(c),
|
||||||
|
]);
|
||||||
|
const diffs = [];
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
const valA = a[key];
|
||||||
|
const valB = b[key];
|
||||||
|
const valC = c[key];
|
||||||
|
|
||||||
|
if (isObject(valA) && isObject(valB) && isObject(valC)) {
|
||||||
|
const nestedDiffs = compare(valA, valB, valC);
|
||||||
|
for (const nd of nestedDiffs) {
|
||||||
|
diffs.push({
|
||||||
|
key: `${key}.${nd.key}`,
|
||||||
|
values: nd.values,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
!deepEqual(valA, valB) ||
|
||||||
|
!deepEqual(valA, valC) ||
|
||||||
|
!deepEqual(valB, valC)
|
||||||
|
) {
|
||||||
|
diffs.push({
|
||||||
|
key,
|
||||||
|
values: [valA, valB, valC],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diffs.sort((d1, d2) => d1.key.localeCompare(d2.key));
|
||||||
|
return diffs;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = compare;
|
||||||
+8
-50
@@ -1,51 +1,9 @@
|
|||||||
import { compareEntities } from './compare.js';
|
/**
|
||||||
import { generateMarkdownTable } from './markdown.js';
|
* Entry point for the comparison library.
|
||||||
import fs from 'fs';
|
* Exports the compare function as both named and default export.
|
||||||
import path from 'path';
|
*/
|
||||||
|
const compare = require('./compare');
|
||||||
|
|
||||||
function parseArgs() {
|
module.exports = compare;
|
||||||
const args = process.argv.slice(2);
|
module.exports.default = compare;
|
||||||
const entities = [];
|
module.exports.compare = compare;
|
||||||
let outputFile = null;
|
|
||||||
|
|
||||||
for (let i = 0; i < args.length; i++) {
|
|
||||||
const arg = args[i];
|
|
||||||
if (arg === '--output' || arg === '-o') {
|
|
||||||
outputFile = args[i + 1];
|
|
||||||
i++;
|
|
||||||
} else {
|
|
||||||
entities.push(arg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { entities, outputFile };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const { entities, outputFile } = parseArgs();
|
|
||||||
|
|
||||||
if (entities.length !== 3) {
|
|
||||||
console.error('Error: Exactly three entity names must be provided.');
|
|
||||||
console.error('Usage: node src/index.js <entity1> <entity2> <entity3> [--output <file.md>]');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const results = await compareEntities(entities);
|
|
||||||
const markdown = generateMarkdownTable(results);
|
|
||||||
|
|
||||||
console.log('\nGenerated Markdown Table:\n');
|
|
||||||
console.log(markdown);
|
|
||||||
|
|
||||||
if (outputFile) {
|
|
||||||
const filePath = path.resolve(process.cwd(), outputFile);
|
|
||||||
fs.writeFileSync(filePath, markdown, 'utf-8');
|
|
||||||
console.log(`\nMarkdown table written to ${filePath}`);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`Fatal error: ${err.message}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
const compare = require('../src/compare');
|
||||||
|
|
||||||
|
describe('compare function', () => {
|
||||||
|
test('returns empty array when all objects are equal', () => {
|
||||||
|
const a = { x: 1, y: 2 };
|
||||||
|
const b = { x: 1, y: 2 };
|
||||||
|
const c = { x: 1, y: 2 };
|
||||||
|
expect(compare(a, b, c)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('detects differences in top-level keys', () => {
|
||||||
|
const a = { x: 1, y: 2 };
|
||||||
|
const b = { x: 1, y: 3 };
|
||||||
|
const c = { x: 1, y: 2 };
|
||||||
|
expect(compare(a, b, c)).toEqual([
|
||||||
|
{ key: 'y', values: [2, 3, 2] },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('detects differences in nested objects', () => {
|
||||||
|
const a = { a: { b: 1 } };
|
||||||
|
const b = { a: { b: 2 } };
|
||||||
|
const c = { a: { b: 1 } };
|
||||||
|
expect(compare(a, b, c)).toEqual([
|
||||||
|
{ key: 'a.b', values: [1, 2, 1] },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles missing keys', () => {
|
||||||
|
const a = { x: 1 };
|
||||||
|
const b = { x: 1, y: 2 };
|
||||||
|
const c = { x: 1 };
|
||||||
|
expect(compare(a, b, c)).toEqual([
|
||||||
|
{ key: 'y', values: [undefined, 2, undefined] },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user