ToCode
الذهاب إلى القناة على Telegram
1 417
المشتركون
-224 ساعات
-37 أيام
-630 أيام
أرشيف المشاركات
1 417
סך הכל כל הכבוד לגאמרוד על השיתוף והפתיחות. מעבר לקוד פתוח פותח הזדמנויות לקהילה לגדול ולמפתחים אחרים ללמוד ובמיוחד כשמדובר על מערכת פרודקשן אמיתית. זה הקוד בגיטהאב שתוכלו גם להנות וללמוד:
https://github.com/antiwork/gumroad
1 417
שלושה דברים שלמדתי מהקוד של Gumroad
אז Gumroad עברו לקוד פתוח וזו הזדמנות מצוינת ללמוד על מבנה פרויקט ריילס גדול של מערכת אמיתית. אני בטוח שבימים הקרובים אבלה יותר זמן עם הריפו שלהם אבל הנה שלושה דברים מרכזיים שקופצים לעין:
מודלים שטוחים, קונסרנים מקוננים
יש המון מודלים בגאמרוד ולמרות זאת כולם באותה תיקיה. מודל מייצג טבלה בבסיס הנתונים ושמירת כל המודלים באותה תיקיה מסדרת את תיקיית models בצורה מקבילה לסכימה וכך קל למצוא דברים. מצד שני מבנה תיקיות במערכת קבצים כן עוזר לנו להתמצא אז את הלוגיקה הם כתבו בקבצי concerns שמסודרים בתיקיות. אין להם בעיה עם Concern-ים שנטענים רק ממודל אחד למשל המודול Follower::AudienceMember שנטען רק מתוך:
class Follower < ApplicationRecord
include ExternalId
include TimestampScopes
include Follower::From
include Deletable
include ConfirmedFollowerEvent::FollowerCallbacks
include Follower::AudienceMember
בדיקות בכל מקום
מבדיקה מדגמית כל מודל שנכנסתי אליו הכיל קוד בדיקה, לדוגמה:
* frozen_string_literal: true *
require "spec_helper"
RSpec.describe Community do
subject(:community) { build(:community) }
describe "associations" do
it { is_expected.to belong_to(:seller).class_name("User") }
it { is_expected.to belong_to(:resource) }
it { is_expected.to have_many(:community_chat_messages).dependent(:destroy) }
it { is_expected.to have_many(:last_read_community_chat_messages).dependent(:destroy) }
it { is_expected.to have_many(:community_chat_recaps).dependent(:destroy) }
end
describe "validations" do
it { is_expected.to validate_uniqueness_of(:seller_id).scoped_to([:resource_id, :resource_type, :deleted_at]) }
end
describe "#name" do
it "returns the resource name" do
community = build(:community, resource: create(:product, name: "Test product"))
expect(community.name).to eq("Test product")
end
end
describe "#thumbnail_url" do
it "returns the resource thumbnail url for email" do
community = build(:community, resource: create(:product))
expect(community.thumbnail_url).to eq(ActionController::Base.helpers.asset_url("native_types/thumbnails/digital.png"))
end
end
end
אני מודה שבחלק מהמקומות הבדיקה דורשת קצת מאמץ בקריאה, לדוגמה הבדיקה הבאה:
it "returns affiliates sorted by # of products" do
order = [affiliate_user_1, affiliate_user_3, affiliate_user_2]
expect(seller.direct_affiliates.sorted_by(key: "products", direction: "asc")).to eq(order)
expect(seller.direct_affiliates.sorted_by(key: "products", direction: "desc")).to eq(order.reverse)
end
מחייבת אותי לגלול לתחילת הקובץ כדי להבין איזה משתמש נוצר עם כמה מוצרים ואישית הייתי מעדיף לראות את ה create צמוד לקוד הבדיקה, אבל בואו - כמות הבדיקות והמגוון מרשימים. יש להם בדיקות מודלים, בדיקות לקונטרולרים ובדיקות מערכת.
פרונטאנד נראה מאתגר
גאמרוד משתמשים בריילס עבור השרת וריאקט לצד לקוח. אני לא מצאתי בדיקות לקומפוננטות בצד הפרונטאנד וגם הקוד עצמו היה נראה לי הרבה פחות מסודר: קבצים של מאות ואלפי שורות, פונקציות שמוגדרות בתוך הקומפוננטות, קוד ולוגיקה בתוך JSX, ובאופן כללי הרגשה של פחות תשומת לב.
דוגמה קטנה האפקט הבא שנועד להגדיל גובה של textarea כשהתוכן משתנה:
React.useEffect(() => {
if (!ref.current) return;
ref.current.style.height = "inherit";
ref.current.style.height = \${ref.current.scrollHeight}px\;
}, [props.value]);
שהיה נכון יותר לכתוב אותו בתור layout effect או הכי טוב לוותר עליו לגמרי ולהשתמש ב CSS.
או במבט יותר גבוה הבחירה לשמור אוביקט בתוך קונטקסט למשל ב DomainSettings.ts שלהם:
type DomainSettings = {
scheme: string;
appDomain: string;
rootDomain: string;
shortDomain: string;
discoverDomain: string;
thirdPartyAnalyticsDomain: string;
};
const Context = React.createContext<DomainSettings | null>(null);
שאומרת שכל פעם שאיזשהו מפתח באוביקט ישתנה כל הקומפוננטות שמקשיבות גם למפתחות אחרים יתרנדרו מחדש, מה שפוגע בביצועים.1 417
מה יש מול מה צריך (או: תחזירו לי את החלומות)
הרבה שנים הדינמיקה בין אנשי פרודקט למתכנתים היתה כמו משיכת חבל בין חלומות למציאות: אנשי פרודקט באו בדרישות, לפעמים הזויות, ואנחנו ניסינו לבנות את הקוד שיתאים וגם שישרוד את שינוי הדרישות הבא שלהם.
הרבה שנים הדינמיקה בין מפתחים למנהלי הפיתוח היתה סוג של משיכת חבל בין חלומות למציאות: אנחנו רצינו לשכתב חלקים במערכת כדי לבנות אבסטרקציות טובות יותר ומנהלי הפיתוח רצו קוד עובד ומהר, מקסימום נעשה ריפקטור בספרינט הבא.
וגם פה ה AI מאיים לשנות את התמונה. מפתחים טובים מסתכלים על ה AI וחולמים על הרגע שלא יצטרכו לכתוב יותר אבסטרקציות כי ה AI יבנה את הקוד בשבילם. אנשי פרודקט טובים מסתכלים על מערכת שקיבלו מ base44 ורוצים לקבל מוצר דומה מהמפתחים שלהם.
ל AI אין חלומות. הוא אלוף בלהראות מה אפשר, מה כולם עושים, איך נראה דף נחיתה טוב, איך נראית קומפוננטה טובה בריאקט, איך כתוב בספר שצריך לעשות. אבל פיתוח תוכנה זה לא רק ספר לימוד, ולחלומות שלנו יש מקום. כל מה שהיום אפשרי הוא אפשרי בזכות מישהו או מישהי שחשבו מחוץ לקופסה, שניסו משהו שאולי לא יצליח, שבנו משהו שלא היה ברור שבכלל אפשרי. וכן גם ברזולוציה הקטנה של המערכת שלכם מותר וצריך לחלום.
״ככה אני רוצה שזה יעבוד״ זה משפט ש AI לא יכול להגיד. וזה בדיוק המשפט שאנחנו צריכים כדי להתקדם.
1 417
2. קלוד יכול להחליט להריץ את הכלי מספר פעמים. התוצאה אינה מופעלת בטרנזאקציה וביטול ריצה באמצע פשוט גורם להפסקת השימוש בכלי, מה שעלול להוביל לבעיות קונסיסטנטיות אם באמת ניתן לו לעבוד על מידע אמיתי. נסו לגרום לקלוד ליצור 10 קבצים והפסיקו אותו באמצע כדי לראות שנתקעתם עם חלק מהקבצים.
3. באתר mcp.so תוכלו למצוא המון שרתי MCP שאנשים אחרים כתבו. מומלץ לקרוא את הקוד לפני שמתקינים.
1 417
כמה מילים ודוגמה על MCP
פרוטוקול MCP הוא הדרך שלנו להוסיף יכולות לסוכני ה AI שאנחנו מריצים על המחשב. כתבתי לפני כמה ימים על הרעיון של שימוש בכלים מתוך מודל שפה. למדנו שם שמודל שפה בסך הכל קורא וכותב טקסט, ורוב כלי ה AI שיש לנו משלבים מודל שפה עם קוד שעוטף את אותו, משפר קצת את הקלט לפני שהוא נשלח למודל השפה ועושה משהו עם הפלט של מודל השפה. בדוגמת הכלים ראינו שאנחנו יכולים לשכנע את מודל השפה לשתול "רמזים" בפלט שלו, ואז הסוכן שעוטף את מודל השפה יכול לתפוס רמזים אלה ולהשתמש בהם כדי להריץ קוד.
פרוטוקול MCP הוא בסך הכל דרך פורמלית בה סוכן מבקש ממודל שפה לשתול את אותם רמזים ומפענח את הפלט של מודל השפה.
בפרוטוקול MCP אנחנו מגדירים יכולות לפי קטגוריות:
1. כלים - יכולות שגורמות לשינוי כלשהו בעולם
2. משאבים - יכולות שקשורות למשיכת מידע לקריאה בלבד
3. פרומפטים - יכולות שקשורות לקיצורי דרך ב UI
לכן אם אני רוצה שאפליקציית Claude Desktop שרצה אצלי על המחשב תוכל ליצור קבצים על הדיסק אני יכול ליצור "כלי" שיוצר קבצים. אם אני רוצה שמתוך צ'ט עם האפליקציה היא תוכל לקרוא תוכן של קבצים מתיקייה מסוימת אני מוסיף את הקבצים בתור משאב לקריאה.
אפשר לחשוב על "שרת MCP" בתור תוסף לאפליקציית שיחה עם מודל שפה, ומה שיפה בו שהוא מתחבר באותו אופן להרבה מאוד אפליקציות שיחה, בדוגמה שלנו גם Claude Desktop תומך ב MCP אבל גם Cursor ולכן בדיוק באותו פרוטוקול אני יכול לתת ל Cursor אופציה להריץ קוד אוטומטית או לקרוא מידע מבסיס הנתונים.
דוגמה: שרת MCP ליצירת קבצים
בשביל הדוגמה כדאי להתקין את אפליקציית Claude Desktop (זה בחינם ועובד גם עם התוכנית החינמית של קלוד). לאחר מכן מתוך סביבה וירטואלית חדשה של פייתון אני יוצר את הקובץ הבא:
* server.py *
import os
from mcp.server.fastmcp import FastMCP
HOME = os.environ["HOME"]
* Create an MCP server *
mcp = FastMCP("Demo")
* Add an addition tool *
@mcp.tool()
def create_empty_file(name: str):
"""creates a new empty file in the home directory"""
try:
filename = f"{HOME}/{name}"
print(f"Trying to create file {filename}")
with open(filename, 'a', encoding='utf8'):
pass
except Exception:
raise Exception(f"Failed to create file {filename}")
@mcp.prompt()
def review_code(code: str) -> str:
return f"Please review this code:\n\n{code}"
הקובץ מכיל כלי אחד ו prompt אחד. הכלי נקרא create_empty_file. יש לו פרמטר בשם name והוא פשוט יוצר קובץ ריק חדש בתיקיית הבית בשם שבחרתי. מחרוזת התיעוד של הפונקציה נשמרת בתור התיעוד של הכלי.
בנוסף הכלי מכיל פרומפט בשם review_code שכולל פרמטר אחד בשם code ומחזיר פרומפט סטנדרטי שעוטף את הקוד שהעברתי.
שימוש בשרת החדש
בשביל להתקין את השרת החדש בקלוד אני מריץ משורת הפקודה:
pip install "mcp[cli]"
ומקבל את הכלי mcp בתוך סביבת הפייתון הוירטואלית שלי. עכשיו אני מפעיל:
mcp install server.py
בשביל לחבר את השרת שלי לאפליקציית Claude Desktop.
(באפליקציות אחרות ההתקנה תהיה שונה כל אפליקציה עם השיטה שלה).
אחרי ההתקנה מפעילים מחדש את קלוד ואפשר לבקש ממנו בצ'אט ליצור קבצים. קלוד יזהה שיש לו Tool שיודע ליצור קבצים ויציע להפעיל את הכלי ואחרי אישור שלכם בתוך הצ'אט הכלי מופעל והקובץ או הקבצים נוצרים.
מה לגבי הפרומפט? אותו אנחנו מפעילים דרך התפריטים. באפליקציית Claude Desktop בדיוק מתחת לשורת הצ'אט יש אייקון של שני דברים מתחברים, לוחצים עליו ואז בוחרים ב Choose an integration את הפרומפט שיצרנו. יופיע מסך קופץ בו נתבקש להקליד ערך לפרמטר code (הפרמטר שהגדרנו לפונקציה) ואחרי אישור המחרוזת שחזרה מהפונקציה נשלחת למודל השפה בתור הפרומפט.
מה הלאה
כמה ניסויים שאני ממליץ גם לכם לעשות על מודל השפה ו MCP-
1. מודל השפה מחליט לקרוא לכלי כי תיאור הכלי נראה לו מתאים לבקשה שלנו לא בגלל שהוא יודע מה הכלי עושה. נסו לשנות את שם הפונקציה ותיאור הכלי כדי להבין מה עובד ומה לא עובד, ואיך אפשר לבלבל קצת את קלוד. אחרי זה נסו להוסיף עוד פונקציות עם שמות דומים או שונים ותראו איך קלוד מתמודד עם קונפליקטים. שימו לב גם שמספיק לשנות את קובץ ה server.py ולהפעיל מחדש את קלוד כדי שהוא ישתמש בכלים החדשים, כלומר מי שמריץ את השרת זה אפליקציית קלוד עצמה.1 417
תזכורת: מה אתה אוהב בעצם בפיתוח תוכנה
אל מול גלי ה AI אני מרגיש שחובה להזכיר:
1. אני אוהב לקרוא קוד ולהבין אותו.
2. אני אוהב להבין למה מערכת מתנהגת כמו שהיא מתנהגת, ואיך היא תישבר. זאת הסיבה שהגעתי לפיתוח תוכנה. זה מה שמשאיר אותי כאן.
3. אני אוהב לפתור בעיות שקשורות לייצוג ידע ולהבין איך לייצג את המידע שלי בצורה שתאפשר לי לענות מהר על שאלות מעניינות.
4. אני אוהב לחשוב איך דברים הולכים להשתנות בעתיד, ואיך לארגן את המידע והקוד כדי שיהיה קל להתאים את המערכת לשינויים עתידיים.
אם כשאתם אומרים שעם AI אני אבנה מערכות פי 10 יותר מהר אתם מתכוונים שה AI יבנה מערכת בלי שאצטרך לחשוב על כל המשמעויות של כל שורת קוד אני חושש שכולנו הולכים להתאכזב.
המשימה שלנו היום היא להבין איזה חלקים בעבודה שלנו ה AI יכול לייעל, ואיזה חלקים זה רק נראה כאילו הוא מייעל אבל בעצם הוא שותל בקוד מוקשים שיהיה לנו קשה לנטרל בעתיד.
1 417
מי זה אנדריי?
ככל שאני עובד עם קופיילוט אני מקבל השלמות מעניינות: משתנה סביבה מאותחל לתיקיית בית של איזה אנדריי, מפתח API מאותחל לערך לא חוקי אבל שמתאים לפורמט, שם משתמש וסיסמה מקבלים השלמה אוטומטית ל admin ו admin.
ברור איך זה קורה - מודל שפה לא יודע להבדיל בין מידע אישי למידע ציבורי, הוא לא "מבין" מה מותר ומה אסור להציע. ברור שהחברות שמפעילות את מודלי השפה מתקינות עליהם מנגנוני הגנה וצנזורה אבל אי אפשר לתפוס הכל.
כדאי לשים לב:
1. עולם ה AI הוא חדש ובכל עולם חדש יש אינסוף בעיות אבטחת מידע. זה ש Cursor שמים מצב "פרטיות" לא אומר שהקוד שלכם נשאר על המחשב שלכם (זה בסך הכל אומר שקרסר לא שומרים אצלם את הקוד שלכם).
2. קוד לא מאובטח נכתב וייכתב ככל שניתן ל AI לכתוב יותר קוד. אנחנו עדיין לא יודעים מה יהיו הבעיות האופייניות מבחינת אבטחה לקוד ש AI כותב. העולם הזה דינמי מאוד.
3. פרוטוקולים מאובטחים לשימוש בכלים עדיין לא קיימים. ל MCP יש בעיות וכבר אנחנו רואים מתקפות שמתבססות על התקנת כלים זדוניים ב IDE.
במערכות רגישות עדיין כדאי להשתמש בממשק ה Chat ולהקפיד על בקרה על הקוד שמדביקים אצלו ומעתיקים ממנו. בקצה השני בפרויקטי צד ו POC-ים כמובן שאין בעיה להשתולל עם כלים כי אין מה שיישבר. כל מי שבאמצע צריך לקבל היום החלטות קשות ולאזן בין הרצון להשתמש בכלי הפיתוח הכי מתקדמים לצורך בשמירה על סטנדרטים של פיתוח מאובטח.
1 417
import { useRouter } from 'next/navigation';
import { RefreshContext } from '@/lib/refresh-provider';
export default function AutoRefresh() {
const router = useRouter();
const addClients = use(RefreshContext)
useEffect(() => {
addClients(1);
return () => addClients(-1);
}, [router]);
return null;
}
עכשיו לא משנה כמה פעמים נוסיף אותו לעמוד ה interval ירוץ רק פעם אחת, וכשלא יהיו יותר אלמנטי Auto Refresh על העמוד הריענון האוטומטי ייפסק.1 417
ריענון אוטומטי למידע צד שרת ב next.js
קומפוננטות צד שרת ב node יכולות לעשות דברים מופלאים, למשל לקרוא קובץ ממערכת הקבצים ולהציג אותו בקלות על המסך או לקרוא מידע מבסיס הנתונים. הבעיה מתחילה כשנתוני צד השרת האלה מתעדכנים. פעם כשהיתה לנו קומפוננטת צד לקוח שקראה את המידע עם react-query ידענו לרענן את המידע כל כמה שניות או לבנות Web Socket כדי לקבל עדכון מהשרת כשהמידע מתעדכן. בפוסט היום נראה דוגמה קצרה איך לעשות דבר דומה ובקלות בקומפוננטות צד שרת.
קומפוננטת צד שרת שמציגה תוכן של קובץ
הקוד הבא ב
page.tsx הוא קומפוננטת צד שרת שמציגה תוכן של קובץ ולידה תיבת טקסט:
import fs from 'node:fs/promises';
export default function Home() {
const data = fs.readFile('README.md', 'utf-8')
return (
<div>
<input type="text" />
<div>
{data}
</div>
</div>
)
}
ריענון אוטומטי מתוך קומפוננטת צד לקוח
כאשר התוכן על הדיסק משתנה אין שום שינוי על המסך. המשתמש יראה את התוכן החדש רק כשילחץ ריענון. לפעמים זה מספיק טוב אבל לפעמים אנחנו כן רוצים עדכון אוטומטי. דרך אחת קלה להציג תמיד את המידע העדכני היא להפעיל "reload" של next באופן יזום. אני כותב קומפוננטת צד לקוח חדשה בשם AutoRefresh עם התוכן הבא:
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
export default function AutoRefresh() {
const router = useRouter();
useEffect(() => {
const interval = setInterval(() => {
router.refresh();
}, 1000);
return () => clearInterval(interval);
}, [router]);
return null;
}
ומוסיף את הקומפוננטה ל page:
import fs from 'node:fs/promises';
import AutoRefresh from './auto-refresh';
export default function Home() {
const data = fs.readFile('README.md', 'utf-8')
return (
<div>
<AutoRefresh />
<input type="text" />
<div>
{data}
</div>
</div>
)
}
ועכשיו הדפדפן שלי מושך כל שניה את הקומפוננטה המרונדרת מהשרת ומעדכן את העמוד עם התוכן החדש. כיף לשים לב שאין דריסה של ה input, כלומר קומפוננטות צד לקוח שעודכנו בצד הלקוח ישמרו על ערכן למרות הריענון.
מה הלאה
כמובן שטעינה מחדש כל שניה יוצרת עומס מיותר על השרת. אנחנו יכולים לשפר את המנגנון אם נוסיף SSE או Websockets שישלחו ללקוחות הודעה כל פעם שיש שינוי במידע בצד שרת, ואז הדפדפן יטען מחדש את העמוד רק אחרי שמקבל הודעה כזו.
החיסרון השני בגישה זו הוא שלא נרצה לשים את קומפוננטת AutoRefresh יותר מפעם אחת ביישום. מנגנון יותר מוצלח יהיה להשתמש ב Provider כדי להפעיל את האפקט ולשמור כמה קומפוננטות צריכות את הריענון. כל קומפוננטה שתשלב את AutoRefresh תעלה את המונה ב-1 ואותו Provider יפעיל את הטיימר רק אם מספר הקומפוננטות שמשתמשות ב AutoRefresh בעמוד גדול מ-1.
בגישת ה Provider אני כותב קובץ בשם refresh-provider.tsx עם התוכן הבא:
'use client';
import { useEffect, useState, createContext } from 'react';
import { useRouter } from 'next/navigation';
export const RefreshContext = createContext((n: number) => {})
export default function RefreshProvider({ children }: { children: React.ReactNode }) {
const [clients, setClients] = useState(0);
const router = useRouter();
const addClients = (n: number) => setClients(c => c + n);
useEffect(() => {
if (clients > 0) {
const timer = setInterval(() => {
router.refresh();
}, 1000);
return () => {
clearInterval(timer);
}
}
}, [router, clients > 0]);
return (
<RefreshContext value={addClients}>
{children}
</RefreshContext>
);
}
אני מעדכן את layout.tsx כדי להשתמש בפרוביידר:
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={\${geistSans.variable} ${geistMono.variable}\}>
<RefreshProvider>
{children}
</RefreshProvider>
</body>
</html>
);
}
ומעדכן את auto-refresh באופן הבא:
'use client';
import { useEffect, use } from 'react';1 417
פיתרון Advent Of Code 2024 יום 7 - לא להיבהל, אני יודע לשנות את Integer
יום 7 של Advent Of Code לא היה מסובך והזכיר לי כמה כיף שאפשר ברובי לשנות את המחלקות המובנות בשפה. בואו נראה מה היה שם.
האתגר
נתון קלט שמורכב מתוצאה ואופרנדים:
190: 10 19
3267: 81 40 27
83: 17 5
156: 15 6
7290: 6 8 6 15
161011: 16 10 13
192: 17 8 14
21037: 9 7 18 13
292: 11 6 16 20
אנחנו צריכים למצוא אם אפשר להוסיף את האופרטורים פלוס וכפול בין האופרנדים כדי להגיע לתוצאה. בחלק השני של האתגר מוסיפים לנו אופרטור בשם || שמשמעותו "שרשור" שני אופרנדים כך ש:
10 || 19 = 1019
ורוצים לראות אם אפשר להגיע לתוצאה עם האופרטור החדש.
פיתרון
החלק המרכזי של הפיתרון לקח בסך הכל מספר שורות ברובי:
def count_options(target, values)
results = [:+, :*].repeated_permutation(values.size - 1).to_a.map do |seq|
values[1..].zip(seq).reduce(values[0]) { |a, (v, op)| a.send(op, v) } == target
end
results.count(true)
end
הפונקציה repeated_permutation מופעלת על מערך ומחזירה את כל הפרמוטציות עם חזרות על האיברים שלו. הנה דוגמה:
3.3.5 :003 > [1, 2].repeated_permutation(3).to_a
=> [[1, 1, 1], [1, 1, 2], [1, 2, 1], [1, 2, 2], [2, 1, 1], [2, 1, 2], [2, 2, 1], [2, 2, 2]]
מה עושים? לוקחים את כל הפרמוטציות של האופרטורים ושותלים אותן ברשימת האופרנדים עם zip. אחרי זה מפעילים reduce כדי לרוץ על הרשימה ולבצע את הפעולות שהגרלנו ובסוף בודקים אם הגענו לתוצאה.
בשביל לקרוא את הקלט היה צריך רק עוד ביטוי רגולארי:
puts (File.readlines('input.txt').map do |line|
target, values = line.match(/(\d+): (.*)/).captures
count_options(target.to_i, values.split.map(&:to_i)).positive? ? target.to_i : 0
end.sum)
ומה עם החלק השני? מאוד פשוט, צריך רק להוסיף מתודה ל Integer:
class Integer
def op_concat(other)
(to_s + other.to_s).to_i
end
end
ואז אפשר להישאר עם אותה פונקציה קצרה ורק לשנות את מערך האופרטורים:
def count_options(target, values)
results = [:+, :*, :op_concat].repeated_permutation(values.size - 1).to_a.map do |seq|
values[1..].zip(seq).reduce(values[0]) { |a, (v, op)| a.send(op, v) } == target
end
results.count(true)
end
סך הכל הפיתרון המלא ברובי עם פחות מ 20 שורות היה:
class Integer
def op_concat(other)
(to_s + other.to_s).to_i
end
end
def count_options(target, values)
results = [:+, :*, :op_concat].repeated_permutation(values.size - 1).to_a.map do |seq|
values[1..].zip(seq).reduce(values[0]) { |a, (v, op)| a.send(op, v) } == target
end
results.count(true)
end
puts (File.readlines('input.txt').map do |line|
target, values = line.match(/(\d+): (.*)/).captures
count_options(target.to_i, values.split.map(&:to_i)).positive? ? target.to_i : 0
end.sum)
ואחרי שנתתי לקלוד לשפר קיבלתי את זה:
class Integer
# Method to concatenate two numbers as strings and convert back to integer
def op_concat(other)
(to_s + other.to_s).to_i
end
end
def count_options(target, values)
# Early return if only one value
return (values[0] == target ? 1 : 0) if values.size == 1
# Define available operations
operations = [:+, :*, :op_concat]
# Count combinations that equal the target
operations.repeated_permutation(values.size - 1).count do |ops|
result = values[0]
values[1..].zip(ops).each do |value, op|
result = result.send(op, value)
end
result == target
end
end
def solve_file(filename)
# Read the file and process each line
File.readlines(filename).sum do |line|
if line.strip.empty?
0 # Handle empty lines
else
# Parse the target and values
match = line.match(/(\d+):(.*)/)
if match
target = match[1].to_i
values = match[2].strip.split.map(&:to_i)
# If there's at least one way to reach the target, count that target
count_options(target, values).positive? ? target : 0
else
0 # Handle invalid lines
end
end
end
end
* Execute the solution if run as a script *
متاح الآن! بحث تيليغرام 2025 — أهم رؤى العام 
