зеркало из https://github.com/microsoft/tiny-calc.git
FrugalList
This commit is contained in:
Родитель
ee7d719274
Коммит
4b2e098640
|
@ -3,6 +3,7 @@ dependencies:
|
||||||
'@rush-temp/es': 'file:projects/es.tgz'
|
'@rush-temp/es': 'file:projects/es.tgz'
|
||||||
'@rush-temp/eslint-config': 'file:projects/eslint-config.tgz_eslint@7.14.0+typescript@4.1.2'
|
'@rush-temp/eslint-config': 'file:projects/eslint-config.tgz_eslint@7.14.0+typescript@4.1.2'
|
||||||
'@rush-temp/example': 'file:projects/example.tgz'
|
'@rush-temp/example': 'file:projects/example.tgz'
|
||||||
|
'@rush-temp/frugallist': 'file:projects/frugallist.tgz'
|
||||||
'@rush-temp/graph': 'file:projects/graph.tgz'
|
'@rush-temp/graph': 'file:projects/graph.tgz'
|
||||||
'@rush-temp/handletable': 'file:projects/handletable.tgz'
|
'@rush-temp/handletable': 'file:projects/handletable.tgz'
|
||||||
'@rush-temp/heap': 'file:projects/heap.tgz'
|
'@rush-temp/heap': 'file:projects/heap.tgz'
|
||||||
|
@ -2875,6 +2876,23 @@ packages:
|
||||||
integrity: sha512-hKDbQi6qxpzLLPswZI9hv3c6UaImw/3ypjPw8TxBdBgch4t8DrpjtFjKZOr5B7mgPdAk7ZA67NF2vY5xTM+a6g==
|
integrity: sha512-hKDbQi6qxpzLLPswZI9hv3c6UaImw/3ypjPw8TxBdBgch4t8DrpjtFjKZOr5B7mgPdAk7ZA67NF2vY5xTM+a6g==
|
||||||
tarball: 'file:projects/example.tgz'
|
tarball: 'file:projects/example.tgz'
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
|
'file:projects/frugallist.tgz':
|
||||||
|
dependencies:
|
||||||
|
'@types/mocha': 8.0.4
|
||||||
|
'@types/node': 14.14.10
|
||||||
|
best-random: 1.0.3
|
||||||
|
eslint: 7.14.0
|
||||||
|
hotloop: 1.2.0
|
||||||
|
mocha: 8.2.1
|
||||||
|
rimraf: 3.0.2
|
||||||
|
ts-node: 9.0.0_typescript@4.1.2
|
||||||
|
typescript: 4.1.2
|
||||||
|
dev: false
|
||||||
|
name: '@rush-temp/frugallist'
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-VbPT6NIgsrDGg3UU/3y5ORe4J+Mqy24aXfgzvM48RtdM2CSMxnM2lVWZliJstSlz8q+0BGi2+fyPc5+bqXCpDA==
|
||||||
|
tarball: 'file:projects/frugallist.tgz'
|
||||||
|
version: 0.0.0
|
||||||
'file:projects/graph.tgz':
|
'file:projects/graph.tgz':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/mocha': 8.0.4
|
'@types/mocha': 8.0.4
|
||||||
|
@ -3059,6 +3077,7 @@ specifiers:
|
||||||
'@rush-temp/es': 'file:./projects/es.tgz'
|
'@rush-temp/es': 'file:./projects/es.tgz'
|
||||||
'@rush-temp/eslint-config': 'file:./projects/eslint-config.tgz'
|
'@rush-temp/eslint-config': 'file:./projects/eslint-config.tgz'
|
||||||
'@rush-temp/example': 'file:./projects/example.tgz'
|
'@rush-temp/example': 'file:./projects/example.tgz'
|
||||||
|
'@rush-temp/frugallist': 'file:./projects/frugallist.tgz'
|
||||||
'@rush-temp/graph': 'file:./projects/graph.tgz'
|
'@rush-temp/graph': 'file:./projects/graph.tgz'
|
||||||
'@rush-temp/handletable': 'file:./projects/handletable.tgz'
|
'@rush-temp/handletable': 'file:./projects/handletable.tgz'
|
||||||
'@rush-temp/heap': 'file:./projects/heap.tgz'
|
'@rush-temp/heap': 'file:./projects/heap.tgz'
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
"extends": [ "@tiny-calc/eslint-config" ],
|
||||||
|
parserOptions: {
|
||||||
|
project: './tsconfig.eslint.json',
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
# NPM dependencies
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
/dist
|
||||||
|
|
||||||
|
# Typescript incremental build cache
|
||||||
|
/*.tsbuildinfo
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*!
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getTestArgs } from "hotloop";
|
||||||
|
import { forEachBench } from "./foreach";
|
||||||
|
|
||||||
|
const { count } = getTestArgs();
|
||||||
|
const list: number[] = new Array(count).fill(0).map((value, index) => index);
|
||||||
|
|
||||||
|
forEachBench(
|
||||||
|
"Array",
|
||||||
|
count,
|
||||||
|
list,
|
||||||
|
(array, callback) => {
|
||||||
|
for (let i = 0; i < array.length; i++) {
|
||||||
|
callback(array[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*!
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getTestArgs } from "hotloop";
|
||||||
|
import { FrugalList, FrugalList_push, FrugalList_forEach } from "../src";
|
||||||
|
import { forEachBench } from "./foreach";
|
||||||
|
|
||||||
|
const { count } = getTestArgs();
|
||||||
|
|
||||||
|
let list: FrugalList<number> = undefined;
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
list = FrugalList_push(list, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
forEachBench(
|
||||||
|
"FrugalList",
|
||||||
|
count,
|
||||||
|
list,
|
||||||
|
FrugalList_forEach
|
||||||
|
)
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*!
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getTestArgs } from "hotloop";
|
||||||
|
import { TwoField, TwoField_push, TwoField_forEach } from "./impl/twofield";
|
||||||
|
import { forEachBench } from "./foreach";
|
||||||
|
|
||||||
|
const { count } = getTestArgs();
|
||||||
|
|
||||||
|
const list: TwoField<number> = {};
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
TwoField_push(list, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
forEachBench(
|
||||||
|
"TwoField",
|
||||||
|
count,
|
||||||
|
list,
|
||||||
|
TwoField_forEach
|
||||||
|
)
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*!
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { strict as assert } from "assert";
|
||||||
|
import { benchmark } from "hotloop";
|
||||||
|
|
||||||
|
let consumedCount = 0;
|
||||||
|
let consumedCache: any;
|
||||||
|
|
||||||
|
export function forEachBench<TSelf, TConsumer>(
|
||||||
|
name: string,
|
||||||
|
count: number,
|
||||||
|
self: TSelf,
|
||||||
|
forEach: (self: TSelf, callback: (consumer: TConsumer) => void) => void
|
||||||
|
): void {
|
||||||
|
// Sanity check that list was initialized correctly.
|
||||||
|
let sum = 0;
|
||||||
|
forEach(self, () => {
|
||||||
|
sum++;
|
||||||
|
});
|
||||||
|
assert.equal(sum, count);
|
||||||
|
|
||||||
|
benchmark(`${name}: ForEach(length=${count})`, () => {
|
||||||
|
forEach(self, (item) => {
|
||||||
|
// Paranoid defense against dead code elimination.
|
||||||
|
consumedCount++;
|
||||||
|
consumedCount |= 0;
|
||||||
|
|
||||||
|
if (consumedCount === 0) {
|
||||||
|
consumedCache = item;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent v8's optimizer from identifying 'cached' as an unused value.
|
||||||
|
process.on('exit', () => {
|
||||||
|
if (consumedCount === -1) {
|
||||||
|
console.log(`Ignore this: ${consumedCache}`);
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*!
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type TwoFieldItem<T> = Exclude<T, undefined>
|
||||||
|
|
||||||
|
export type TwoField<T> = {
|
||||||
|
item0?: T;
|
||||||
|
items?: T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TwoField_push<T>(self: TwoField<T>, consumer: TwoFieldItem<T>) {
|
||||||
|
if (self.item0 === undefined) {
|
||||||
|
self.item0 = consumer;
|
||||||
|
} else if (self.items === undefined) {
|
||||||
|
self.items = [consumer];
|
||||||
|
} else {
|
||||||
|
self.items.push(consumer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TwoField_delete<T>(self: TwoField<T>, consumer: TwoFieldItem<T>) {
|
||||||
|
if (self.item0 === undefined) {
|
||||||
|
self.item0 = consumer;
|
||||||
|
} else if (self.items === undefined) {
|
||||||
|
self.items = [consumer];
|
||||||
|
} else {
|
||||||
|
self.items.push(consumer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TwoField_forEach<T>(self: TwoField<T>, callback: (consumer: T) => void): void {
|
||||||
|
if (self.item0 === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(self.item0);
|
||||||
|
|
||||||
|
if (self.items !== undefined) {
|
||||||
|
for (let i = 0; i < self.items.length; i++) {
|
||||||
|
callback(self.items[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*!
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { run } from "hotloop";
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-void
|
||||||
|
void (async () => {
|
||||||
|
let count = 0;
|
||||||
|
console.group(`ConsumerSet (length=${count})`)
|
||||||
|
await run([
|
||||||
|
{ "path": "./foreach-array.ts", args: { count }},
|
||||||
|
{ "path": "./foreach-frugallist.ts", args: { count }},
|
||||||
|
{ "path": "./foreach-twofield.ts", args: { count }},
|
||||||
|
]);
|
||||||
|
console.groupEnd();
|
||||||
|
|
||||||
|
count = 1;
|
||||||
|
console.group(`ConsumerSet (length=${count})`)
|
||||||
|
await run([
|
||||||
|
{ "path": "./foreach-array.ts", args: { count }},
|
||||||
|
{ "path": "./foreach-frugallist.ts", args: { count }},
|
||||||
|
{ "path": "./foreach-twofield.ts", args: { count }},
|
||||||
|
]);
|
||||||
|
console.groupEnd();
|
||||||
|
|
||||||
|
count = 2;
|
||||||
|
console.group(`ConsumerSet (length=${count})`)
|
||||||
|
await run([
|
||||||
|
{ "path": "./foreach-array.ts", args: { count }},
|
||||||
|
{ "path": "./foreach-frugallist.ts", args: { count }},
|
||||||
|
{ "path": "./foreach-twofield.ts", args: { count }},
|
||||||
|
]);
|
||||||
|
console.groupEnd();
|
||||||
|
})();
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"name": "@tiny-calc/frugallist",
|
||||||
|
"version": "0.0.0-alpha.5",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"sideEffects": "false",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/microsoft/tiny-calc.git"
|
||||||
|
},
|
||||||
|
"author": "Microsoft",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"bench": "cd bench && ts-node index.ts",
|
||||||
|
"build": "tsc",
|
||||||
|
"clean": "rimraf ./dist *.build.log",
|
||||||
|
"dev": "npm run build -- --watch",
|
||||||
|
"lint": "eslint --ext=ts --format visualstudio src",
|
||||||
|
"test": "mocha -r ts-node/register test/**/*.spec.ts"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tiny-calc/eslint-config": "0.0.0-alpha.5",
|
||||||
|
"@tiny-calc/ts-config": "0.0.0-alpha.5",
|
||||||
|
"@types/mocha": "^8.0.4",
|
||||||
|
"@types/node": "^14.14.10",
|
||||||
|
"best-random": "^1.0.3",
|
||||||
|
"eslint": "^7.13.0",
|
||||||
|
"hotloop": "^1.2.0",
|
||||||
|
"mocha": "^8.2.1",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
|
"ts-node": "^9.0.0",
|
||||||
|
"typescript": "^4.0.5"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
/*!
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { FrugalList, FrugalListItem } from "./types";
|
||||||
|
|
||||||
|
export function FrugalList_forEach<T>(self: FrugalList<T>, callback: (value: FrugalListItem<T>) => void): void {
|
||||||
|
if (self !== undefined) {
|
||||||
|
if (Array.isArray(self)) {
|
||||||
|
for (let index = 0; index < self.length; index++) {
|
||||||
|
callback(self[index]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback(/* value: */ self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
/*!
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { FrugalList, FrugalListItem } from "./types";
|
||||||
|
export { FrugalList_forEach } from "./forEach";
|
||||||
|
export { FrugalList_push } from "./push";
|
||||||
|
export { FrugalList_removeFirst } from "./removeFirst";
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*!
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { FrugalList, FrugalListItem } from "./types";
|
||||||
|
|
||||||
|
export function FrugalList_push<T>(self: FrugalList<T>, value: FrugalListItem<T>): FrugalList<T> {
|
||||||
|
if (self === undefined) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(self)) {
|
||||||
|
self.push(value);
|
||||||
|
return self;
|
||||||
|
} else {
|
||||||
|
return [self, value];
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*!
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { FrugalList, FrugalListItem } from "./types";
|
||||||
|
|
||||||
|
export function FrugalList_removeFirst<T>(self: FrugalList<T>, value: FrugalListItem<T>): FrugalList<T> {
|
||||||
|
if (self === value) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(self)) {
|
||||||
|
const index = self.indexOf(value);
|
||||||
|
if (index >= 0) {
|
||||||
|
self.splice(/* start: */ index, /* deleteCount: */ 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
/*!
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Must not store the value 'undefined' or any Array type inside a FrugalList.
|
||||||
|
*/
|
||||||
|
export type FrugalListItem<T> = Exclude<T, undefined | Array<any>>; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
|
||||||
|
export type FrugalList<T> = undefined | FrugalListItem<T> | Array<FrugalListItem<T>>;
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*!
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "mocha";
|
||||||
|
import { TestFixture } from "./testfixture";
|
||||||
|
|
||||||
|
describe("FrugalList", () => {
|
||||||
|
const values = [0, 1, 2];
|
||||||
|
let list: TestFixture<number>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
list = new TestFixture<number>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("push", () => {
|
||||||
|
for (const value of values) {
|
||||||
|
list.push(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("remove in order", () => {
|
||||||
|
for (const value of values) {
|
||||||
|
list.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const value of values) {
|
||||||
|
list.removeFirst(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("remove in reverse order", () => {
|
||||||
|
for (const value of values) {
|
||||||
|
list.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const reversed = values.slice(0).reverse();
|
||||||
|
for (const value of reversed) {
|
||||||
|
list.removeFirst(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("remove with duplicates", () => {
|
||||||
|
for (const value of values) {
|
||||||
|
list.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const value of values) {
|
||||||
|
list.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const value of values) {
|
||||||
|
list.removeFirst(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*!
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "mocha";
|
||||||
|
import { strict as assert } from "assert";
|
||||||
|
import {
|
||||||
|
FrugalList,
|
||||||
|
FrugalList_push,
|
||||||
|
FrugalListItem,
|
||||||
|
FrugalList_removeFirst,
|
||||||
|
FrugalList_forEach
|
||||||
|
} from "../src";
|
||||||
|
|
||||||
|
export class TestFixture<T> {
|
||||||
|
private actual: FrugalList<T>;
|
||||||
|
private expected: FrugalListItem<T>[] = [];
|
||||||
|
|
||||||
|
public push(item: FrugalListItem<T>): void {
|
||||||
|
this.actual = FrugalList_push(this.actual, item);
|
||||||
|
this.expected.push(item);
|
||||||
|
|
||||||
|
this.vet();
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeFirst(item: FrugalListItem<T>): void {
|
||||||
|
this.actual = FrugalList_removeFirst(this.actual, item);
|
||||||
|
const index = this.expected.indexOf(item);
|
||||||
|
if (index >= 0) {
|
||||||
|
this.expected.splice(/* start: */ index, /* deleteCount: */ 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.vet();
|
||||||
|
}
|
||||||
|
|
||||||
|
private vet() {
|
||||||
|
const actual: FrugalListItem<T>[] = [];
|
||||||
|
FrugalList_forEach(this.actual, (item) => {
|
||||||
|
actual.push(item);
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.deepEqual(actual, this.expected);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
// extend your base config so you don't have to redefine your compilerOptions
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts",
|
||||||
|
"test/**/*.ts",
|
||||||
|
"bench/**/*.ts",
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"extends": "@tiny-calc/ts-config/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"declarationDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"outDir": "./dist"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"]
|
||||||
|
}
|
|
@ -46,6 +46,12 @@
|
||||||
"reviewCategory": "production",
|
"reviewCategory": "production",
|
||||||
"versionPolicyName": "public"
|
"versionPolicyName": "public"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"packageName": "@tiny-calc/frugallist",
|
||||||
|
"projectFolder": "packages/common/datastructures/frugallist",
|
||||||
|
"reviewCategory": "production",
|
||||||
|
"versionPolicyName": "public"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"packageName": "@tiny-calc/handletable",
|
"packageName": "@tiny-calc/handletable",
|
||||||
"projectFolder": "packages/common/datastructures/handletable",
|
"projectFolder": "packages/common/datastructures/handletable",
|
||||||
|
|
Загрузка…
Ссылка в новой задаче